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>
279 lines
28 KiB
Markdown
279 lines
28 KiB
Markdown
# Java Edition 1.19.x — Protocol Deep-Dive
|
||
|
||
| Release | Protocol # | Release date | minecraft-data dir | ViaVersion package (into this version) |
|
||
|---|---|---|---|---|
|
||
| 1.19 | **759** | 2022-06-07 | `data/pc/1.19` | `v1_18_2to1_19` |
|
||
| 1.19.1 | **760** | 2022-07-27 | *(no separate dir; = 1.19.2)* | `v1_19to1_19_1` |
|
||
| 1.19.2 | **760** | 2022-08-05 | `data/pc/1.19.2` | `v1_19to1_19_1` |
|
||
| 1.19.3 | **761** | 2022-12-07 | `data/pc/1.19.3` | `v1_19_1to1_19_3` |
|
||
| 1.19.4 | **762** | 2023-03-14 | `data/pc/1.19.4` | `v1_19_3to1_19_4` |
|
||
|
||
Protocol numbers cross-checked against minecraft-data `protocolVersions.json` (`1.19`→759 dataVersion 3105, `1.19.1`→760 dv 3117, `1.19.2`→760 dv 3120, `1.19.3`→761 dv 3218, `1.19.4`→762 dv 3337) and each dir's `version.json`. **1.19.1 and 1.19.2 share protocol 760** — the wire format is identical; 1.19.2 was a same-week bug-fix that did not bump the protocol. Release dates from minecraft.wiki release articles (fetched 2026-06-19): [1.19](https://minecraft.wiki/w/Java_Edition_1.19), [1.19.1](https://minecraft.wiki/w/Java_Edition_1.19.1), [1.19.3](https://minecraft.wiki/w/Java_Edition_1.19.3), [1.19.4](https://minecraft.wiki/w/Java_Edition_1.19.4). ViaVersion source at `/tmp/mcproto-refs/ViaVersion/`.
|
||
|
||
---
|
||
|
||
## Headline — The Wild Update + Secure Chat
|
||
|
||
1.19 ("**The Wild Update**", 2022-06-07) shipped the Deep Dark biome + Warden, Ancient Cities, the Mangrove Swamp, and the Allay/Frog mobs ([minecraft.wiki/w/Java_Edition_1.19](https://minecraft.wiki/w/Java_Edition_1.19), fetched 2026-06-19). But on the wire, **the dominant story of the whole 1.19 line is chat signing / Secure Chat** — not gameplay. The wiki states plainly that as of 1.19, "Chat messages between players, as well as chat from the `/say`, `/msg`, `/teammsg`, and `/me` commands, are now cryptographically signed", and players receive a Mojang-provided key pair on startup ([1.19 article](https://minecraft.wiki/w/Java_Edition_1.19), fetched 2026-06-19).
|
||
|
||
The reason this release line carries **four protocol numbers** (759 → 760 → 761 → 762) is that the signing design **churned three times**:
|
||
|
||
- **759 (1.19):** v1 — client signs each chat message with a per-message signature, key delivered in Login Start; signed/unsigned content split; chat preview.
|
||
- **760 (1.19.1/1.19.2):** v2 — the **player-reporting** rework. A signature *chain* is introduced (each message signs over the preceding signature + a set of last-seen message acknowledgements), so message order is cryptographically verifiable for reports.
|
||
- **761 (1.19.3):** **partial revert + re-architecture.** Chat preview removed entirely; per-message "header" packets gone; the chain is rebuilt around a per-session ID + monotonic message index (`MessageLink`); system messages are no longer in the signed path; signing becomes more conditional on `enforce-secure-profile`. The disguised-chat packet is introduced for server-authored messages that need chat-type decoration without a signature.
|
||
- **762 (1.19.4):** refinements only — no change to the signing payload; clients reset their secure-chat session state on the Login (Play) packet.
|
||
|
||
This is the most complicated signing story in the protocol's history; the per-patch sub-sections below document each model precisely from ViaVersion source.
|
||
|
||
---
|
||
|
||
## Login-phase: the session profile public key (759–760 only)
|
||
|
||
Secure Chat needs the client to prove ownership of a Mojang-signed key pair. In **1.19–1.19.2** that key is delivered **during the Login state**, appended to **Login Start (Serverbound `HELLO`)**, and it is what later chat-message signatures are verified against. This is the protocol's first use of a player-supplied public key in login. Full field layout and the Encryption-Response salt/signature variant are in [../05-login-encryption.md](../05-login-encryption.md) §2 and §5; summarised here for the signing context:
|
||
|
||
- **Login Start gains an optional `Signature` (profile key) container** (1.19–1.19.2): `Timestamp` (i64 key-expiry), `Public Key` (DER `SubjectPublicKeyInfo`), and a Mojang `Signature` over them. ViaVersion's type is `Types.OPTIONAL_PROFILE_KEY`. In 1.19.1+ Login Start also carries an optional `Player UUID`.
|
||
- `v1_18_2to1_19/Protocol1_18_2To1_19.java:278-284` — serverbound `HELLO` maps the name and **reads-and-discards** `OPTIONAL_PROFILE_KEY` (ViaVersion's own clients downstream don't sign).
|
||
- `v1_19to1_19_1/Protocol1_19To1_19_1.java:231-248` — serverbound `HELLO` reads the incoming profile key but **substitutes ViaVersion's own `ChatSession1_19_0.getProfileKey()`** when a chat session is configured, then reads the optional UUID (`Types.OPTIONAL_UUID`).
|
||
- **Encryption Response salt/signature variant** (1.19–1.19.2): when the client holds a profile key it replies to Encryption Request with `salt + signature` instead of the encrypted verify-token nonce; the server verifies it against the profile key. ViaVersion swaps this back to a plain nonce when its downstream client has no key:
|
||
- `v1_18_2to1_19/Protocol1_18_2To1_19.java:286-307` (the salt branch, `// 🧂`).
|
||
- `v1_19to1_19_1/Protocol1_19To1_19_1.java:264-283`.
|
||
- **761 (1.19.3) moves the key out of Login Start.** The profile key is no longer sent at login; instead the *active* key is sent in-Play via the new `CHAT_SESSION_UPDATE` packet (see §761). At login 1.19.3 only carries name + optional UUID:
|
||
- `v1_19_1to1_19_3/Protocol1_19_1To1_19_3.java:284-294` — serverbound `HELLO` writes `OPTIONAL_PROFILE_KEY` from the chat session (or null) but the *client* no longer supplies one; `:295-322` handles the Encryption-Response salt/signature using the in-Play `ChatSession1_19_1` key.
|
||
|
||
> Cross-reference: [../05-login-encryption.md](../05-login-encryption.md) lines 35–53 (Login Start `Signature` container, present 1.19–1.19.2, removed 1.19.3) and 100–123 (Encryption Response profile-key form).
|
||
|
||
---
|
||
|
||
## 759 — 1.19 (2022-06-07): chat signing v1
|
||
|
||
**Model.** Each player chat message is individually signed by the client. There is **no chain** yet: the signature covers a fixed-width metadata block plus the (canonicalised) decorated message JSON. The server delivers player chat to recipients in a dedicated, signed `PLAYER_CHAT` packet distinct from `SYSTEM_CHAT`.
|
||
|
||
**Signature payload (1.19, `ChatSession1_19_0`).** `v1_18_2to1_19` is a *down*-translation (1.18.2-side ↔ 1.19-side), and ViaVersion's own clients don't sign, so the canonical signing payload lives in `ChatSession1_19_0.signChatMessage`:
|
||
|
||
```java
|
||
// api/.../signature/storage/ChatSession1_19_0.java:42-51
|
||
byte[] data = new byte[32]; // big-endian
|
||
buffer.putLong(metadata.salt()); // 8B salt
|
||
buffer.putLong(sender.MSB).putLong(sender.LSB); // 16B sender UUID
|
||
buffer.putLong(metadata.timestamp().getEpochSecond); // 8B timestamp (seconds)
|
||
signer.accept(data);
|
||
signer.accept(GsonUtil.sort(content.decorated()) // canonical-sorted JSON of
|
||
.toString().getBytes(UTF_8)); // the decorated component
|
||
```
|
||
|
||
So the v1 signed bytes are `salt(8) ‖ senderUUID(16) ‖ timestampSeconds(8) ‖ sortedDecoratedJSON`, signed `SHA256withRSA` with the player's private key.
|
||
|
||
**Serverbound chat packets (1.19, `ServerboundPackets1_19`):**
|
||
|
||
| Packet | ID | Fields (1.19) |
|
||
|---|---|---|
|
||
| `CHAT_COMMAND` | 0x03 | command String, timestamp i64, salt i64, VarInt array of `{argumentName, signature[]}`, `signedPreview` bool |
|
||
| `CHAT` | 0x04 | message String, timestamp i64, salt i64, signature byte[], `signedPreview` bool |
|
||
| `CHAT_PREVIEW` | 0x05 | preview request (queryId + text) |
|
||
|
||
Source: `v1_18_2to1_19/packet/ServerboundPackets1_19.java:27-29`. ViaVersion (translating *up* from a 1.18 server that has no signing) simply **reads and drops** all the signing fields and **cancels `CHAT_PREVIEW`**: `Protocol1_18_2To1_19.java:225-254`.
|
||
|
||
**Clientbound chat packets (1.19, `ClientboundPackets1_19`):**
|
||
|
||
| Packet | ID | Notes |
|
||
|---|---|---|
|
||
| `CHAT_PREVIEW` | 0x0C | server-side preview response |
|
||
| `PLAYER_CHAT` | 0x30 | signed player message: `signedContent` (Component), `unsignedContent` (opt Component), chat-type VarInt, sender UUID, sender name, team name, **timestamp i64, salt i64, signature byte[]** |
|
||
| `SERVER_DATA` | 0x3F | MOTD/icon/`previewsChat` |
|
||
| `SET_DISPLAY_CHAT_PREVIEW` | 0x4B | toggles preview UI |
|
||
| `SYSTEM_CHAT` | 0x5F | unsigned system/overlay message (Component + type VarInt) |
|
||
|
||
Source: `v1_18_2to1_19/packet/ClientboundPackets1_19.java:36,72,87,99,119`. Note the **signed/unsigned content split** in `PLAYER_CHAT` — a 1.19 novelty: the server can show an unsigned, server-decorated rendering while still carrying the original signed text for reporting.
|
||
|
||
ViaVersion from a 1.18 server: there is no signed player chat to translate *up*, so 1.18's `CHAT` (0x0F) is rewritten into `SYSTEM_CHAT` (0x5F) — *every* incoming message becomes a system message ("we don't want to analyze and remove player names"), which sidesteps signing entirely: `Protocol1_18_2To1_19.java:212-223`.
|
||
|
||
**ViaVersion package/commits.** Package `common/.../protocols/v1_18_2to1_19/` (`Protocol1_18_2To1_19.java` + `packet/{Clientbound,Serverbound}Packets1_19.java` + `provider/AckSequenceProvider.java` + `storage/{NonceStorage1_19,SequenceStorage,DimensionRegistryStorage}.java`). `git log --oneline -- common/.../v1_18_2to1_19` (top relevant): `ab3927dff` "Implement our own hash writing", `32e51b52a` "Cleanup LOGIN/STATUS packet handlers (#4113)", `c5756fe45` "Rename Position to BlockPosition", `501f65e21` "Packet and entity type renames", `e965e9713` "Package/class renames and moves". <!-- VERIFY: no surviving commit subject names the original 759 chat-signing implementation; it predates the renames captured in this shallow-unshallowed log -->
|
||
|
||
---
|
||
|
||
## 760 — 1.19.1 / 1.19.2 (2022-07-27 / 2022-08-05): player reporting + signature chain v2
|
||
|
||
**Headline.** 1.19.1 added the **player-reporting** system: players can report abusive chat, multiple messages per report, with categories (harassment, hate speech, etc.); reported players can be banned from online play/Realms after review. Messages that are unsigned or server-tampered are now flagged "Not Secure"/"Modified". `enforce-secure-profile` now **defaults to true** on dedicated servers. Crucially: "The order of chat messages are now cryptographically verified" — the chain. ([minecraft.wiki/w/Java_Edition_1.19.1](https://minecraft.wiki/w/Java_Edition_1.19.1), fetched 2026-06-19.) **1.19.2 is wire-identical (still protocol 760)** — a bug-fix that did not change the format.
|
||
|
||
**Model — the v2 signature chain (`ChatSession1_19_1`).** Each message now signs over the **preceding message's signature** (the "header") plus the body (which itself folds in **last-seen message acknowledgements**). This is what makes message *order* tamper-evident for reporting.
|
||
|
||
```java
|
||
// api/.../signature/storage/ChatSession1_19_1.java:43-52
|
||
MessageHeader header = new MessageHeader(this.precedingSignature, sender);
|
||
MessageBody body = new MessageBody(content, timestamp, salt, lastSeenMessages);
|
||
header.update(signer); // preceding-sig ‖ senderUUID
|
||
body.update(signer); // SHA-256 hash of body, see below
|
||
this.precedingSignature = signature; // chain advances
|
||
```
|
||
|
||
Header bytes (`chain/v1_19_1/MessageHeader.java:38-44`): `precedingSignature (if present) ‖ senderUUID`.
|
||
|
||
Body — the body is **SHA-256-hashed first**, then the digest is fed to the signer (`chain/v1_19_1/MessageBody.java:53-75`). The pre-hash buffer is:
|
||
|
||
```
|
||
salt(i64) ‖ timestampSeconds(i64) ‖ plainContent(UTF-8) ‖ 0x46
|
||
‖ [if decorated] sortedDecoratedJSON(UTF-8)
|
||
‖ for each lastSeenMessage: 0x46 ‖ uuidMSB(i64) ‖ uuidLSB(i64) ‖ signatureBytes
|
||
```
|
||
|
||
`0x46` (= byte 70, `HASH_SEPARATOR_BYTE`) delimits sections. So v2 differs from v1 in three ways: (a) a `SHA-256` pre-hash of the body, (b) inclusion of the **plain** content plus a separator before the optional decorated JSON, and (c) the **last-seen acknowledgement set** is part of the signed body.
|
||
|
||
**Serverbound chat (1.19.1, `ServerboundPackets1_19_1`):**
|
||
|
||
| Packet | ID | Change vs 1.19 |
|
||
|---|---|---|
|
||
| `CHAT_ACK` | 0x03 | **new** — acknowledge last-seen messages out of band |
|
||
| `CHAT_COMMAND` | 0x04 | +`lastSeenMessages` array + optional `lastReceivedMessage` appended |
|
||
| `CHAT` | 0x05 | +`lastSeenMessages` array + optional `lastReceivedMessage` |
|
||
| `CHAT_PREVIEW` | 0x06 | unchanged role |
|
||
|
||
Source: `v1_19to1_19_1/packet/ServerboundPackets1_19_1.java:27-30`. The IDs all shift by one to make room for `CHAT_ACK` at 0x03. ViaVersion's signing handler on the serverbound `CHAT`/`CHAT_COMMAND` calls `chatSession.signChatMessage(metadata, decoratableMessage)` and writes the resulting signature + `signedPreview = decoratableMessage.isDecorated()`, then reads (and drops, for the upstream 1.19-side) the `PLAYER_MESSAGE_SIGNATURE_ARRAY` last-seen and `OPTIONAL_PLAYER_MESSAGE_SIGNATURE`: `Protocol1_19To1_19_1.java:112-193`. `CHAT_ACK` is cancelled outbound: `:194`.
|
||
|
||
**Clientbound chat (1.19.1, `ClientboundPackets1_19_1`):**
|
||
|
||
| Packet | ID | Change vs 1.19 |
|
||
|---|---|---|
|
||
| `CUSTOM_CHAT_COMPLETIONS` | 0x15 | **new** |
|
||
| `DELETE_CHAT` | 0x18 | **new** — server retracts a message by signature |
|
||
| `PLAYER_CHAT_HEADER` | 0x32 | **new** — standalone signed header (preceding-sig + body hash) for messages whose body the client already has |
|
||
| `PLAYER_CHAT` | 0x33 | reworked: carries the last-seen context + filter mask |
|
||
| `SERVER_DATA` | 0x42 | +`enforcesSecureChat` bool appended |
|
||
| `SET_DISPLAY_CHAT_PREVIEW` | 0x4E | unchanged role |
|
||
| `SYSTEM_CHAT` | 0x62 | type field becomes an `overlay` bool downstream |
|
||
|
||
Source: `v1_19to1_19_1/packet/ClientboundPackets1_19_1.java:45,48,74,75,90,122`. ViaVersion, lacking a way to faithfully reproduce the 760 signed chat onto a 759 client, **collapses `PLAYER_CHAT` (760) back into `SYSTEM_CHAT` (759)** — re-decorating the message via the chat-type registry rather than forwarding signatures: `Protocol1_19To1_19_1.java:85-111` + the `decorateChatMessage`/`translatabaleComponentFromTag` helpers (`:321-417`). It also injects the `enforcesSecureChat` flag into `SERVER_DATA` from its own config: `:221-229` (`create(Types.BOOLEAN, Via.getConfig().enforceSecureChat())`).
|
||
|
||
**Velocity-forwarding wrinkle.** Because the profile key now travels in Velocity modern forwarding, a 760 key would reach a server expecting a 759 key; ViaVersion rewrites the `velocity:player_info` forwarding-version byte down to 1 in the login `CUSTOM_QUERY`: `Protocol1_19To1_19_1.java:284-308`.
|
||
|
||
**ViaVersion package/commits.** Package `common/.../protocols/v1_19to1_19_1/` (`Protocol1_19To1_19_1.java` + `data/{ChatDecorationResult,ChatRegistry1_19_1}.java` + `storage/{ChatTypeStorage,NonceStorage1_19_1}.java`). `git log --oneline -- …/v1_19to1_19_1`: `fe9ca4992` "Update mcstructs", `29f299d88` "Update MCStructs to 3.0.0 (#4422)", `32e51b52a` "Cleanup LOGIN/STATUS packet handlers (#4113)", `b1f64fd08` "Use enhanced switches in more places (#4043)". <!-- VERIFY: the original 760 chain/reporting implementation commit subject is not in the captured log (predates the renames) -->
|
||
|
||
---
|
||
|
||
## 761 — 1.19.3 (2022-12-07): partial revert + chain v3
|
||
|
||
**Headline.** 1.19.3 **removed chat preview entirely** ("Removed chat preview"); deleted messages now show a "deleted by the server" placeholder for ≥3s; the "Modified" tag stops appearing for style-only server edits. ([minecraft.wiki/w/Java_Edition_1.19.3](https://minecraft.wiki/w/Java_Edition_1.19.3), fetched 2026-06-19.) The wiki release article does not narrate the wire-level chat-security rework — that detail comes from ViaVersion source below.
|
||
|
||
**What was reverted / re-architected at the wire level:**
|
||
|
||
1. **Chat preview gone.** All preview packets are dropped: ViaVersion cancels `CHAT_PREVIEW`, `SET_DISPLAY_CHAT_PREVIEW`, `PLAYER_CHAT_HEADER`, and `DELETE_CHAT` when translating a 1.19.1 server down: `v1_19_1to1_19_3/Protocol1_19_1To1_19_3.java:325-328`. The standalone `PLAYER_CHAT_HEADER` packet (0x32 in 760) is **gone** from the 761 clientbound set.
|
||
2. **`signedPreview` bool removed** from serverbound `CHAT`/`CHAT_COMMAND`; the message format is acknowledgement-based instead.
|
||
3. **Last-seen moves to an offset + bitset** (`ACKNOWLEDGED_BIT_SET`) rather than a full signature array on every message — far more compact: serverbound `CHAT` (0x05) ends with `offset VarInt` + `ACKNOWLEDGED_BIT_SET`: `Protocol1_19_1To1_19_3.java:230-270`; `CHAT_COMMAND` (0x04) likewise `:176-229`.
|
||
4. **New `CHAT_SESSION_UPDATE` (serverbound 0x20)** — the player's profile key + a per-session ID are announced *in-Play* via this packet, no longer in Login Start. ViaVersion cancels it (its downstream clients have no key): `Protocol1_19_1To1_19_3.java:324`.
|
||
5. **`DISGUISED_CHAT` (clientbound 0x18) introduced** for server-authored messages that need chat-type decoration but **no signature** (e.g. `/say`, command output) — separating "decorated but unsigned" from genuinely signed `PLAYER_CHAT`. ViaVersion rewrites a 1.19.1 `PLAYER_CHAT` *down* into `DISGUISED_CHAT`: `Protocol1_19_1To1_19_3.java:128-174`.
|
||
6. **System messages out of the signed path.** Combined with `DISGUISED_CHAT`, system/`/say`-style output no longer rides the signature chain; only genuine player chat is signed, and signing is optional unless `enforce-secure-profile` forces it.
|
||
|
||
**Model — the v3 chain (`ChatSession1_19_3`).** The chain is rebuilt around a **per-session random `sessionId` + a monotonic message index** (`MessageLink`), replacing the v2 "preceding-signature header":
|
||
|
||
```java
|
||
// api/.../signature/storage/ChatSession1_19_3.java:37-54
|
||
private final UUID sessionId = UUID.randomUUID();
|
||
private MessageLink link = new MessageLink(uuid, sessionId); // index 0
|
||
...
|
||
signer.accept(Ints.toByteArray(1)); // a constant version/prefix int = 1
|
||
messageLink.update(signer); // senderUUID ‖ sessionId ‖ index(i32)
|
||
messageBody.update(signer); // see below
|
||
```
|
||
|
||
`MessageLink.update` (`chain/v1_19_3/MessageLink.java:45-49`): `senderUUID ‖ sessionId(UUID) ‖ index(i32)`, with `index` incrementing per message (`next()`, capped at `Integer.MAX_VALUE`).
|
||
|
||
`MessageBody.update` (`chain/v1_19_3/MessageBody.java:46-57`) is now fed to the signer **raw (no intermediate SHA-256 of the whole body)**:
|
||
|
||
```
|
||
salt(i64) ‖ timestampSeconds(i64) ‖ contentLength(i32) ‖ content(UTF-8)
|
||
‖ lastSeenCount(i32) ‖ for each lastSeen: signatureBytes
|
||
```
|
||
|
||
Differences from v2: (a) the `MessageHeader{precedingSig, sender}` is replaced by `MessageLink{sender, sessionId, index}` — order is now proven by a session-scoped counter rather than a literal back-link to the previous signature; (b) a constant `int 1` is prefixed; (c) the body uses a **length-prefixed** plain content and last-seen list (only the **signatureBytes**, not the UUID, per acknowledged message) and **drops the decorated JSON** from the signed payload entirely; (d) the body is no longer pre-hashed by the chain code itself.
|
||
|
||
**Serverbound chat (1.19.3, `ServerboundPackets1_19_3`):**
|
||
|
||
| Packet | ID | Change vs 1.19.1 |
|
||
|---|---|---|
|
||
| `CHAT_ACK` | 0x03 | now carries an offset/count, not a full array <!-- VERIFY: exact CHAT_ACK 761 field layout not read from enum --> |
|
||
| `CHAT_COMMAND` | 0x04 | argument sigs use `SIGNATURE_BYTES`; ends with `offset` + `ACKNOWLEDGED_BIT_SET` |
|
||
| `CHAT` | 0x05 | optional `SIGNATURE_BYTES`, ends with `offset` + `ACKNOWLEDGED_BIT_SET` |
|
||
| `CHAT_SESSION_UPDATE` | 0x20 | **new** — profile key + session id |
|
||
|
||
Source: `v1_19_1to1_19_3/packet/ServerboundPackets1_19_3.java:27,28,29,56`. ViaVersion's serverbound `CHAT` handler signs with `chatSession.signChatMessage(metadata, decoratableMessage, messagesStorage.lastSignatures())` when a session exists, else writes empty signature: `Protocol1_19_1To1_19_3.java:230-270`.
|
||
|
||
**Clientbound chat (1.19.3, `ClientboundPackets1_19_3`):**
|
||
|
||
| Packet | ID | Change vs 1.19.1 |
|
||
|---|---|---|
|
||
| `CUSTOM_CHAT_COMPLETIONS` | 0x14 | renumbered |
|
||
| `DELETE_CHAT` | 0x16 | retained (signature-based delete), preview-deletes gone |
|
||
| `DISGUISED_CHAT` | 0x18 | **new** — decorated-but-unsigned |
|
||
| `PLAYER_CHAT` | 0x31 | reworked: `previousSignature` opt + `PLAYER_MESSAGE_SIGNATURE` + last-seen + filter mask |
|
||
| `SERVER_DATA` | 0x41 | MOTD becomes mandatory Component; icon becomes byte[] (1.19.4 change) |
|
||
| `SYSTEM_CHAT` | 0x60 | renumbered; system messages now strictly unsigned |
|
||
|
||
Source: `v1_19_1to1_19_3/packet/ClientboundPackets1_19_3.java:44,46,48,73,89,120`. `PLAYER_CHAT_HEADER` and the two preview packets are **absent** from this enum.
|
||
|
||
**Acknowledgement bookkeeping.** ViaVersion maintains a `ReceivedMessagesStorage`: every incoming signed `PLAYER_CHAT` is recorded, and after 64 unacknowledged it auto-sends a `CHAT_ACK`: `Protocol1_19_1To1_19_3.java:135-148`.
|
||
|
||
**ViaVersion package/commits.** Package `common/.../protocols/v1_19_1to1_19_3/` (`Protocol1_19_1To1_19_3.java` + `storage/{NonceStorage1_19_3,ReceivedMessagesStorage}.java` + rewriters). `git log --oneline -- …/v1_19_1to1_19_3` (relevant): `3eec520eb` "Send enable features packet after the play login packet in 1.19.1->1.19.3 (#4205)", `3caaed00d` "Write enabled features as string array", `815ec24af` "Remove removed registries from command arguments", `ab3927dff` "Implement our own hash writing", `32e51b52a` "Cleanup LOGIN/STATUS packet handlers (#4113)". <!-- VERIFY: the original 761 chat-rework commit subject is not in the captured log -->
|
||
|
||
---
|
||
|
||
## 762 — 1.19.4 (2023-03-14): refinements (no signing-payload change)
|
||
|
||
**Headline.** 1.19.4 added Display entities, Interaction entities, armour trims, archaeology (brush + suspicious sand), the Cherry Grove biome, the Sniffer, and `/ride` + `/damage`. ([minecraft.wiki/w/Java_Edition_1.19.4](https://minecraft.wiki/w/Java_Edition_1.19.4), fetched 2026-06-19.)
|
||
|
||
**Chat / signing:** **the signed-message payload did not change from 761.** The only chat-security change is procedural: "Clients now reset their secure chat session state when receiving the login packet" ([1.19.4 article](https://minecraft.wiki/w/Java_Edition_1.19.4), fetched 2026-06-19) — i.e. the `MessageLink` index/session resets on a (re)Login, so a dimension change / server-switch starts a fresh chain. ViaVersion's `v1_19_3to1_19_4` package contains **no chat-signing handler at all**; its `registerPackets` only touches `COMMANDS`, `UPDATE_MOB_EFFECT` (infinite-duration −1 sentinel), and `SERVER_DATA` (MOTD → mandatory Component, icon String → `OPTIONAL_BYTE_ARRAY_PRIMITIVE`): `v1_19_3to1_19_4/Protocol1_19_3To1_19_4.java:60-105`. The `CHAT`/`PLAYER_CHAT`/`DISGUISED_CHAT` packets pass through structurally unchanged at 762 (no rewriter registered for them).
|
||
|
||
**ViaVersion package/commits.** Package `common/.../protocols/v1_19_3to1_19_4/`. `git log --oneline -- …/v1_19_3to1_19_4`: `b4d8fe6ba` "Display infinite potion effect duration for 1.19.4+…(#4953)", `97aff00ce` "Fill registries used by delayed item loading", `c13b40a37` "Add ParticleRewriter base (#4203)", `501f65e21` "Packet and entity type renames". No chat-signing commits — consistent with "no payload change at 762".
|
||
|
||
---
|
||
|
||
## Signature-chain evolution (759 → 761)
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
subgraph v1["759 (1.19) — ChatSession1_19_0"]
|
||
A1["per-message signature<br>NO chain"]
|
||
A2["payload: salt ‖ senderUUID ‖ timestamp ‖ sortedDecoratedJSON"]
|
||
A1 --> A2
|
||
end
|
||
subgraph v2["760 (1.19.1/1.19.2) — ChatSession1_19_1"]
|
||
B1["chain via PRECEDING signature<br>(MessageHeader)"]
|
||
B2["header: precedingSig ‖ senderUUID"]
|
||
B3["body (SHA-256 hashed): salt ‖ ts ‖ plain ‖ 0x46 [‖ decoratedJSON]<br>‖ for each lastSeen: 0x46 ‖ uuid ‖ sig"]
|
||
B1 --> B2 --> B3
|
||
end
|
||
subgraph v3["761 (1.19.3) — ChatSession1_19_3"]
|
||
C1["chain via sessionId + index<br>(MessageLink)"]
|
||
C2["prefix int 1 ‖ link: senderUUID ‖ sessionId ‖ index"]
|
||
C3["body: salt ‖ ts ‖ len ‖ content ‖ lastSeenCount ‖ sigs<br>(no decoratedJSON, no decorated path)"]
|
||
C1 --> C2 --> C3
|
||
end
|
||
v1 -->|"+ player reporting,<br>last-seen acks"| v2
|
||
v2 -->|"preview removed,<br>header→sessionId+index,<br>DISGUISED_CHAT split"| v3
|
||
```
|
||
|
||
Sources: `ChatSession1_19_0.java:42-51`, `ChatSession1_19_1.java:43-52` + `chain/v1_19_1/{MessageHeader,MessageBody}.java`, `ChatSession1_19_3.java:37-54` + `chain/v1_19_3/{MessageLink,MessageBody}.java`.
|
||
|
||
---
|
||
|
||
## Proxy / translation impact
|
||
|
||
A proxy or version-translation layer sitting across the 1.19 line must understand **four mutually-incompatible chat shapes**, because each protocol bump moved the signature fields and the chain rules:
|
||
|
||
1. **All four signing models coexist in the wild.** Vanilla 1.19, 1.19.1/.2, 1.19.3, and 1.19.4 clients each speak a different chat-packet shape. A proxy that translates between them (ViaVersion) cannot just renumber packets — it must **re-sign or strip-and-downgrade** the message:
|
||
- Down-translating signed chat to an older format usually means **discarding the signature and re-rendering as a system/disguised message** (ViaVersion does exactly this: 760→759 collapses `PLAYER_CHAT`→`SYSTEM_CHAT` (`Protocol1_19To1_19_1.java:85-111`); 761→760 turns `PLAYER_CHAT`→`DISGUISED_CHAT` (`Protocol1_19_1To1_19_3.java:128-174`)).
|
||
- When the proxy itself holds the player's key (a configured `ChatSession`), it **forges valid signatures** for the target format (`signChatMessage(...)` in each protocol's serverbound `CHAT`/`CHAT_COMMAND` handler).
|
||
2. **Risk: relaying stripped signatures to an `enforce-secure-profile` server.** If `enforce-secure-profile=true` (default since 1.19.1), a server **rejects** unsigned or invalidly-signed player chat. A proxy that strips signatures while down/upgrading — without re-signing — will have its players' chat dropped or the connection kicked. ViaVersion mitigates by (a) substituting its own `ChatSession` key in Login Start (759/760) / `CHAT_SESSION_UPDATE` (761), and (b) re-signing per target format. A proxy that does **not** hold the key can only forward to non-enforcing servers, or must route player chat as system/disguised (unsigned) messages — which the target may flag "Not Secure" or reject.
|
||
3. **`enforcesSecureChat` flag must be synthesised.** `SERVER_DATA` gained the `enforcesSecureChat` bool at 760; a proxy bridging from an older server has to fabricate it (ViaVersion sources it from `Via.getConfig().enforceSecureChat()`: `Protocol1_19To1_19_1.java:227`).
|
||
4. **Last-seen / acknowledgement bookkeeping is mandatory at 760+.** Because the chain folds in last-seen signatures, a translating proxy must track received `PLAYER_CHAT` signatures and emit `CHAT_ACK`/offset+bitset acknowledgements, or the server's chain validation desyncs (ViaVersion's `ReceivedMessagesStorage`, auto-`CHAT_ACK` at 64 unacked: `Protocol1_19_1To1_19_3.java:135-148`).
|
||
5. **Login-phase profile key handling differs 759/760 vs 761.** A proxy must read/strip/substitute the Login-Start profile key and the Encryption-Response salt/signature variant for 759/760, but switch to the in-Play `CHAT_SESSION_UPDATE` path for 761+. Velocity modern-forwarding carries the key too, so the forwarding-version byte may need clamping (`Protocol1_19To1_19_1.java:284-308`). See [../05-login-encryption.md](../05-login-encryption.md) §2/§5.
|
||
6. **762 is cheap.** No signing change; a proxy only resets per-session chat state on Login and handles the `SERVER_DATA` MOTD/icon type change.
|
||
|
||
---
|
||
|
||
## Sources
|
||
|
||
- minecraft.wiki release articles (fetched 2026-06-19): [1.19](https://minecraft.wiki/w/Java_Edition_1.19), [1.19.1](https://minecraft.wiki/w/Java_Edition_1.19.1), [1.19.3](https://minecraft.wiki/w/Java_Edition_1.19.3), [1.19.4](https://minecraft.wiki/w/Java_Edition_1.19.4).
|
||
- ViaVersion `/tmp/mcproto-refs/ViaVersion/` — `common/.../protocols/{v1_18_2to1_19, v1_19to1_19_1, v1_19_1to1_19_3, v1_19_3to1_19_4}/` (Protocol classes + packet enums) and `api/.../minecraft/signature/` (`ChatSession1_19_0/1_19_1/1_19_3` + `chain/v1_19_1/{MessageHeader,MessageBody}` + `chain/v1_19_3/{MessageLink,MessageBody}`). Git logs per package as cited.
|
||
- minecraft-data `/tmp/mcproto-refs/minecraft-data/data/pc/` — `1.19`,`1.19.2`,`1.19.3`,`1.19.4` `version.json` + `common/protocolVersions.json` (protocol numbers + dataVersions).
|
||
- Cross-link: [../05-login-encryption.md](../05-login-encryption.md) (profile keys in Login Start; Encryption Response salt/signature form).
|
||
```
|