Authentication

With a valid session established, users will enter their usernames and passwords and hit the Login button in order to have their identity verified.

This is done through the MSG_USER_AUTHEN_V3 request that will be crafted and sent by clients.

ClientKey1

ClientKey1, commonly abbreviated as CK1, is a string generated by the client as a hash of the user's password tied to the current session to avoid data replay from captured authentication requests.

The algorithm goes as follows, with password being the user's plaintext password input, sid being the cached Session ID for the connection, time_secs and time_millis being seconds since epoch and subsecond millis extracted from the cached Session Offer timestamp, respectively:

from base64 import b64encode as base64
from hashlib import sha512

# Produce a base64-encoded SHA512 hash of the password and hash that again.
state = sha512(base64(sha512(password).digest()))
# Mix in salt built from previously cached session information.
state.update(f"{sid}{time_secs}{time_millis}")

# Receive CK1 as the base64-encoded hash of *password hash* and *salt*.
clientkey1 = base64(state.digest())

REC1 (serverbound)

REC1 is a field in the aforementioned MSG_USER_AUTHEN_V3 data message that holds a record of the user's credentials.

The actual data there is encrypted with Twofish in OFB mode.

The logic used to derive Twofish keys and IVs for this operation is:

func generateIV() []byte {
    const ivConstant = 0xB6

    iv := make([]byte, 16)
    for i := 0; i < len(iv); i++ {
        iv[i] = ivConstant - byte(i)
    }

    return iv
}

func generateKey(sid uint16, timeSecs uint32, timeMillis uint32) []byte {
    const keyConstant = 0x17

    key := make([]byte, 32)
    for i := 0; i < len(key); i++ {
        key[i] = keyConstant + byte(i)
    }

    le := make([]byte, 4)

    binary.LittleEndian.PutUint16(le, sid)
    key[4] = le[0]
    key[5] = le[2] // This is always zero
    key[6] = le[1]

    binary.LittleEndian.PutUint32(le, timeSecs)
    key[8] = le[0]
    key[9] = le[2]
    key[12] = le[1]
    key[13] = le[3]

    binary.LittleEndian.PutUint32(le, timeMillis)
    key[14] = le[0]
    key[15] = le[1]

    return key
}

With this at hand, the actual Record can be built and encrypted:

func buildRecord1(username string, ck1 string, sid uint16, timeSecs uint32, timeMillis uint32) []byte {
    key := generateKey(sid, timeSecs, timeMillis)
    iv := generateIV()

    // Prepare the Twofish OFB context for later encryption
    block, _ := twofish.NewCipher(key)
    stream := cipher.NewOFB(block, iv)

    // Build the plaintext Record we would like to send
    record := fmt.Sprintf("%v %v %v", sid, username, ck1)

    // Encrypt and return the record
    stream.XORKeyStream(record, record)
    return record
}

Servers will need to decrypt the received record, split the plaintext at whitespace and parse it as Session ID, Username, ClientKey1 which can be accordingly validated by deriving the CK1 for the password hash stored along with the username.

REC1 (clientbound)

After successful validation of an authentication request, the server is expected to respond with a MSG_USER_AUTHEN_RSP message.

This contains the unique UserID for the account, a PayingUser boolean denoting whether the account has active membership, and yet another Rec1 string.

It is Twofish OFB encrypted as well and uses the same keys and IVs as showcased above. Clients should use the same logic for decrypting that string.

The Record is yet another base64-encoded string of 64 bytes, referred to as ClientKey2. The exact algorithm in use on KI's serverside remains unknown.

Note that ClientKey2 is, in itself, a secret value. It is session-agnostic and the mere knowledge of it is proof of identity to the server that will be validated when entering the game. Implementors are encouraged to use a suitable CSPRNG to generate random data and invalidate actively cached CK2s in fixed intervals, forcing users to re-authenticate.

When everything went smoothly, WizardGraphicalClient will be launced with the -U flag.

Errors

Many different errors may occur during authentication. For that purpose, MSG_USER_AUTHEN_RSP features an Error field which holds a String ID.

Below is a non-exhaustive list of known codes and when they should be sent:

StringWhen to send
""No error
"AccountBanned"User's account is banned
"MachineBanned"User's machine is banned
"AuthenFailed"Invalid credentials or internal server error
"AISNoLogin"Chinese Anti-Indulgence System (legacy)
"Timeout"Timeout while trying to process the request
"FtpCapped"???
"ErrorNoLock"???
"FailedUpload"???

Legacy Authen

The login protocol also features MSG_USER_AUTHEN and MSG_USER_AUTHEN_V2. They are no longer supported and server implementations must ignore these requests.