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:
String | When 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.