diff --git a/docs/HISTORY.md b/docs/HISTORY.md index ed5e9d30..69faad19 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,8 @@ # History +## 1.56.0 +* Add support for Minecraft 1.21.5 + ## 1.55.0 * [Fix `client.end()` (#1376)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/3bd4dc1b2002cd7badfa5b9cf8dda35cd6cc9ac1) (thanks @h5mcbox) * [Fix #1369 online-mode error 1.20.5-1.21.4 (#1375)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/5ec3dd4b367fcc039fbcb3edd214fe3cf8178a6d) (thanks @h5mcbox) diff --git a/docs/README.md b/docs/README.md index fbcaa436..815241cb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,7 @@ Parse and serialize minecraft packets, plus authentication and encryption. * Supports Minecraft PC version 1.7.10, 1.8.8, 1.9 (15w40b, 1.9, 1.9.1-pre2, 1.9.2, 1.9.4), 1.10 (16w20a, 1.10-pre1, 1.10, 1.10.1, 1.10.2), 1.11 (16w35a, 1.11, 1.11.2), 1.12 (17w15a, 17w18b, 1.12-pre4, 1.12, 1.12.1, 1.12.2), and 1.13 (17w50a, 1.13, 1.13.1, 1.13.2-pre1, 1.13.2-pre2, 1.13.2), 1.14 (1.14, 1.14.1, 1.14.3, 1.14.4) - , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4), 1.20 (1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6), 1.21 (1.21, 1.21.1, 1.21.3, 1.21.4) + , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4), 1.20 (1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6), 1.21 (1.21, 1.21.1, 1.21.3, 1.21.4, 1.21.5) * Parses all packets and emits events with packet fields as JavaScript objects. * Send a packet by supplying fields as a JavaScript object. diff --git a/package.json b/package.json index 687b1c8b..4403b4fe 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "endian-toggle": "^0.0.0", "lodash.get": "^4.1.2", "lodash.merge": "^4.3.0", - "minecraft-data": "^3.78.0", + "minecraft-data": "^3.85.0", "minecraft-folder-path": "^1.2.0", "node-fetch": "^2.6.1", "node-rsa": "^0.4.2", diff --git a/src/client/chat.js b/src/client/chat.js index f14269be..d74242e5 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -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 + } + 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) + } + + 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,7 +443,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_command', params) } return @@ -401,21 +458,31 @@ module.exports = function (client, options) { 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', { diff --git a/src/client/play.js b/src/client/play.js index 6e06dc15..bd4c9f5b 100644 --- a/src/client/play.js +++ b/src/client/play.js @@ -48,9 +48,13 @@ module.exports = function (client, options) { function enterConfigState (finishCb) { if (client.state === states.CONFIGURATION) return - // If we are returning to the configuration state from the play state, we ahve to acknowledge it. + // If we are returning to the configuration state from the play state, we have to acknowledge it. if (client.state === states.PLAY) { client.write('configuration_acknowledged', {}) + // Reset chat index counter when returning to configuration state + if (client._expectedChatIndex !== undefined) { + client._expectedChatIndex = 0 + } } client.state = states.CONFIGURATION client.on('select_known_packs', () => { @@ -66,6 +70,11 @@ module.exports = function (client, options) { } function onReady () { + // Reset chat index on player join + if (mcData.supportFeature('chatIndex')) { + client._expectedChatIndex = 0 + } + client.emit('playerJoin') if (mcData.supportFeature('signedChat')) { if (options.disableChatSigning && client.serverFeatures.enforcesSecureChat) { diff --git a/src/server/chat.js b/src/server/chat.js index dc9dc3b0..ca1500a7 100644 --- a/src/server/chat.js +++ b/src/server/chat.js @@ -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 + } + } + // 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 + } + + // 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 + } } class LastSeenMessages extends Array { diff --git a/src/version.js b/src/version.js index dfed3bb2..c30b95e6 100644 --- a/src/version.js +++ b/src/version.js @@ -2,5 +2,5 @@ module.exports = { defaultVersion: '1.21.1', - supportedVersions: ['1.7', '1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2', '1.20.4', '1.20.6', '1.21.1', '1.21.3', '1.21.4'] + supportedVersions: ['1.7', '1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2', '1.20.4', '1.20.6', '1.21.1', '1.21.3', '1.21.4', '1.21.5'] }