d73c1c9537
8 topical docs (overview, data types, lifecycle, handshake, status/ping, login+encryption, configuration, version-differences) + proxy-forwarding set + 16 per-version release-line docs, sourced from minecraft.wiki, ViaVersion (source + commits), minecraft-data, node-minecraft-protocol, Velocity, BungeeCord. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
620 lines
27 KiB
Markdown
620 lines
27 KiB
Markdown
# 05 — Login State, Encryption & Compression
|
||
|
||
> **Scope:** Java Edition 1.7.10 → latest (1.21.x). Covers the Login protocol state,
|
||
> the encryption handshake, Mojang session auth, packet compression, and the 1.19/1.20.2
|
||
> versioned deviations. All crypto details are sourced from the reference implementations
|
||
> listed in each section.
|
||
|
||
---
|
||
|
||
## 1. Login State Overview
|
||
|
||
The Login state begins immediately after the client sends a Handshake packet with
|
||
`next state = 2`. It ends when the server sends **Login Success** (and, since 1.20.2,
|
||
the client acknowledges it). All packets in this state use the standard framing:
|
||
|
||
```
|
||
<Packet Length: VarInt> <Packet ID: VarInt> <Data: bytes>
|
||
```
|
||
|
||
After **Set Compression** is negotiated (if at all), each packet gains an extra
|
||
**Data Length** VarInt (see §4). After **Encryption Response** is processed, the entire
|
||
TCP stream — including the Length prefix — is encrypted with AES/CFB8 (see §3).
|
||
|
||
---
|
||
|
||
## 2. Packet Reference
|
||
|
||
### 2.1 Login Start (0x00 Serverbound)
|
||
|
||
The first Login-state packet sent by the client.
|
||
|
||
| Field | Type | Version range | Notes |
|
||
|---|---|---|---|
|
||
| Username | String(16) | all | Player username, max 16 chars |
|
||
| Signature | Optional container | 1.19–1.19.2 | Profile public key; see §5 |
|
||
| └ Timestamp | i64 | 1.19–1.19.2 | Key expiry timestamp (ms since epoch) |
|
||
| └ Public Key | Prefixed byte array | 1.19–1.19.2 | DER SubjectPublicKeyInfo of player's key |
|
||
| └ Signature | Prefixed byte array | 1.19–1.19.2 | Mojang-signed; see §5 |
|
||
| Has UUID | Optional bool (not present in 1.20.2+) | 1.19.1–1.20.1 | Presence flag |
|
||
| Player UUID | UUID | 1.19.1+ | Optional (flag-gated) in 1.19.1–1.20.1; always present 1.20.2+ |
|
||
|
||
**Source:** `minecraft-data/data/pc/1.7/protocol.json`,
|
||
`minecraft-data/data/pc/1.19/protocol.json`,
|
||
`minecraft-data/data/pc/1.19.2/protocol.json`,
|
||
`minecraft-data/data/pc/1.20.2/protocol.json`;
|
||
`BungeeCord/protocol/src/main/java/net/md_5/bungee/protocol/packet/LoginRequest.java:28–39`
|
||
|
||
Version notes:
|
||
|
||
- **≤1.19.2:** `Signature` container present (profile public key for chat-signing); removed in 1.19.3.
|
||
- **1.19:** only `signature` field (no UUID).
|
||
- **1.19.2:** adds optional `playerUUID` after the signature.
|
||
- **1.19.1–1.20.1:** UUID is optional (preceded by a boolean flag).
|
||
- **1.20.2+:** UUID is unconditionally included (no flag byte).
|
||
(`BungeeCord LoginRequest.java:35–38`: `if (protocolVersion >= MINECRAFT_1_20_2)` reads UUID directly.)
|
||
|
||
---
|
||
|
||
### 2.2 Encryption Request (0x01 Clientbound)
|
||
|
||
Sent by the server in online mode to initiate the key exchange.
|
||
|
||
| Field | Type | Version range | Notes |
|
||
|---|---|---|---|
|
||
| Server ID | String(20) | all | Historically a 10-char hex; **empty string `""` in vanilla 1.7+** |
|
||
| Public Key | Prefixed byte array | 1.8+ | DER-encoded X.509 SubjectPublicKeyInfo of server's RSA-1024 public key |
|
||
| Public Key | 2-byte-length byte array | 1.7 (pre-Netty) | Same key, length prefixed with `i16` |
|
||
| Verify Token | Prefixed byte array | all | 4 random bytes generated per-connection |
|
||
| Should Authenticate | Boolean | 1.20.5+ | Whether Mojang session auth should be performed |
|
||
|
||
**Source:** `Velocity/proxy/.../EncryptionRequestPacket.java:62–74` (1.7 vs 1.8+ branching on
|
||
`readByteArray` vs `readByteArray17`; `shouldAuthenticate` field read at line 67–69);
|
||
`BungeeCord/protocol/.../EncryptionRequest.java:26–30`;
|
||
`minecraft-data/data/pc/1.7/protocol.json` (countType `i16`);
|
||
`minecraft-data/data/pc/1.8/protocol.json` (countType `varint`).
|
||
|
||
Notes:
|
||
|
||
- The Server ID string was meaningful in old Alpha/Beta versions but **has been an empty
|
||
string `""` in all vanilla servers since 1.7** (wiki.minecraft.net/w/Java_Edition_protocol/Encryption).
|
||
Some implementations (node-minecraft-protocol `server/login.js:89`) generate a short random
|
||
hex string instead.
|
||
- `shouldAuthenticate` (1.20.5+) lets the server tell the client not to call the Mojang
|
||
session endpoint; used for offline-mode negotiation without breaking the encryption handshake.
|
||
(`Velocity/proxy/.../EncryptionRequestPacket.java:67–69`)
|
||
|
||
---
|
||
|
||
### 2.3 Encryption Response (0x01 Serverbound)
|
||
|
||
Client reply containing the RSA-encrypted shared secret and token.
|
||
|
||
**Standard form (all versions except 1.19–1.19.2 with profile keys):**
|
||
|
||
| Field | Type | Notes |
|
||
|---|---|---|
|
||
| Shared Secret | Prefixed byte array | 128 bytes (RSA-PKCS1 v1.5 ciphertext of 16-byte secret) |
|
||
| Verify Token | Prefixed byte array | 128 bytes (RSA-PKCS1 v1.5 ciphertext of the 4-byte token) |
|
||
|
||
**1.19–1.19.2 form with profile keys (chat signing enabled):**
|
||
|
||
| Field | Type | Notes |
|
||
|---|---|---|
|
||
| Shared Secret | Prefixed byte array | 128 bytes as above |
|
||
| Has Verify Token | Boolean | `true` = standard token; `false` = signed salt |
|
||
| — if true: Verify Token | Prefixed byte array | 128 bytes as above |
|
||
| — if false: Salt | i64 | Random 64-bit salt |
|
||
| — if false: Message Signature | Prefixed byte array | SHA256withRSA sig over `verifyToken ‖ salt` |
|
||
|
||
**Source:** `Velocity/proxy/.../EncryptionResponsePacket.java:66–101` (version branching);
|
||
`BungeeCord/protocol/.../EncryptionResponse.java:24–55`;
|
||
`node-minecraft-protocol/src/client/encrypt.js:52–73` (1.19 `hasVerifyToken`/`salt`/`messageSignature` branch);
|
||
`node-minecraft-protocol/src/server/login.js:109–141` (server-side verification).
|
||
|
||
Notes:
|
||
|
||
- `Has Verify Token = false` only occurs in 1.19–1.19.2 when the client has a profile key
|
||
(chat-signing enabled). The server verifies the salt+signature against the player's
|
||
profile public key from Login Start instead of decrypting the token.
|
||
(`server/login.js:120–126`: `crypto.verify('sha256WithRSAEncryption', …)`)
|
||
- In 1.19.3+, profile keys were removed from Login Start; `Has Verify Token` field is gone
|
||
and the packet reverts to the simple two-field form.
|
||
(`Velocity/proxy/.../EncryptionResponsePacket.java:70–74`: version range `>= 1.19 && < 1.19.3`)
|
||
|
||
---
|
||
|
||
### 2.4 Set Compression (0x03 Clientbound)
|
||
|
||
Optional packet. If sent, all subsequent packets in Login (and Play) use the compressed
|
||
framing described in §4.
|
||
|
||
| Field | Type | Notes |
|
||
|---|---|---|
|
||
| Threshold | VarInt | Minimum uncompressed size to trigger compression. Negative = disable. |
|
||
|
||
**Source:** `BungeeCord/protocol/.../SetCompression.java:19–22`;
|
||
`node-minecraft-protocol/src/server/login.js:179–181` (`client.write('compress', { threshold: 256 })`).
|
||
|
||
Notes:
|
||
|
||
- Added in **1.8** (protocol version 47, snapshot 14w28a).
|
||
(`node-minecraft-protocol/src/server/login.js:179`: `if (client.protocolVersion >= 27)`)
|
||
- Vanilla default threshold is **256 bytes** uncompressed payload.
|
||
- A threshold of `-1` disables compression; packets keep the uncompressed framing.
|
||
- **Must be sent before Login Success** if used. The server enables its own compressor and
|
||
decompressor as soon as it sends this packet; the client enables them on receipt.
|
||
|
||
---
|
||
|
||
### 2.5 Login Success (0x02 Clientbound)
|
||
|
||
Signals the end of authentication. Carries the player's resolved UUID and username.
|
||
|
||
| Field | Type | Version range | Notes |
|
||
|---|---|---|---|
|
||
| UUID | String (hyphenated) | ≤1.15 | `"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"` |
|
||
| UUID | UUID (128-bit wire) | 1.16+ | Raw 16-byte big-endian UUID |
|
||
| Username | String(16) | all | Canonical (case-corrected) name from Mojang |
|
||
| Properties | Array of Property | 1.19+ | Texture/cape data; each has `name`, `value`, optional `signature` |
|
||
| Strict Error Handling | Boolean | 1.20.5–1.21.1 | Vanilla sends `true` |
|
||
|
||
**Source:** `BungeeCord/protocol/.../LoginSuccess.java:28–43` (UUID wire-type branch at 1.16;
|
||
properties at 1.19; `strictErrorHandling` at 1.20.5–1.21.1);
|
||
`node-minecraft-protocol/src/server/login.js:184–188`.
|
||
|
||
Notes:
|
||
|
||
- UUID comes from the Mojang session server response (`id` field, dashes stripped) in online
|
||
mode. In offline mode it is `UUID.nameUUIDFromBytes("OfflinePlayer:" + username)`.
|
||
- **Login Success is sent encrypted** (all prior negotiation including Encryption Response
|
||
triggers immediate cipher switch; see §3.5).
|
||
|
||
---
|
||
|
||
### 2.6 Login Acknowledged (0x03 Serverbound)
|
||
|
||
No fields. The client sends this to confirm it has processed Login Success and is ready to
|
||
enter the **Configuration** state. Added in **1.20.2**.
|
||
|
||
**Source:** `minecraft-data/data/pc/1.20.2/protocol.json` (toServer `0x03: login_acknowledged`);
|
||
`node-minecraft-protocol/src/server/login.js:189–191`
|
||
(`if (client.supportFeature('hasConfigurationState')) client.once('login_acknowledged', …)`);
|
||
`minecraft-data/data/pc/common/features.json` (`hasConfigurationState` versions: `["1.20.2", "latest"]`).
|
||
|
||
---
|
||
|
||
## 3. Encryption Handshake
|
||
|
||
### 3.1 Sequence Diagram
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant C as Client
|
||
participant S as Server
|
||
participant M as sessionserver.mojang.com
|
||
|
||
C->>S: Handshake (next_state=2)
|
||
C->>S: Login Start (username [+ profile key 1.19–1.19.2])
|
||
Note over S: online-mode? generate RSA-1024 keypair (once at startup)
|
||
S->>C: Encryption Request (serverId="", DER pubkey, 4-byte verifyToken)
|
||
Note over C: generate 16-byte random sharedSecret
|
||
Note over C: RSA-PKCS1-v1.5 encrypt sharedSecret → enc_secret (128 B)
|
||
Note over C: RSA-PKCS1-v1.5 encrypt verifyToken → enc_token (128 B)
|
||
Note over C: compute serverIdHash = MinecraftSHA1(serverId‖sharedSecret‖pubKey)
|
||
C->>M: POST /session/minecraft/join {accessToken, selectedProfile, serverId=serverIdHash}
|
||
M-->>C: 204 No Content
|
||
C->>S: Encryption Response (enc_secret, enc_token)
|
||
Note over C: enable AES/CFB8 (key=sharedSecret, IV=sharedSecret) — all outgoing encrypted now
|
||
Note over S: RSA-decrypt enc_secret → sharedSecret
|
||
Note over S: RSA-decrypt enc_token → verify == original verifyToken?
|
||
Note over S: compute serverIdHash = MinecraftSHA1(serverId‖sharedSecret‖pubKey)
|
||
S->>M: GET /session/minecraft/hasJoined?username=&serverId=serverIdHash
|
||
M-->>S: 200 {id, name, properties} or 204 (not found)
|
||
Note over S: enable AES/CFB8 (key=sharedSecret, IV=sharedSecret) — all outgoing encrypted now
|
||
S->>C: [Set Compression] (optional, 1.8+)
|
||
S->>C: Login Success (uuid, username, properties) — encrypted
|
||
C->>S: Login Acknowledged (1.20.2+) — encrypted
|
||
```
|
||
|
||
---
|
||
|
||
### 3.2 RSA Key Exchange
|
||
|
||
**Key size:** 1024-bit RSA, generated once at server startup.
|
||
|
||
- BungeeCord: `EncryptionUtil.java:51` — `generator.initialize(1024)`
|
||
- Velocity: `VelocityServer.java:255` — `EncryptionUtils.createRsaKeyPair(1024)`
|
||
|
||
**Public key encoding:** The bytes sent in Encryption Request are the **DER-encoded
|
||
X.509 SubjectPublicKeyInfo** (i.e. `java.security.PublicKey.getEncoded()` using the
|
||
`X509EncodedKeySpec`). This wraps the raw RSA key in an ASN.1 structure identifying
|
||
the algorithm (`rsaEncryption OID 1.2.840.113549.1.1.1`).
|
||
|
||
The client reconstructs the public key from this DER blob:
|
||
(`node-minecraft-protocol/src/client/encrypt.js:48–50`)
|
||
|
||
```js
|
||
// encrypt.js:48–50
|
||
const pubKey = mcPubKeyToPem(packet.publicKey) // DER → PEM wrapper
|
||
const encryptedSharedSecretBuffer =
|
||
crypto.publicEncrypt({ key: pubKey, padding: crypto.constants.RSA_PKCS1_PADDING }, sharedSecret)
|
||
const encryptedVerifyTokenBuffer =
|
||
crypto.publicEncrypt({ key: pubKey, padding: crypto.constants.RSA_PKCS1_PADDING }, packet.verifyToken)
|
||
```
|
||
|
||
**Padding:** PKCS#1 v1.5 (`RSA_PKCS1_PADDING` / `RSA/ECB/PKCS1Padding`).
|
||
|
||
- BungeeCord: `EncryptionUtil.java:144–148` — `Cipher.getInstance("RSA/ECB/PKCS1Padding")`
|
||
- Velocity: `EncryptionUtils.java:190–193` — `Cipher.getInstance("RSA")` (JCA default = PKCS#1 v1.5)
|
||
|
||
**Ciphertext size:** With a 1024-bit key and PKCS#1 v1.5 padding, any input ≤ 117 bytes
|
||
produces a **128-byte ciphertext**. Both the encrypted shared secret and encrypted verify
|
||
token are therefore 128 bytes on the wire.
|
||
|
||
---
|
||
|
||
### 3.3 Server-ID Hash Algorithm
|
||
|
||
The **serverIdHash** (called `serverId` in the Mojang API) is a non-standard SHA-1 digest
|
||
formatted as a signed two's-complement hex integer, possibly with a leading `-`.
|
||
|
||
**Inputs (in order):**
|
||
|
||
1. `serverId` string bytes, encoded as ISO-8859-1 (Latin-1)
|
||
— in vanilla this is always an empty string, so no bytes are contributed.
|
||
2. `sharedSecret` — the raw 16-byte AES key.
|
||
3. Server's RSA public key — DER-encoded SubjectPublicKeyInfo bytes.
|
||
|
||
**Algorithm:**
|
||
|
||
```python
|
||
import hashlib, textwrap
|
||
|
||
def minecraft_sha1_hex(server_id: str, shared_secret: bytes, server_pub_der: bytes) -> str:
|
||
h = hashlib.sha1()
|
||
h.update(server_id.encode('iso-8859-1'))
|
||
h.update(shared_secret)
|
||
h.update(server_pub_der)
|
||
digest = h.digest() # 20 raw bytes
|
||
# Interpret as a signed big-endian integer (two's complement):
|
||
n = int.from_bytes(digest, byteorder='big', signed=True)
|
||
return format(n, 'x') # hex, with '-' prefix if negative
|
||
```
|
||
|
||
Java equivalent (BungeeCord `InitialHandler.java:517–525`):
|
||
|
||
```java
|
||
MessageDigest sha = MessageDigest.getInstance("SHA-1");
|
||
sha.update(request.getServerId().getBytes("ISO_8859_1"));
|
||
sha.update(sharedKey.getEncoded());
|
||
sha.update(EncryptionUtil.keys.getPublic().getEncoded());
|
||
String hash = new BigInteger(sha.digest()).toString(16);
|
||
// BigInteger(byte[]) treats the input as two's-complement big-endian → negative if MSB set
|
||
```
|
||
|
||
Velocity (`EncryptionUtils.java:203–212`) omits the `serverId` update because the
|
||
field is always `""` in Velocity:
|
||
|
||
```java
|
||
digest.update(sharedSecret); // EncryptionUtils.java:207
|
||
digest.update(key.getEncoded()); // EncryptionUtils.java:208
|
||
return twosComplementHexdigest(digest.digest());
|
||
// twosComplementHexdigest: new BigInteger(digest).toString(16) (line 179)
|
||
```
|
||
|
||
**Wiki examples** (minecraft.wiki/w/Java_Edition_protocol/Encryption):
|
||
|
||
| Input string | serverIdHash |
|
||
|---|---|
|
||
| `Notch` | `4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48` |
|
||
| `jeb_` | `-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1` |
|
||
| `simon` | `88e16a1019277b15d58faf0541e11910eb756f6` |
|
||
|
||
Note: these examples use the string as the entire input (simulating a server where
|
||
`serverId = "Notch"` etc.), not a realistic scenario.
|
||
|
||
**Why two's complement?** The SHA-1 output is 20 bytes. Mojang's original Minecraft code
|
||
treated this raw byte array as a signed big-endian integer via Java's `BigInteger(byte[])`.
|
||
If the high bit of the first byte is set (digest[0] ≥ 0x80), the integer is negative, and
|
||
the hex string gets a leading `-`. This is non-standard but has been the canonical format
|
||
since Minecraft Beta 1.2.
|
||
|
||
---
|
||
|
||
### 3.4 AES/CFB8 Stream Cipher
|
||
|
||
After both sides process Encryption Response, every subsequent byte on the TCP connection
|
||
is encrypted. There are **no unencrypted bytes** after this point — not even the packet
|
||
length VarInt.
|
||
|
||
**Cipher parameters:**
|
||
|
||
| Parameter | Value |
|
||
|---|---|
|
||
| Algorithm | AES |
|
||
| Mode | CFB8 (Cipher Feedback, 8-bit segment size) |
|
||
| Key | 16-byte shared secret |
|
||
| IV | 16-byte shared secret (same as key) |
|
||
| Padding | None |
|
||
|
||
BungeeCord (`JavaCipher.java`):
|
||
```java
|
||
this.cipher = Cipher.getInstance("AES/CFB8/NoPadding");
|
||
cipher.init(mode, key, new IvParameterSpec(key.getEncoded()));
|
||
// key.getEncoded() == the 16-byte shared secret, used as IV
|
||
```
|
||
|
||
Velocity (`JavaVelocityCipher.java:50–61`):
|
||
```java
|
||
this.cipher = Cipher.getInstance("AES/CFB8/NoPadding");
|
||
this.cipher.init(
|
||
encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE,
|
||
key,
|
||
new IvParameterSpec(key.getEncoded()) // IV = key = shared secret
|
||
);
|
||
```
|
||
|
||
node-minecraft-protocol (`transforms/encryption.js:6–17`):
|
||
```js
|
||
// encryption.js:6–9
|
||
function createCipher(secret) {
|
||
if (crypto.getCiphers().includes('aes-128-cfb8')) {
|
||
return crypto.createCipheriv('aes-128-cfb8', secret, secret) // key=secret, IV=secret
|
||
}
|
||
return new Cipher(secret) // fallback uses aes-js CFB with segment size 1 byte
|
||
}
|
||
```
|
||
|
||
**Important:** Reusing the key as the IV is a known weakness (noted in Velocity source,
|
||
`JavaVelocityCipher.java:52–58`: *"reusing the key as the IV defeats the entire point"*).
|
||
The cipher is initialized this way by Mojang design; it cannot be changed without a
|
||
protocol breaking change.
|
||
|
||
**Continuous operation:** The AES/CFB8 state is maintained across packets. The cipher is
|
||
never reset or re-initialized between packets; the feedback register carries over.
|
||
|
||
---
|
||
|
||
### 3.5 Timing: When Encryption Activates
|
||
|
||
- **Client:** enables AES/CFB8 immediately after *sending* Encryption Response
|
||
(`node-minecraft-protocol/src/client/encrypt.js:73`: `client.setEncryption(sharedSecret)`)
|
||
- **Server:** enables AES/CFB8 after *receiving* Encryption Response and verifying it
|
||
(`node-minecraft-protocol/src/server/login.js:150`: `client.setEncryption(sharedSecret)`;
|
||
Velocity `InitialLoginSessionHandler.java:234`: `mcConnection.enableEncryption(decryptedSharedSecret)`)
|
||
|
||
The server enables encryption before calling the Mojang session server, so if `hasJoined`
|
||
fails, the server sends a disconnect packet that is already encrypted.
|
||
|
||
---
|
||
|
||
## 4. Session Authentication (Online Mode)
|
||
|
||
### 4.1 Client → Mojang: `join`
|
||
|
||
Before sending Encryption Response, the client posts to the Mojang session server:
|
||
|
||
```
|
||
POST https://sessionserver.mojang.com/session/minecraft/join
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"accessToken": "<launcher access token>",
|
||
"selectedProfile": "<UUID of player, dashes removed>",
|
||
"serverId": "<serverIdHash>"
|
||
}
|
||
```
|
||
|
||
Expected response: **204 No Content** on success.
|
||
|
||
(`node-minecraft-protocol/src/client/encrypt.js:41–43`: `yggdrasilServer.join(accessToken, selectedProfile.id, packet.serverId, sharedSecret, packet.publicKey, cb)` — the `join` call in the yggdrasil library computes the hash internally from `serverId + sharedSecret + publicKey`.)
|
||
|
||
### 4.2 Server → Mojang: `hasJoined`
|
||
|
||
After enabling encryption and verifying the token, the server queries:
|
||
|
||
```
|
||
GET https://sessionserver.mojang.com/session/minecraft/hasJoined
|
||
?username=<username>
|
||
&serverId=<serverIdHash>
|
||
[&ip=<client_ip>] # optional; prevents proxy-hopping
|
||
```
|
||
|
||
- **200 OK** → body is a GameProfile JSON `{id, name, properties[]}`. Login proceeds.
|
||
- **204 No Content** → client did not call `join`; kick with `"offline_mode_player"` or similar.
|
||
|
||
(`Velocity/proxy/.../InitialLoginSessionHandler.java:68–71`: URL template;
|
||
BungeeCord `InitialHandler.java:527–528`: URL construction with `URLEncoder.encode`.)
|
||
|
||
### 4.3 Mojang → Microsoft Account Migration
|
||
|
||
The original `sessionserver.mojang.com` endpoints still work after the Mojang → Microsoft
|
||
account migration. All Minecraft clients (Bedrock and Java launcher) now use Microsoft OAuth
|
||
tokens internally, but the session server interface is unchanged at the API level.
|
||
<!-- VERIFY: confirm current sessionserver.mojang.com still accepts calls in 2025/2026 -->
|
||
|
||
### 4.4 Offline Mode
|
||
|
||
In offline mode, the server skips:
|
||
- Sending Encryption Request (no encryption handshake at all)
|
||
- Calling `hasJoined`
|
||
|
||
UUID is derived deterministically: `UUID.nameUUIDFromBytes("OfflinePlayer:" + username)` (MD5-based
|
||
UUID v3 equivalent). (`BungeeCord/proxy/.../connection/InitialHandler.java:560`)
|
||
|
||
---
|
||
|
||
## 5. 1.19 Profile Public Keys (Chat Signing)
|
||
|
||
Minecraft 1.19 introduced per-player RSA key pairs signed by Mojang, allowing chat message
|
||
signing. The **Login Start** packet gained a `Signature` field to deliver the player's public
|
||
key to the server.
|
||
|
||
This only affects the **login phase** through two mechanisms:
|
||
|
||
1. **Login Start (1.19–1.19.2):** Client sends its profile public key (DER + Mojang signature).
|
||
2. **Encryption Response (1.19–1.19.2):** Client may replace the encrypted verify token with
|
||
a salt + signature over `verifyToken ‖ salt`, proving possession of the private key
|
||
corresponding to the profile key.
|
||
|
||
### Profile Key Container (in Login Start, 1.19–1.19.2)
|
||
|
||
| Sub-field | Type | Notes |
|
||
|---|---|---|
|
||
| Timestamp | i64 | Key expiry in milliseconds since epoch |
|
||
| Public Key | Prefixed byte array | DER SubjectPublicKeyInfo |
|
||
| Signature | Prefixed byte array | Mojang signature over the key material |
|
||
|
||
**Signature verification (server side):**
|
||
|
||
- **1.19:** verify against Mojang's public key over `UTF8(expiryTimestamp + PEM(playerPubKey))`.
|
||
(`node-minecraft-protocol/src/server/login.js:72`: `Buffer.from(timestamp + mcPubKeyToPem(publicKey), 'utf8')`)
|
||
- **1.19.2 (profileKeySignatureV2):** verify against Mojang's public key over
|
||
`UUID(playerUUID) ‖ i64(timestamp) ‖ DER(playerPubKey)`.
|
||
(`node-minecraft-protocol/src/server/login.js:71`:
|
||
`concat('UUID', playerUUID, 'i64', timestamp, 'buffer', publicKey.export({type:'spki', format:'der'}))`)
|
||
- Both use **RSA-SHA1** (`crypto.verify('RSA-SHA1', …)`).
|
||
|
||
(`minecraft-data/data/pc/common/features.json`: `signatureEncryption` active on `["1.19", "1.19.2"]`;
|
||
`profileKeySignatureV2` active on `["1.19.2", "latest"]`.)
|
||
|
||
### Removal in 1.19.3
|
||
|
||
Profile keys were removed from Login Start in 1.19.3. The `Signature` field is absent, the
|
||
`Has Verify Token` field in Encryption Response is also gone, and the standard two-field
|
||
Encryption Response format is used again.
|
||
|
||
(`Velocity/proxy/.../EncryptionResponsePacket.java:70–74`:
|
||
`if (version.noLessThan(MINECRAFT_1_19) && version.lessThan(MINECRAFT_1_19_3)) { salt check }`)
|
||
|
||
---
|
||
|
||
## 6. Compression
|
||
|
||
### 6.1 Negotiation
|
||
|
||
The server may send **Set Compression** (0x03 clientbound) at any point during the Login
|
||
state, before Login Success. The threshold value (VarInt) controls when zlib is applied:
|
||
|
||
- **threshold < 0** — compression is disabled (revert to uncompressed framing).
|
||
- **threshold = 0** — compress all packets.
|
||
- **threshold > 0** (common: 256) — compress only packets whose uncompressed data length
|
||
exceeds the threshold.
|
||
|
||
Compression was added in **1.8** (snapshot 14w28a, protocol version 47).
|
||
(`node-minecraft-protocol/src/server/login.js:179`: `if (client.protocolVersion >= 27)`)
|
||
|
||
### 6.2 Packet Format Before Set Compression
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ Packet Length (VarInt) — byte count of ID + Data │
|
||
│ Packet ID (VarInt) │
|
||
│ Data (bytes) │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 6.3 Packet Format After Set Compression
|
||
|
||
Two VarInts precede the payload:
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ Packet Length (VarInt) — byte count of (Data Length │
|
||
│ VarInt + possibly compressed │
|
||
│ ID+Data) │
|
||
│ Data Length (VarInt) — 0 = not compressed; │
|
||
│ >0 = uncompressed byte count │
|
||
│ Packet ID + Data (bytes) — zlib-compressed if DL>0, │
|
||
│ raw otherwise │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**Uncompressed case** (data length < threshold):
|
||
- Data Length VarInt = **0**
|
||
- ID + Data follow uncompressed
|
||
|
||
**Compressed case** (data length ≥ threshold):
|
||
- Data Length VarInt = uncompressed byte count of ID+Data
|
||
- ID + Data bytes are zlib (`DEFLATE`) compressed
|
||
|
||
BungeeCord implementation (`netty/LengthPrependerAndCompressor.java:47–93`):
|
||
|
||
```java
|
||
// uncompressed path (line 54–63):
|
||
if (oldBodyLen < threshold) {
|
||
DefinedPacket.writeVarInt(oldBodyLen + 1, lenBuf); // Packet Length
|
||
lenBuf.writeByte(0); // Data Length = 0 → uncompressed
|
||
// ... append raw body ...
|
||
}
|
||
// compressed path (line 78–93):
|
||
else {
|
||
DefinedPacket.writeVarInt(oldBodyLen, buf); // Data Length = original size
|
||
zlib.process(msg, buf); // compress
|
||
// Packet Length written as VarInt in front
|
||
}
|
||
```
|
||
|
||
### 6.4 Interaction with Encryption
|
||
|
||
Compression and encryption are independent layers. When both are active:
|
||
|
||
1. **Write path:** `Compress → Encrypt → TCP`
|
||
2. **Read path:** `TCP → Decrypt → Decompress`
|
||
|
||
The encrypted bytestream carries compressed packet data; the AES/CFB8 cipher sees
|
||
compressed bytes and does not know about packet boundaries.
|
||
|
||
---
|
||
|
||
## 7. Version Summary
|
||
|
||
| Feature | Introduced | Removed / Changed |
|
||
|---|---|---|
|
||
| Login Start: basic (name only) | 1.7 | — |
|
||
| Encryption request/response | 1.2.5 (Beta) | — |
|
||
| Byte arrays length-prefixed with VarInt | 1.8 | — |
|
||
| Set Compression packet | 1.8 (14w28a) | — |
|
||
| Login Start: profile public key | 1.19 | Removed 1.19.3 |
|
||
| Encryption Response: Has Verify Token + salt | 1.19 | Removed 1.19.3 |
|
||
| Login Start: optional UUID | 1.19.1 | Mandatory 1.20.2+ |
|
||
| Profile key signature V2 | 1.19.2 | — |
|
||
| Login Acknowledged packet | 1.20.2 | — |
|
||
| Configuration state after login | 1.20.2 | — |
|
||
| Encryption Request: Should Authenticate | 1.20.5 | — |
|
||
| Login Success: Strict Error Handling | 1.20.5 | Removed 1.21.2 |
|
||
| Login Success: UUID as raw 128-bit wire type | 1.16 | — |
|
||
| Login Success: Properties array | 1.19 | — |
|
||
|
||
---
|
||
|
||
## 8. Sources
|
||
|
||
| Reference | Path |
|
||
|---|---|
|
||
| node-minecraft-protocol encrypt.js | `node-minecraft-protocol/src/client/encrypt.js` |
|
||
| node-minecraft-protocol encryption.js | `node-minecraft-protocol/src/transforms/encryption.js` |
|
||
| node-minecraft-protocol server/login.js | `node-minecraft-protocol/src/server/login.js` |
|
||
| Velocity EncryptionRequestPacket | `Velocity/proxy/.../packet/EncryptionRequestPacket.java` |
|
||
| Velocity EncryptionResponsePacket | `Velocity/proxy/.../packet/EncryptionResponsePacket.java` |
|
||
| Velocity EncryptionUtils | `Velocity/proxy/.../crypto/EncryptionUtils.java` |
|
||
| Velocity JavaVelocityCipher | `Velocity/native/.../encryption/JavaVelocityCipher.java` |
|
||
| Velocity InitialLoginSessionHandler | `Velocity/proxy/.../client/InitialLoginSessionHandler.java` |
|
||
| BungeeCord EncryptionUtil | `BungeeCord/proxy/.../EncryptionUtil.java` |
|
||
| BungeeCord InitialHandler | `BungeeCord/proxy/.../connection/InitialHandler.java` |
|
||
| BungeeCord JavaCipher | `BungeeCord/native/.../cipher/JavaCipher.java` |
|
||
| BungeeCord LengthPrependerAndCompressor | `BungeeCord/proxy/.../netty/LengthPrependerAndCompressor.java` |
|
||
| BungeeCord LoginRequest | `BungeeCord/protocol/.../packet/LoginRequest.java` |
|
||
| BungeeCord LoginSuccess | `BungeeCord/protocol/.../packet/LoginSuccess.java` |
|
||
| BungeeCord EncryptionRequest | `BungeeCord/protocol/.../packet/EncryptionRequest.java` |
|
||
| BungeeCord EncryptionResponse | `BungeeCord/protocol/.../packet/EncryptionResponse.java` |
|
||
| BungeeCord SetCompression | `BungeeCord/protocol/.../packet/SetCompression.java` |
|
||
| minecraft-data protocol.json (all versions) | `minecraft-data/data/pc/<version>/protocol.json` |
|
||
| minecraft-data features.json | `minecraft-data/data/pc/common/features.json` |
|
||
| minecraft.wiki Protocol Encryption | `https://minecraft.wiki/w/Java_Edition_protocol/Encryption` |
|
||
|
||
<!-- VERIFY: sessionserver.mojang.com/session/minecraft/join and hasJoined still live as of 2026 — Microsoft migration kept these endpoints but confirm they remain unredirected. -->
|
||
<!-- VERIFY: RSA-1024 key size — BungeeCord and Velocity both confirmed at 1024 bits; check if vanilla Minecraft server (NMS) uses a different size. -->
|
||
<!-- VERIFY: Login Start UUID presence in 1.19.1 exact behaviour — BungeeCord LoginRequest.java:33–38 shows optional boolean + UUID; verify the flag is `buf.readBoolean()` not some other encoding in vanilla packets. -->
|