Where the encryption key lives
An earlier post described how DAoC encrypts its traffic, read out of OpenDAoC's source. That code implements the scheme from client 1.110: the server sends its public key in its first reply, the client encrypts a random session key with it and sends it back. We wrote that up as settled.
Then we recorded a session from the current official client against an official server and looked at it. The current client is 1.130. The handshake it does is not the one in the source, and the difference is interesting enough to write down, because as far as we can tell nobody else has.
The server sends no key
In the 1.110 scheme the first server packet carries the RSA public key. In our recording it
doesn't. The server's first game packet is the version reply — opcode 0x22 — and its whole
payload is the version string 1.130b followed by a two-byte build number. There is no key in
it, and there is no later packet that could be one: the very next thing on the wire is from the
client, and it's already encrypted.
So the client starts encrypting before the server has sent it anything to encrypt with. That only works if the client already holds the key it needs.
Which means the public key is in the client
The piece the client must already have is the server's public key. The exchange is still the same shape as before — the client picks a random session key for the connection, encrypts it under the server's public key, and sends it — but the public key now ships inside the client instead of arriving over the wire. The client's first message is about 192 bytes, which is exactly one block of 1536-bit RSA, consistent with a wrapped key.
This is called key transport: one side chooses the session key and delivers it, rather than both sides computing it together. The client can do it in its first packet, with no round trip, which is exactly what we see — it encrypts 2–3 milliseconds after its first message, before any reply comes back.
Why move the key into the client?
Putting the public key in the client closes a gap. If the server announces its key at the start of every connection, someone sitting between client and server can replace it with their own, read everything the client sends, and pass it along re-encrypted. A key built into the client can't be swapped that way without changing the client itself.
There's a cost on the other side: anyone who ever obtains the server's private key can decrypt every past recording, because every session key was sent under that one public key. A scheme where both sides contribute to the key (Diffie-Hellman) wouldn't have that property. DAoC chose the simpler, swappable-resistant option.
What this does and doesn't let us do
The practical answer from last time still holds, for a sharper reason. There is no shared secret hidden in the client — the thing in the client is a public key, which by definition can't decrypt anything. Each session key is random and is sent wrapped so only the server's private key opens it. A recording on its own can't be decrypted, and finding the key inside the client wouldn't help. That's why the old packet loggers read the live session key out of the running client's memory instead.
We're not taking the official client apart, so none of that is our path. What the recording does give us, even fully encrypted, is the shape of the traffic: how big each packet is, how often they go out, which ones repeat. That's enough to line retail up against our own unencrypted client and learn a surprising amount without reading a single payload — which is the next post.
For the record
- The 1.110 scheme in OpenDAoC's source and the 1.130 handshake on the wire are different: the server used to send its public key; now it doesn't, so the key lives in the client.
- Supporting a modern encrypting client on an open server would mean implementing this handshake server-side. We've filed that as future work — it's a long way from our current goal of a working client, so it waits.
- Details and the open questions are in our open questions (R1, R6).