Shadowburn Project

A core for vanilla private servers


17 Oct 2018 by Friate

Logging in with Vanilla

This is going to be a deep dive on how authentication works with Vanilla. This will also go over some of the authentication code used in the Shadowburn Project. Hopefully this post will be helpful for anybody else interested in learning about how Vanilla authenticates accounts. Another great article has been written that includes how encryption and decryption of the packet headers works, which happens after login is completed.

Vanilla Login Protocol

The Vanilla client uses the SRP6 protocol to authenticate accounts connecting to the server. When implemented properly, no password hashes need to be stored on the server or transmitted to/from the client. What does need to be stored on the server is the account’s salt and verifier.

There’s a Packet Reference below which explains all of the information that’s passed back and forth between the client and the server when the user clicks the Login button. That, along with the SRP6 design, is just enough to get a very basic understanding of how the SRP protocol works.

Creating an Account

Before authentication can be attempted, an account needs to be setup with SRP in mind. As mentioned above, a salt and a verifier will need to be generated and stored with the account in order to authenticate with SRP.

Generating the Salt

The salt is simply 32 random bytes that should be cryptographically strong. In Elixir, this is done by calling OTP’s :crypto module.

salt = :crypto.strong_rand_bytes(32)

That was easy enough.

Generating the Verifier

The verifier is a little more involved. The SRP design defines the verifier as:

n = A large safe prime (n = 2q+1, where q is prime)
generator = A generator modulo n
x = Hash(salt, password)
v = (generator^x) % n

Shadowburn currently uses the same constants for n and generator that are defined in MaNGOS. Since these values are passed to the client during authentication, they could potentially be changed in the code, however any accounts already created with the old values would no longer be able to be authenticated.

Vanilla uses sha1 for the hashing function. It also follows this formula very closely, however it defines x slightly differently.

After applying the changes mentioned, this is the new definition.

n = 894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7
generator = 7
x = sha1(salt, sha1(upper("<username>:<password>")))
v = (generator^x) % n

The code to do this in Elixir looks very similar to the above.

hash = :crypto.hash(:sha, String.upcase(username <> ":" <> password))
x = :crypto.hash(:sha, salt <> hash))
verifier = :crypto.mod_pow(@g, x, @n)

With those two values generated and stored with the account, the account is ready for authentication.

Authentication Flows

In Shadowburn, each new connection to the authentication server starts a finite-state machine which begins in the challenge state. There are two flows that the state machine can go through for the connection to end up in the authenticated state. The flow that is used is determined by the first packet received from the client.

With either authentication flow, the connection cannot move backwards to a previous state. Any failed attempt to move forward will result in the connection being closed and requires reconnecting and starting from the beginning.

Logon Flow

The first authentication flow, known as Logon Flow, verifies that the user knows the correct password and results in a session key being assigned to the session.

In this flow, the state will go from challenge to auth_proof to authenticated. These state transitions are described in detail below.

Moving from challenge to auth_proof

When the server receives the Client Logon Challenge Packet, it knows that the user is attempting to authenticate using the Logon Flow.

The server first checks that the account exists and that the account or IP has not been banned. It also checks if the account is locked to a particular IP address. If it is, then it checks that the connection is coming from that IP.

The server then retrieves the account’s verifier from the database and generates 19 random bytes which becomes private key b. The verifier and hard coded g and n is used along with b to generate the public SRP key B.

verifier = account.verifier
private_b = :crypto.strong_rand_bytes(19)
{public_b, _} = :crypto.generate_key(:srp, {:host, [verifier, @g, @n, :"6"]}, private_b)

After this has been generated, the server sends the client the Server Logon Challenge Packet, changes the state to auth_proof, and waits for the client to respond with the Client Logon Proof Packet to move forward.

It can be helpful to note that at this step, the server has sent the client the server’s public SRP key B which is ephemeral (used only for this authentication attempt), the account’s salt, and the hard coded values g and n.

Moving from auth_proof to authenticated

When the server receives the Client Logon Proof Packet it needs to perform a calculation using the information it has and arrive at the same client_proof hash that the client sent. If the hash that the server arrives at doesn’t match the hash that the client arrived at, then an invalid password was entered and we should notify the client and terminate the connection.

Once the server has the client’s public key A, it can generate the scrambler, which is done by hashing both the client and server’s public keys.

scrambler = :crypt.hash(:sha, public_a <> public_b)

With the scrambler, the server can begin to generate the session key. To start, it calls into OTP’s :crypto module to compute the key.

s = :crypto.compute_key(:srp, public_a, {public_b, private_b}, {:host, [verifier, @n, :"6", scrambler]})
session = interleave(s)

After this, it performs an interleave operation on the result, which takes the binary s and splits it into two binaries. The first contains all of the bytes in the even offsets, the the second contains all of the bytes in the odd offsets.

s = <<1, 2, 3, 4, 5, 6>>
t1 = <<1, 3, 5>>
t2 = <<2, 4, 6>>

