- Java 100%
Yippers Authentication & Encryption Rework
This project aims to replace the Minecraft authentication and encryption scheme with a protocol that is:
- Not dependent on Mojang's servers
- More cryptographically boring (in some ways)
WARNING: I AM DEFINITELY NOT A CRYPTOGRAPHER. I'm just trying my best here and this will mostly be a learning exprience; there will likely be exploits. You have been warned!
Overview
Authentication works sort of like it does in TeamSpeak 3, where the client gets a private/public key pair and authenticates to the server using that.
Instead of using RSA for exchanging the encryption key, an ECDH (X448) key exchange with ephemeral keys are used instead, which is itself signed by the server's public key (Ed448) to verify authenticity.
Server public keys follow a "trust on first use" model: the first time a server is contacted, it's public key is blindly trusted, but is verified for every session after that, similar to SSH. Server IPs can also be optionally have the public key appended to them, similar to Onion URIs, to avoid needing to blindly trust the server the first time as long as the public key has been accquired over a trusted channel.
A "nonce" derived from the ephemeral DH key is also signed by the client to prove it holds a given private key for the public key it wants to authenticate to the server with, providing authentication without passwords.
The unauthenticated long-running AES encryption used for packet encryption is replaced with a per-packet, ChaCha20-Poly1305 encryption which authenticates each packet before it is processed.
In this protocol the "Encrypt", "Login", and "Set Compression" steps from the traditional protocol are done together. Compression is also always implied to be required if the version supports it.
Java 11 is required for JEP 324 (Curve448) and JEP 329 (ChaCha20-Poly1305) support. Minecraft versions requiring older Java versions probably won't be supported just yet. I would personally also like to support Better than Adventure! and beta versions in the future too.
Post-quantum cryptography would probably be another consideration, and really ought to be added before this leaves any kind of beta state.
Threat Model
We assue the attacker can:
- Sniff packets on the network
- Modify packets as they are being sent, including adding and deleting arbitrary packets
- Sit in the middle of a connection
Under this model, so long as the user has the correct public key for the server, the only thing that should happen is the attacker completely blocking the connection.
Proxy-in-the-Middle Attack
We would preferably like to protect against unrelated servers authenticating to a different server by acting as a proxy server. With a completely random challenge (like TeamSpeak?), the proxy server could just pass the challenge on to the client as its own and get the signed result, this allowing the proxy to authenticate to the server as the client.
Instead of rolling a random challenge to sign, we make the authentication challenge (e.g. the random number the client should sign to prove it has the private key) tied to the master encryption key used; that is, the key that comes out of the ephemeral key exchange should be used to derive the challenge.
The problem is solved since:
- The proxy could try to connect as a proxy like normal, but then the client and server will have a key mismatch, leading to the client signing a different challenge than the server is expecting it to sign, and the authentication will fail as a result.
- The proxy could try to pass along the server and/or client's ephemeral ECDH key, but it won't know the encrpytion key by the end of it.
... unless of course you think of an attack that I cannot! Which, please do report if so, by the way :>
Packet format
When purple compression and encryption is enabled
After establishing the encrypted connection, packets have the following format:
| Type | Field | Notes |
|---|---|---|
| VarInt | packet_length |
The total length of this packet, excluding this field. |
| Bytes | packet_data |
Normal data of the packet with the packet length stripped off the front. For compatiblity, the correct packet length will be prepended after decryption. |
| Bytes[16] | tag |
The authentication tag for this packet |
This applies to both compressed and uncompressed packets, which use the vanilla compression scheme.
Unlike vanilla MC encryption, every packet is encrypted individually. Packet data is encrypted with ChaCha20 and authenticated with Poly1305 using Java's built in support for the ChaCha20-Poly1305 AEAD cipher, which follows RFC 8439. The key is derived from the ECDH shared secret using HKDF with SHA256, and is the same for every packet. The nonce (IV) is based on the number of packets sent by the client or sever up to this point: serverbound and clientbound packets each have their own packet counters, which are then shifted left one and the least significant bit is set to 0 for serverbound packets or 1 for clientbound packets to form the nonce.
The authentication tag does add some overhead; however, attacks that modify the packets don't work at all instead of leading to a corrupt bitstream "eventually". (In vanilla MC encryption, while this kind of attack will end up corrupting the next packet, the current packet can still be processed and possibly lead to unintended command execution if the packet is short enough.)
Modified Login Flow
Unless noted, the other peer is assumed to wait for the other peer's next step.
- C → S: Purple Serverbound Handshake
- S → C: Purple Clientbound Handshake
- Derive keys and enable encryption
- S → C: Purple Server Authentication
- Client MUST verify the server's public key, or save it for the first time
- C → S: Purple Client Authentication
- Server authenticates the client
- Normal session starting from Login Success packet (albeit with encryption changes)
(1) (the server and client key exchanges are sent in parallel)
Packets
Purple Serverbound Handshake
| Packet Identifier | State | Bound to |
|---|---|---|
knot:purple_serverbound_handshake |
Handshake | Client |
| Field name | Field type | Notes |
|---|---|---|
PurpleProtocolVersion |
VarInt | Version of the Purple Protocol used by the client. |
EphemeralPublicKey |
Prefixed Array of Byte | Client's side of the ephemeral X448 handshake |
Purple Clientbound Handshake
| Packet Identifier | State | Bound to |
|---|---|---|
knot:purple_clientbound_handshake |
Handshake | Server |
| Field name | Field type | Notes |
|---|---|---|
PurpleProtocolVersion |
VarInt | Version of the Purple Protocol used by the server. |
EphemeralPublicKey |
Prefixed Array of Byte | Server's side of the ephemeral X448 handshake |
Purple Server Authentication
| Packet ID | State | Bound to |
|---|---|---|
knot:purple_server_authentication |
Handshake | Client |
| Field name | Field type | Notes |
|---|---|---|
Authentication |
Authentication Data | Authentication data, used to authenticate the server to the client |
Purple Client Authentication (Login or Transfer)
| Packet ID | State | Bound to |
|---|---|---|
knot:purple_client_login |
Handshake | Server |
| Field name | Field type | Notes |
|---|---|---|
Authentication |
Authentication Data | Authentication data, used to authenticate the client to the server |
MinecraftProtocolVersion |
VarInt | The Minecraft protocol version of the client |
ServerAddress |
String (max 255) | Hostname that was used to connect to the server, as in Handshake |
WantedProfile |
Purple Profile | Profile data, incidating what the client would like to appear as |
Structures and Data Types
UUID
The UUID of players on servers using the YipItsAuth protocol shall be a random UUID (UUIDv4), just as online players have. The only difference is that this UUID will change between severs, making it only useful as an identifier within the server/world itself and not between servers.
Game Profile
This type is from Mojang's authlib, and is now generally avoided where possible. Because it is still used by some aspects of the game, it is not completely replaced or modified in any way.
Purple Profile
Instead of using the Game Profile, a new Purple Profile type exists which has the following fields:
| Field name | Field type | Notes |
|---|---|---|
UUID |
UUID | The meaning of this field changes depending on use. When sent by the client before the server has given its UUID, it is their offline UUID (or online UUID, if available) and is used for migrating accounts to Purple Authentication, if that UUID has not already been claimed by an identity and already exists on the server. When sent in any other context, this is the UUID of the player as given by the server. |
Username |
String (16) | Username of the player. When sent by the client at the initial login, this is the wanted username if the user isn't already registered. The server is free to ignore this and send the already-bound username of the client instead. |
SkinData |
Prefixed Array of Byte | PNG skin data to use instead of that present in the profile. May be empty. |
CapeData |
Prefixed Array of Byte | PNG skin data to use instead of that present in the profile. May be empty. |
ModelType |
VarInt | Type of model used for skin (e.g. classic or slim) |
Since Minecraft skin/cape/elytra textures are tiny images, I don't see it as a huge problem that they are sent over the network everytime the profile is loaded. My Tails skin for example is only 2KiB, which is much less than even a chunk.
Authentication Data
| Field name | Field type | Notes |
|---|---|---|
PublicKey |
Prefixed Array of Byte | The client or server's public key. On the client side, this will be ignored if the client already knows the server's public key. |
Signature |
Prefixed Array of Byte | A signature of the shared encryption key. This is used by the server to prove that the client has the private key for the given public key, and vice verca. Since the server has infulenced the random value with its side of the key exchange, it can be fairly confident in its uniqueness. |