Skip to content

Conversation

RomainNeup
Copy link

@RomainNeup RomainNeup commented Mar 26, 2025

resolve #1385
Waiting for PrismarineJS/minecraft-data#995 to be merge for minecraft-data to be up to date

Comment on lines +222 to +231
// Override and extend logSentMessageFromPeer to increment index
const originalLogSent = client.logSentMessageFromPeer
client.logSentMessageFromPeer = function (chatPacket) {
// Include chat index in outgoing player_chat packets
if (client.supportFeature('chatIndex')) {
chatPacket.index = client._chatMessageIndex++
}

return originalLogSent ? originalLogSent.call(this, chatPacket) : true
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not good

Copy link

@Arc8ne Arc8ne Jun 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@extremeheat just to clarify, why is that change not good?

Comment on lines +122 to +126
const calculatedChecksum = calculateChecksum(packet.previousMessages)
if (packet.checksum !== 0 && packet.checksum !== calculatedChecksum) {
debug('Chat checksum mismatch', packet.checksum, calculatedChecksum)
raise('multiplayer.disconnect.chat_validation_failed')
return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we should be enforcing all these checks at the library level as opposed to the user level. We did add some abstractions to chat for the purpose of not having to reimplement complex logic on the user's side. Perhaps it would make sense to have these kind of kick checks behind a flag.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@extremeheat is there an existing flag that these kinds of kick checks could be behind or would a new flag have to be created?

Comment on lines +231 to +233
client.emit('error', new Error(`Chat index mismatch. Expected: ${client._expectedChatIndex}, got: ${packet.index}`))
client.end('Chat message received out of order')
return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as other comment

Comment on lines +426 to +428
// Add checksum for 1.21.5+
if (mcData.supportFeature('chatChecksum')) {
params.checksum = options.checksum !== undefined ? options.checksum : calculateChecksum(client._lastSeenMessages)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: if both the if branches rely on same code you can avoid the duplicate code here by putting it up top

// Calculate checksum for the lastSeen message signatures (1.21.5+)
const calculateChecksum = (lastSeen) => {
if (!mcData.supportFeature('chatChecksum') || !lastSeen || lastSeen.length === 0) {
return 0 // Default value for backward compatibility
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate here what you mean by "backward compatibility"?

@RomainNeup
Copy link
Author

RomainNeup commented Mar 26, 2025

I still have an issue on the client side. I'm investigating, it seems related to the login packet but I checked the protocol (directly by decompiling the client and comparing with 1.21.4) and nothing as changed (at least from my POV).

PartialReadError: Read error for undefined : Missing characters in string, found size is 9 expected size was 12
    at new ExtendableError (/Users/romainneuplanche/Documents/perso/github_public/node-minecraft-protocol/node_modules/protodef/src/utils.js:63:13)
    at new PartialReadError (/Users/romainneuplanche/Documents/perso/github_public/node-minecraft-protocol/node_modules/protodef/src/utils.js:70:5)
    at Object.string (eval at compile (/Users/romainneuplanche/Documents/perso/github_public/node-minecraft-protocol/node_modules/protodef/src/compiler.js:262:12), <anonymous>:106:15)
    at Object.packet_scoreboard_display_objective (eval at compile (/Users/romainneuplanche/Documents/perso/github_public/node-minecraft-protocol/node_modules/protodef/src/compiler.js:262:12), <anonymous>:3360:57)
    at eval (eval at compile (/Users/romainneuplanche/Documents/perso/github_public/node-minecraft-protocol/node_modules/protodef/src/compiler.js:262:12), <anonymous>:4122:96)
    at packet (eval at compile (/Users/romainneuplanche/Documents/perso/github_public/node-minecraft-protocol/node_modules/protodef/src/compiler.js:262:12), <anonymous>:4162:9)
    at CompiledProtodef.read (/Users/romainneuplanche/Documents/perso/github_public/node-minecraft-protocol/node_modules/protodef/src/compiler.js:70:12)
    at e.message (/Users/romainneuplanche/Documents/perso/github_public/node-minecraft-protocol/node_modules/protodef/src/compiler.js:111:49)
    at tryCatch (/Users/romainneuplanche/Documents/perso/github_public/node-minecraft-protocol/node_modules/protodef/src/utils.js:50:16)
    at CompiledProtodef.parsePacketBuffer (/Users/romainneuplanche/Documents/perso/github_public/node-minecraft-protocol/node_modules/protodef/src/compiler.js:111:29)

@rom1504
Copy link
Member

rom1504 commented Apr 27, 2025

part of 1.21.5 support; tracked at PrismarineJS/mineflayer#3641

@Arc8ne
Copy link

Arc8ne commented Jun 28, 2025

I still have an issue on the client side. I'm investigating, it seems related to the login packet but I checked the protocol (directly by decompiling the client and comparing with 1.21.4) and nothing as changed (at least from my POV).

PartialReadError: Read error for undefined : Missing characters in string, found size is 9 expected size was 12
    at new ExtendableError (/Users/romainneuplanche/Documents/perso/github_public/node-minecraft-protocol/node_modules/protodef/src/utils.js:63:13)
    at new PartialReadError (/Users/romainneuplanche/Documents/perso/github_public/node-minecraft-protocol/node_modules/protodef/src/utils.js:70:5)
    at Object.string (eval at compile (/Users/romainneuplanche/Documents/perso/github_public/node-minecraft-protocol/node_modules/protodef/src/compiler.js:262:12), <anonymous>:106:15)
    at Object.packet_scoreboard_display_objective (eval at compile (/Users/romainneuplanche/Documents/perso/github_public/node-minecraft-protocol/node_modules/protodef/src/compiler.js:262:12), <anonymous>:3360:57)
    at eval (eval at compile (/Users/romainneuplanche/Documents/perso/github_public/node-minecraft-protocol/node_modules/protodef/src/compiler.js:262:12), <anonymous>:4122:96)
    at packet (eval at compile (/Users/romainneuplanche/Documents/perso/github_public/node-minecraft-protocol/node_modules/protodef/src/compiler.js:262:12), <anonymous>:4162:9)
    at CompiledProtodef.read (/Users/romainneuplanche/Documents/perso/github_public/node-minecraft-protocol/node_modules/protodef/src/compiler.js:70:12)
    at e.message (/Users/romainneuplanche/Documents/perso/github_public/node-minecraft-protocol/node_modules/protodef/src/compiler.js:111:49)
    at tryCatch (/Users/romainneuplanche/Documents/perso/github_public/node-minecraft-protocol/node_modules/protodef/src/utils.js:50:16)
    at CompiledProtodef.parsePacketBuffer (/Users/romainneuplanche/Documents/perso/github_public/node-minecraft-protocol/node_modules/protodef/src/compiler.js:111:29)

@RomainNeup Is it alright if you could provide a quick overview / steps on how to reproduce this issue? I would be willing to look into it further.

@extremeheat extremeheat changed the title Add support for new protocol checksum & chatIndex 1.21.5 - Add support for new protocol checksum & chatIndex Jun 29, 2025
Copy link
Member

@extremeheat extremeheat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The big changes for nmp seem related to chat.

There's a new checksum and a new globalIndex. I attached some explanation from Gemini generated from the Minecraft code diff. Basically, client needs no changes for globalIndex, it's for bookkeeping purposes only whereas checksum is generated by client and enforced by the server.

globalIndex

The addition of globalIndex to the ClientboundPlayerChatPacket is a key change for making the chat system more robust. Here's a detailed breakdown of how it works.

What is globalIndex?

The globalIndex is a simple, monotonically increasing integer assigned by the server to every chat message packet it sends to a client.

On the server side (specifically in ServerGamePacketListenerImpl.java), a new counter nextChatIndex has been introduced. Each time the server sends a chat packet to a player, it does the following:

  1. Gets the current value of nextChatIndex.
  2. Includes this value in the ClientboundPlayerChatPacket as the globalIndex.
  3. Increments nextChatIndex by one.

This means that from the client's perspective, every single message it receives in its chat feed—whether from a player, the system, or a death message—now has a unique, sequential ID.

How is it different from the existing index?

This is the most important distinction to understand. The packet now contains two indices, and they serve entirely different purposes.

  • index (from SignedMessageLink):

    • Scope: Per-player.
    • Managed by: The sending client.
    • Purpose: Cryptographic Chaining and Security. This index is part of the secure chat system. Each player maintains their own message counter. When they send a message, they include their current index and sign it. This allows the server and other clients to verify that the message hasn't been tampered with and that it's part of an unbroken sequence from that specific player. It's for message authenticity.
  • globalIndex:

    • Scope: Per-receiver (the client's view of chat).
    • Managed by: The server.
    • Purpose: Message Management and Identification. This index is used by the server to give the client a simple, unambiguous way to refer to a specific message later. Its primary function is to make deleting messages reliable. It's for message identification.

Why was globalIndex added?

The globalIndex solves a critical problem: how to reliably and uniquely identify a message for deletion.

Previously, the ClientboundDeleteChatPacket had to identify a message using its MessageSignature. This had several drawbacks:

  1. System Messages: System messages (like "Player has joined the game" or command output) don't have a cryptographic signature, so they couldn't be easily targeted for deletion.
  2. Ambiguity: While unlikely, it's theoretically possible for signature collisions or other edge cases to create ambiguity.
  3. Complexity: Relying on a complex cryptographic signature for a simple identification task is overkill.

The globalIndex provides a much simpler and more robust solution.

How it Works in Practice (Example Scenario)

Imagine you are a client connected to a server. Here's what your chat log might look like with the new system:

  1. Server sends a welcome message:

    • Packet: ClientboundSystemChatPacket (no globalIndex here, it's part of the player chat packet) - let's assume the server processes this and wants to show it to you. Correction: The server would wrap this in a player-chat-like structure to send to the client. Let's adjust the example. The server sends chat packets that result in these lines appearing on your screen.
  2. PlayerA sends "Hi everyone!"

    • Server sends you ClientboundPlayerChatPacket with:
      • globalIndex: 101
      • sender: PlayerA
      • index: 5 (PlayerA's 5th message)
  3. You die to a zombie.

    • Server sends you a system message packet representing the death message.
    • It might be assigned globalIndex: 102.
  4. A moderator wants to delete PlayerA's message.

    • The server now has a perfect, unambiguous "handle" for that message. It can send a new (or modified) ClientboundDeleteChatPacket that simply says: "Delete message with globalIndex 101."
    • The client receives this, finds the message in its chat history with globalIndex 101, and removes it. There's no need to parse content or compare complex signatures.

Summary

In short, the globalIndex is a server-side serial number for chat messages sent to a client. It was added to provide a simple and foolproof way for the server to identify and manage messages on the client side, primarily for making message deletion robust and reliable for all types of chat content. It works alongside the existing per-player index, which is used for cryptographic security.

checksum

Of course! The new checksum byte is a clever and efficient mechanism for ensuring data integrity within the secure chat system.

What the Checksum Does (The 'Why')

The primary purpose of the checksum is to detect desynchronization between the client and the server.

In the secure chat system, the client periodically sends a LastSeenMessages.Update packet to the server. This packet tells the server, "Here is the list of recent messages I have seen and acknowledged." The server uses this information to validate the cryptographic chain of subsequent messages from the player.

However, a problem can arise if the client's and server's understanding of the "list of recent messages" diverges. This is called a desync. For example:

  • The client might have missed a message packet.
  • The server might have processed messages in a slightly different order.

If a desync occurs, the client's acknowledgement update becomes meaningless. The client might say "I've seen message #5," but on the server, message #5 could be a completely different message. This would cause the server to incorrectly validate or invalidate future chat messages.

The checksum byte solves this problem by acting as a lightweight "fingerprint" of the client's list of last seen messages.

Here's the workflow:

  1. The client generates the list of messages it has seen (LastSeenMessages).
  2. It computes a checksum byte based on this list.
  3. It sends the LastSeenMessages.Update packet, which contains the list of acknowledgements and the checksum.
  4. The server receives the update. Before applying it, it reconstructs what it believes the client's last seen message list should be and computes its own checksum.
  5. It compares the checksum it just computed with the checksum received from the client.
    • If they match: Everything is in sync. The server can trust the acknowledgement update and proceeds.
    • If they do not match: A desync has occurred. The server knows it cannot trust the client's chat state and disconnects the player with the error message: "Checksum mismatch on last seen update: the client and server must have desynced". This is a fail-safe to prevent the secure chat system from malfunctioning.

There's one special case: a checksum of 0 is ignored, likely for backward compatibility or special packet handling.

How the Checksum is Generated (The 'How')

The checksum is generated through a simple, deterministic hashing process. Here is the step-by-step breakdown based on the code changes:

Step 1: The Checksum of a Single Message Signature

The fundamental building block is the checksum of an individual MessageSignature. A new method checksum() was added to MessageSignature.java:

// in MessageSignature.java
int checksum() {
   return Arrays.hashCode(this.bytes);
}

This uses Java's standard Arrays.hashCode() on the raw byte array of the signature. It produces a consistent int hash for any given signature.

Step 2: Aggregating into a List Checksum

Next, the LastSeenMessages class (which is essentially a list of signatures) aggregates these individual checksums into one. A new method computeChecksum() was added:

// in LastSeenMessages.java
byte computeChecksum() {
   int b = 1; // Initial value
   for(MessageSignature d : this.entries) {
      b = 31 * b + d.checksum(); // Classic hashing pattern
   }
   byte e = (byte)b;
   return e == 0 ? 1 : e; // Avoids the special '0' value
}

This method iterates through all the message signatures in its list and combines their individual checksums using a standard hashing algorithm (hash = 31 * hash + new_element_hash).

Step 3: Converting to a Byte and Sending

  1. The final int result from the loop is cast to a byte. This simply takes the lowest 8 bits of the integer, which is a fast way to create a small checksum.
  2. The code explicitly checks if the resulting byte is 0. Since 0 is used as a special value to ignore the checksum, the method ensures a valid checksum can never be zero by changing it to 1 if it is.
  3. This final byte is then included in the LastSeenMessages.Update packet and sent from the client to the server for validation.

In summary, the checksum is a hash of hashes—an aggregation of the hash codes of all message signatures in the client's "last seen" list. It's a very fast and efficient way for the server to confirm that its view of the client's chat history is perfectly in sync, making the entire secure chat system more reliable.

Comment on lines +209 to +220
function calculateChecksum (messages) {
if (!messages || messages.length === 0) return 0

let checksum = 0
for (const msg of messages) {
if (msg.signature) {
// XOR the first byte of each signature for a simple checksum
checksum ^= msg.signature[0] || 0
}
}
return checksum
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The checksum algorithm doesn't seem to match Minecraft, did you write this intentionally? The server does verify this, so this has to be correct if chat signing is enabled (the default) or the server will kick for CHAT_VALIDATION_FAILED. I attached some logs from Gemini explaining the changes below

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support protocol 770 (minecraft pc 1.21.5
4 participants