It then hashes those two lists using sha1 and zips them together, so that the first byte from t1_hash is next to the first byte of t2_hash, until both hashes are interleaved.

t1_hash = <<1, 2, 3, 4, 5>>
t2_hash = <<5, 4, 3, 2, 1>>
interleaved = <<1, 5, 2, 4, 3, 3, 4, 2, 5, 1>>

This is what the interleave function looks like in Shadowburn.

defp interleave(s) do
  list = Binary.to_list(s)

  t1 = Binary.from_list(interleave_t1(list))
  t2 = Binary.from_list(interleave_t2(list))

  t1_hash = Binary.to_list(:crypto.hash(:sha, t1))
  t2_hash = Binary.to_list(:crypto.hash(:sha, t2))

  Binary.from_list(List.flatten(Enum.map(List.zip([t1_hash, t2_hash]), &Tuple.to_list/1)))
end

defp interleave_t1([a, _ | rest]), do: [a | interleave_t1(rest)]
defp interleave_t1([]), do: []
  
defp interleave_t2([_, b | rest]), do: [b | interleave_t2(rest)]
defp interleave_t2([]), do: []

At this point, the server has the session key. It now needs to perform some additional operations in order to get to a hash that it can compare to the hash the client sent as proof that it too has the session key.

To do this, the server takes the hash of n and the hash of g and does an xor operation on them called t3. It then gets the hash of the username called t4. Finally, it concatenates t3, t4, salt, A, B, and session together and does a final hash.

mod_hash = :crypto.hash(:sha, @n)
generator_hash = :crypto.hash(:sha, @g)
t3 = :crypto.exor(mod_hash, generator_hash)
t4 = :crypto.hash(:sha, username)
m = :crypto.hash(:sha, t3 <> t4 <> salt <> public_a <> public_b <> session)

If this hash m matches the client_proof that was sent by the client, then the server knows that the user has entered the correct password for this account.

It then generates a final hash called the server_proof which it sends back to the client in the Server Logon Proof Packet and changes the state to authenticated.

server_proof = :crypto.hash(:sha, public_a <> client_proof <> session)

Reconnection Flow

The second authentication flow, known as the Reconnection Flow, verifies that the client knows the session key.

In this flow, the state will go from challenge to recon_proof to authenticated. These state transitions are described in detail below.

Moving from challenge to recon_proof

When the server receives the Client Reconnection Challenge Packet, it knows that the user is attempting to authenticate using the Logon Flow.

The server first checks that the account exists and that the account or IP has not been banned. It also checks if the account is locked to a particular IP address. If it is, then it checks that the connection is coming from that IP.

The server then generates 16 random bytes known as the challenge_data and sends it to the client in the Server Reconnection Challenge Packet. The server changes the state to recon_proof and waits for the client to respond with the Client Reconnection Proof Packet.

challenge_data = :crypto.strong_rand_bytes(16)

It can be helpful to note that at this step, the server has only sent the client the 16 random bytes that the client will combine with other information that will be known to both the client and the server to prove that they have the session key.

Moving from recon_proof to authenticated

When the client receives the Server Reconnection Challenge Packet it generates random 16 bytes of it’s own which we refer to as the proof_data and sends that back, along with the client_proof back to the server in the Client Reconnection Proof Packet.

When the server receives the Client Reconnection Proof Packet it performs the same computation that the client did with the session key that it has stored for the account.

server_proof = :crypto.hash(:sha, username <> proof_data <> challenge_data <> session)

If the server_proof matches the client_proof, then the server knows that the client has the session key. The server changes the state to authenticated and notifies that client that everything succeeded with the Server Reconnection Proof Packet.

Authentication Achieved

Now that the account has been authenticated, the client will send a Client Realm List Packet to request a list of realms from the server. This request will come from the client periodically, every 15 seconds or so. The server will build up a response for the client to display.

realm_packets = Enum.map(realms, fn({realm, character_count}) ->
  << realm.realm_type :: little-size(32) >> <> 
  << realm.flags :: size(8) >> <> 
  realm.name <> << 0 >> <>
  realm.address <> << 0 >> <>
  << realm.population / realm.max_population :: little-float-size(32) >> <> # population
  << character_count || 0>> <> # number of characters
  << realm.timezone :: size(8) >> <>
  << 0 >> # unknown
end)

realm_list = Enum.reduce(realm_packets, << >>, fn(realm_packet, acc) -> acc <> realm_packet end)
realm_count = Enum.count(realms)

#               packet size                                   unknown     # of realms
packet = << 16, byte_size(realm_list) + 7 :: little-size(16), 0, 0, 0, 0, realm_count >> <> realm_list <> << 2, 0 >>

Packet Reference

This packet reference has been copied from WoWDev and has been modified to add more clarity.

Client Logon Challenge Packet

