-
-
Notifications
You must be signed in to change notification settings - Fork 253
1.21.5 - Add support for new protocol checksum & chatIndex #1386
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,6 +23,19 @@ module.exports = function (client, options) { | |
client._lastChatSignature = null | ||
client._lastRejectedMessage = null | ||
|
||
// Chat packet index tracking (for 1.21.5+) | ||
client._expectedChatIndex = 0 | ||
|
||
client.on('login', () => { | ||
// Reset expected chat index on login | ||
client._expectedChatIndex = 0 | ||
}) | ||
|
||
// Reset expected chat index when entering configuration state | ||
client.on('finish_configuration', () => { | ||
client._expectedChatIndex = 0 | ||
}) | ||
|
||
// This stores the last n (5 or 20) messages that the player has seen, from unique players | ||
if (mcData.supportFeature('chainedChatWithHashing')) client._lastSeenMessages = new LastSeenMessages() | ||
else client._lastSeenMessages = new LastSeenMessagesWithInvalidation() | ||
|
@@ -45,6 +58,23 @@ module.exports = function (client, options) { | |
} | ||
}() | ||
|
||
// 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 | ||
} | ||
|
||
// Simple hash calculation from the last seen signatures | ||
let checksum = 0 | ||
for (const msg of lastSeen) { | ||
if (msg.messageSignature) { | ||
// XOR the first byte of each signature for a simple checksum | ||
checksum ^= msg.messageSignature[0] || 0 | ||
} | ||
} | ||
return checksum | ||
} | ||
|
||
function updateAndValidateSession (uuid, message, currentSignature, index, previousMessages, salt, timestamp) { | ||
const player = client._players[uuid] | ||
|
||
|
@@ -195,6 +225,16 @@ module.exports = function (client, options) { | |
}) | ||
|
||
client.on('player_chat', (packet) => { | ||
// Validate chat packet index for 1.21.5+ | ||
if (mcData.supportFeature('chatIndex') && 'index' in packet) { | ||
if (packet.index !== client._expectedChatIndex) { | ||
client.emit('error', new Error(`Chat index mismatch. Expected: ${client._expectedChatIndex}, got: ${packet.index}`)) | ||
client.end('Chat message received out of order') | ||
return | ||
Comment on lines
+231
to
+233
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as other comment |
||
} | ||
client._expectedChatIndex++ | ||
} | ||
|
||
if (mcData.supportFeature('useChatSessions')) { | ||
const tsDelta = BigInt(Date.now()) - packet.timestamp | ||
const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0 | ||
|
@@ -372,17 +412,27 @@ module.exports = function (client, options) { | |
if (mcData.supportFeature('useChatSessions')) { // 1.19.3+ | ||
const { acknowledged, acknowledgements } = getAcknowledgements() | ||
const canSign = client.profileKeys && client._session | ||
client.write((mcData.supportFeature('seperateSignedChatCommandPacket') && canSign) ? 'chat_command_signed' : 'chat_command', { | ||
|
||
// Prepare the packet parameters | ||
const params = { | ||
command, | ||
timestamp: options.timestamp, | ||
salt: options.salt, | ||
argumentSignatures: canSign ? signaturesForCommand(command, options.timestamp, options.salt, options.preview, acknowledgements) : [], | ||
messageCount: client._lastSeenMessages.pending, | ||
acknowledged | ||
}) | ||
} | ||
|
||
// Add checksum for 1.21.5+ | ||
if (mcData.supportFeature('chatChecksum')) { | ||
params.checksum = options.checksum !== undefined ? options.checksum : calculateChecksum(client._lastSeenMessages) | ||
Comment on lines
+426
to
+428
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
} | ||
|
||
client.write((mcData.supportFeature('seperateSignedChatCommandPacket') && canSign) ? 'chat_command_signed' : 'chat_command', params) | ||
client._lastSeenMessages.pending = 0 | ||
} else { | ||
client.write('chat_command', { | ||
// Prepare the packet parameters | ||
const params = { | ||
command, | ||
timestamp: options.timestamp, | ||
salt: options.salt, | ||
|
@@ -393,29 +443,46 @@ module.exports = function (client, options) { | |
messageSignature: e.signature | ||
})), | ||
lastRejectedMessage: client._lastRejectedMessage | ||
}) | ||
} | ||
|
||
// Add checksum for 1.21.5+ | ||
if (mcData.supportFeature('chatChecksum')) { | ||
params.checksum = options.checksum !== undefined ? options.checksum : calculateChecksum(client._lastSeenMessages) | ||
} | ||
|
||
client.write('chat_command', params) | ||
} | ||
|
||
return | ||
} | ||
|
||
if (mcData.supportFeature('useChatSessions')) { | ||
const { acknowledgements, acknowledged } = getAcknowledgements() | ||
client.write('chat_message', { | ||
|
||
// Prepare the packet parameters | ||
const params = { | ||
message, | ||
timestamp: options.timestamp, | ||
salt: options.salt, | ||
signature: (client.profileKeys && client._session) ? client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined, | ||
offset: client._lastSeenMessages.pending, | ||
acknowledged | ||
}) | ||
} | ||
|
||
// Add checksum for 1.21.5+ | ||
if (mcData.supportFeature('chatChecksum')) { | ||
params.checksum = options.checksum !== undefined ? options.checksum : calculateChecksum(client._lastSeenMessages) | ||
} | ||
|
||
client.write('chat_message', params) | ||
client._lastSeenMessages.pending = 0 | ||
|
||
return | ||
} | ||
|
||
if (options.skipPreview || !client.serverFeatures.chatPreview) { | ||
client.write('chat_message', { | ||
// Prepare the packet parameters | ||
const params = { | ||
message, | ||
timestamp: options.timestamp, | ||
salt: options.salt, | ||
|
@@ -426,7 +493,14 @@ module.exports = function (client, options) { | |
messageSignature: e.signature | ||
})), | ||
lastRejectedMessage: client._lastRejectedMessage | ||
}) | ||
} | ||
|
||
// Add checksum for 1.21.5+ | ||
if (mcData.supportFeature('chatChecksum')) { | ||
params.checksum = options.checksum !== undefined ? options.checksum : calculateChecksum(client._lastSeenMessages) | ||
} | ||
|
||
client.write('chat_message', params) | ||
client._lastSeenMessages.pending = 0 | ||
} else if (client.serverFeatures.chatPreview) { | ||
client.write('chat_preview', { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -43,6 +43,19 @@ module.exports = function (client, server, options) { | |
|
||
if (!options.generatePreview) options.generatePreview = message => message | ||
|
||
// Chat index tracking for 1.21.5+ | ||
client._chatMessageIndex = 0 | ||
|
||
// Reset chat index when client enters play state | ||
client.on('login', () => { | ||
client._chatMessageIndex = 0 | ||
}) | ||
|
||
// Reset chat index when entering configuration state | ||
client.on('finish_configuration', () => { | ||
client._chatMessageIndex = 0 | ||
}) | ||
|
||
function validateMessageChain (packet) { | ||
try { | ||
validateLastMessages(pending, packet.previousMessages, packet.lastRejectedMessage) | ||
|
@@ -104,6 +117,16 @@ module.exports = function (client, server, options) { | |
} | ||
lastTimestamp = packet.timestamp | ||
|
||
// Verify checksum if supported | ||
if (client.supportFeature('chatChecksum') && 'checksum' in packet) { | ||
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 | ||
Comment on lines
+122
to
+126
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
} | ||
} | ||
|
||
// Checks here: 1) make sure client can chat, 2) chain/session is OK, 3) signature is OK, 4) log if expired | ||
if (client.settings.disabledChat) return raise('chat.disabled.options') | ||
if (client.supportFeature('chainedChatWithHashing')) validateMessageChain(packet) // 1.19.1 | ||
|
@@ -112,6 +135,17 @@ module.exports = function (client, server, options) { | |
if ((BigInt(Date.now()) - packet.timestamp) > messageExpireTime) debug(client.socket.address(), 'sent expired message TS', packet.timestamp) | ||
}) | ||
|
||
client.on('chat_command', (packet) => { | ||
// Verify checksum if supported | ||
if (client.supportFeature('chatChecksum') && 'checksum' in packet) { | ||
const calculatedChecksum = calculateChecksum(packet.previousMessages) | ||
if (packet.checksum !== 0 && packet.checksum !== calculatedChecksum) { | ||
debug('Chat command checksum mismatch', packet.checksum, calculatedChecksum) | ||
raise('multiplayer.disconnect.chat_validation_failed') | ||
} | ||
} | ||
}) | ||
|
||
// Client will occasionally send a list of seen messages to the server, here we listen & check chain validity | ||
client.on('message_acknowledgement', (packet) => { | ||
if (client.supportFeature('useChatSessions')) { | ||
|
@@ -170,6 +204,31 @@ module.exports = function (client, server, options) { | |
} | ||
return true | ||
} | ||
|
||
// Helper function to calculate checksum | ||
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 | ||
} | ||
Comment on lines
+209
to
+220
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
// 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 | ||
} | ||
Comment on lines
+222
to
+231
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not good There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @extremeheat just to clarify, why is that change not good? |
||
} | ||
|
||
class LastSeenMessages extends Array { | ||
|
There was a problem hiding this comment.
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"?