Offset Type Name Description
0x00 uint8 command 0x00
0x01 uint8 error  
0x02 uint16 packet size Length of the packet, minus 4 (the size of command, error, and packet size)
0x04 char[4] game ‘WoW’
0x08 uint8[3] version 0x01 0x01 0x02
0x0B uint16 build 4125
0x0D char[4] platform eg. ‘x86’
0x11 char[4] os eg. ‘Win’
0x15 char[4] country eg. ‘enUS’
0x19 uint32 worldregion_bias offset in minutes from UTC time, eg. 180 means 180 minutes
0x1D uint32 ip  
0x21 uint8 account_name_length  
0x22 char[account_name_length] account_name  

Server Logon Challenge Packet

Offset Type Name Description
0x00 uint8 command 0x00
0x01 uint8 unknown 0x00
0x02 uint8 error code Error Codes
0x03 char[32] B SRP public server key (ephemeral)
0x23 uint8 g length SRP generator length
0x24 uint8 g SRP generator
0x25 uint8 n length SRP modulus length
0x26 char[32] n SRP modulus
0x46 char[32] srp salt SRP user’s salt
0x66 char[16] crc salt A salt to be used in Client Logon Proof.

Client Logon Proof Packet

Offset Type Name Description
0x00 uint8 command 0x01
0x01 uint8[32] A SRP public client key (ephemeral)
0x21 uint8[20] client_proof  
0x35 uint8[20] crc_hash  
0x49 uint8 num_keys  

Server Logon Proof Packet

Offset Type Name Description
0x00 uint8 command 0x01
0x01 uint8 error Error Codes
0x02 uint8[20] server_proof  
0x16 uint32 unknown  

Client Reconnection Challenge Packet

This packet is the same as the Client Logon Challenge Packet, except the command is 0x02.

Offset Type Name Description
0x00 uint8 command 0x02
0x01 uint8 error  
0x02 uint16 packet size Length of the packet, minus 4 (the size of command, error, and packet size)
0x04 char[4] game ‘WoW’
0x08 uint8[3] version 0x01 0x01 0x02
0x0B uint16 build 4125
0x0D char[4] platform eg. ‘x86’
0x11 char[4] os eg. ‘Win’
0x15 char[4] country eg. ‘enUS’
0x19 uint32 worldregion_bias offset in minutes from UTC time, eg. 180 means 180 minutes
0x1D uint32 ip  
0x21 uint8 account_name_length  
0x22 char[account_name_length] account_name  

Server Reconnection Challenge Packet

Offset Type Name Description
0x00 uint8 command 0x02
0x01 uint8 error Error Codes
0x02 char[16] challenge_data random data, used as a challenge
0x12 uint64 unknown  
0x1A uint64 unknown  

Client Reconnection Proof Packet

Offset Type Name Description
0x00 uint8 command 0x03
0x01 char[16] proof data  
0x11 char[20] client proof  
0x25 char[20] unknown hash  
0x39 uint8 unknown  

Server Reconnection Proof Packet

Offset Type Name Description
0x00 uint8 command 0x03
0x01 uint8 error Error Codes

Client Realm List Packet

Offset Type Name Description
0x00 unint8 command 0x10
0x01 uint32 unknown null

Server Realm List Packet

The server responds with a packet composed of a Realm Information Header, as many Realm Information items as specified, and the Realm Information Footer.

Realm Information Header

Offset Type Name Description
0x00 uint8 command 0x10
0x01 uint16 size size of the rest of the packet, minus 3 (the size of command and size)
0x03 uint32 unknown null
0x07 uint8 number of realms Number of realms in the packet

Realm Information

Offset Type Name Description
0x00 uint32 realm_type 0 is normal, 1 is PVP
0x04 uint8 flags Realm Flags
0x05 char[] name Name of the realm (zero terminated string)
  char[] address Address of the realm “ip:port” (zero terminated string)
  float population Population value.
  uint8 num_chars The number of characters the account has on the server.
  uint8 time_zone  
  uint8 unknown  
Offset Type Name Description
0x00 uint16 unknown  

Realm Flags

Flags Meaning
0x01 Color the realm name in red.
0x02 Realm is offline.

Op Codes

Name Value Notes
LOGIN_CHALL 0x00  
LOGIN_PROOF 0x01  
RECON_CHALL 0x02  
RECON_PROOF 0x03  
REALM_LIST 0x10 Asks for a list of realms
XFER_INITIATE 0x30 unused
XFER_DATA 0x31 unused

Error Codes

Name Value
LOGIN_OK 0x00
LOGIN_FAILED 0x01
LOGIN_FAILED2 0x02
LOGIN_BANNED 0x03
LOGIN_UNKNOWN_ACCOUNT 0x04
LOGIN_UNKNOWN_ACCOUNT3 0x05
LOGIN_ALREADYONLINE 0x06
LOGIN_NOTIME 0x07
LOGIN_DBBUSY 0x08
LOGIN_BADVERSION 0x09
LOGIN_DOWNLOAD_FILE 0x0A
LOGIN_FAILED3 0x0B
LOGIN_SUSPENDED 0x0C
LOGIN_FAILED4 0x0D
LOGIN_CONNECTED 0x0E
LOGIN_PARENTALCONTROL 0x0F
LOGIN_LOCKED_ENFORCED 0x10