Update to 1.19.3 (#1069)

* Add 1.19.3 player_info parsing

* player_remove packet parsing

* 1.19.3 chat parsing

* Outgoing chat for 1.19.3

* Fix lint

* Server chat validation

* add 1.19.2 and 1.19.3 in version.js

* Add 1.19.2 and 1.19.3 in ci.yml

* Deprecated client.verifyMessage for server clients

* Update docs

* Deprecate client.verifyMessage for server clients

* Fix tests

* Fix lint

* Fix packetTest

* Fix test

* Remove unneeded log statement

* Update types/docs

* Remove unnecessary feature check

* Remove _session from docs

Co-authored-by: Romain Beaumont <romain.rom1@gmail.com>
This commit is contained in:
Frej Alexander Nielsen 2023-01-21 20:31:17 +01:00 committed by GitHub
parent 92a5219915
commit cf1f67117d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 461 additions and 98 deletions

View file

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
mcVersion: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17', '1.17.1', '1.18.2', '1.19']
mcVersion: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3']
fail-fast: false
steps:

View file

@ -44,7 +44,7 @@ Write a packet to all `clients` but encode it only once.
### client.verifyMessage(packet) : boolean
Verifies if player's chat message packet was signed with their Mojang provided key
(1.19-1.19.2) Verifies if player's chat message packet was signed with their Mojang provided key. Handled internally (and thus deprecated) in 1.19.3 and above
### client.logSentMessageFromPeer(packet)
(1.19.1+) You must call this function when the server receives a message from a player and that message gets
@ -306,7 +306,7 @@ Unregister a channel `name` and send the unregister packet if `custom` is true.
### client.chat(message)
Send a chat message to the server, with signing on 1.19+.
### client.signMessage(message: string, timestamp: BigInt, salt?: number) : Buffer
### client.signMessage(message: string, timestamp: BigInt, salt?: number, preview?: string, acknowledgements?: Buffer[]) : Buffer
(1.19) Generate a signature for a chat message to be sent to server

View file

@ -22,8 +22,13 @@ module.exports = function (client, options) {
client._lastChatSignature = null
client._lastRejectedMessage = null
// This stores the last 5 messages that the player has seen, from unique players
client._lastSeenMessages = new LastSeenMessages()
// 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()
// This stores the last 128 inbound (signed) messages for 1.19.3 chat validation
client._signatureCache = new SignatureCache()
// This stores last 1024 inbound messages for report lookup
client._lastChatHistory = new class extends Array {
capacity = 1024
@ -35,6 +40,31 @@ module.exports = function (client, options) {
}
}()
function updateAndValidateSession (uuid, message, currentSignature, index, previousMessages, salt, timestamp) {
const player = client._players[uuid]
if (player && player.hasChainIntegrity) {
if (!player.lastSignature || player.lastSignature.equals(currentSignature) || index > player.sessionIndex) {
player.lastSignature = currentSignature
} else {
player.hasChainIntegrity = false
}
if (player.hasChainIntegrity) {
const length = Buffer.byteLength(message, 'utf8')
const acknowledgements = previousMessages.length > 0 ? ['i32', previousMessages.length, 'buffer', Buffer.concat(...previousMessages.map(msg => msg.signature || client._signatureCache[msg.id]))] : ['i32', 0]
const signable = concat('i32', 1, 'UUID', uuid, 'UUID', player.sessionUuid, 'i32', index, 'i64', salt, 'i64', timestamp / 1000n, 'i32', length, 'pstring', message, ...acknowledgements)
player.hasChainIntegrity = crypto.verify('RSA-SHA256', signable, player.publicKey, currentSignature)
}
return player.hasChainIntegrity
}
return false
}
function updateAndValidateChat (uuid, previousSignature, currentSignature, payload) {
// Get the player information
const player = client._players[uuid]
@ -63,7 +93,30 @@ module.exports = function (client, options) {
return false
}
client.on('player_remove', (packet) => {
for (const player of packet.players) {
delete client._players[player.UUID]
}
})
client.on('player_info', (packet) => {
if (mcData.supportFeature('playerInfoActionIsBitfield')) { // 1.19.3+
if (packet.action & 2) { // chat session
for (const player of packet.data) {
if (!player.chatSession) continue
client._players[player.UUID] = {
publicKey: crypto.createPublicKey({ key: player.chatSession.publicKey.keyBytes, format: 'der', type: 'spki' }),
publicKeyDER: player.chatSession.publicKey.keyBytes,
sessionUuid: player.chatSession.uuid
}
client._players[player.UUID].sessionIndex = true
client._players[player.UUID].hasChainIntegrity = true
}
}
return
}
if (packet.action === 0) { // add player
for (const player of packet.data) {
if (player.crypto) {
@ -83,88 +136,163 @@ module.exports = function (client, options) {
}
})
client.on('profileless_chat', (packet) => {
// Profileless chat is parsed as an unsigned player chat message but logged as a system message
client.emit('playerChat', {
formattedMessage: packet.message,
type: packet.type,
senderName: packet.name,
targetName: packet.target,
verified: false
})
client._lastChatHistory.push({
type: 2, // System message
message: {
decorated: packet.content // This should actually decorate the message with the sender and target name using the chat type
},
timestamp: Date.now()
})
})
client.on('system_chat', (packet) => {
client.emit('systemChat', {
positionid: packet.isActionBar ? 2 : 1,
formattedMessage: packet.content
})
client._lastChatHistory.push({
type: 2, // System message
message: {
decorated: packet.content
},
timestamp: Date.now()
})
})
client.on('message_header', (packet) => {
updateAndValidateChat(packet.senderUuid, packet.previousSignature, packet.signature, packet.messageHash)
client._lastChatHistory.push({
type: 1, // Message header
previousSignature: packet.previousSignature,
signature: packet.signature,
messageHash: packet.messageHash
})
})
client.on('hide_message', (packet) => {
if (mcData.supportFeature('useChatSessions')) {
const signature = packet.signature || client._signatureCache[packet.id]
if (signature) client._lastSeenMessages = client._lastSeenMessages.map(ack => (ack.signature === signature && ack.pending) ? null : ack)
}
})
client.on('player_chat', (packet) => {
if (!mcData.supportFeature('chainedChatWithHashing')) { // 1.19.0
const pubKey = client._players[packet.senderUuid]?.publicKey
if (mcData.supportFeature('useChatSessions')) {
const tsDelta = BigInt(Date.now()) - packet.timestamp
const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0
const verified = !packet.unsignedChatContent && updateAndValidateSession(packet.senderUuid, packet.plainMessage, packet.signature, packet.index, packet.previousMessages, packet.salt, packet.timestamp) && !expired
client.emit('playerChat', {
formattedMessage: packet.signedChatContent || packet.unsignedChatContent,
plainMessage: packet.plainMessage,
unsignedContent: packet.unsignedContent,
type: packet.type,
sender: packet.senderUuid,
senderName: packet.senderName,
senderTeam: packet.senderTeam,
verified: (pubKey && !packet.unsignedChatContent) ? client.verifyMessage(pubKey, packet) : false
senderName: packet.networkName,
targetName: packet.networkTargetName,
verified
})
client._lastChatHistory.push({
type: 0, // Player message
signature: packet.signature,
message: {
plain: packet.plainMessage
},
session: {
index: packet.index,
uuid: client._players[packet.senderUuid]?.sessionUuid
},
timestamp: packet.timestamp,
salt: packet.salt,
lastSeen: packet.previousMessages.map(msg => msg.signature || client._signatureCache[msg.id])
})
if (client._lastSeenMessages.push(packet.signature) && client._lastSeenMessages.pending > 64) {
client.write('message_acknowledgement', {
count: client._lastSeenMessages.pending
})
client._lastSeenMessages.pending = 0
}
return
}
const hash = crypto.createHash('sha256')
hash.update(concat('i64', packet.salt, 'i64', packet.timestamp / 1000n, 'pstring', packet.plainMessage, 'i8', 70))
if (packet.formattedMessage) hash.update(packet.formattedMessage)
for (const previousMessage of packet.previousMessages) {
hash.update(concat('i8', 70, 'UUID', previousMessage.messageSender))
hash.update(previousMessage.messageSignature)
if (mcData.supportFeature('chainedChatWithHashing')) {
const hash = crypto.createHash('sha256')
hash.update(concat('i64', packet.salt, 'i64', packet.timestamp / 1000n, 'pstring', packet.plainMessage, 'i8', 70))
if (packet.formattedMessage) hash.update(packet.formattedMessage)
for (const previousMessage of packet.previousMessages) {
hash.update(concat('i8', 70, 'UUID', previousMessage.messageSender))
hash.update(previousMessage.messageSignature)
}
// Chain integrity remains even if message is considered unverified due to expiry
const tsDelta = BigInt(Date.now()) - packet.timestamp
const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0
const verified = !packet.unsignedChatContent && updateAndValidateChat(packet.senderUuid, packet.previousSignature, packet.signature, hash.digest()) && !expired
client.emit('playerChat', {
plainMessage: packet.plainMessage,
unsignedContent: packet.unsignedChatContent,
formattedMessage: packet.formattedMessage,
type: packet.type,
sender: packet.senderUuid,
senderName: packet.networkName,
targetName: packet.networkTargetName,
verified
})
// We still accept a message (by pushing to seenMessages) even if the chain is broken. A vanilla client
// will reject a message if the client sets secure chat to be required and the message from the server
// isn't signed, or the client has blocked the sender.
// client1.19.1/client/net/minecraft/client/multiplayer/ClientPacketListener.java#L768
client._lastChatHistory.push({
type: 0, // Player message
previousSignature: packet.previousSignature,
signature: packet.signature,
message: {
plain: packet.plainMessage,
decorated: packet.formattedMessage
},
messageHash: packet.bodyDigest,
timestamp: packet.timestamp,
salt: packet.salt,
lastSeen: packet.previousMessages
})
if (client._lastSeenMessages.push({ sender: packet.senderUuid, signature: packet.signature }) && client._lastSeenMessages.pending++ > 64) {
client.write('message_acknowledgement', {
previousMessages: client._lastSeenMessages.map((e) => ({
messageSender: e.sender,
messageSignature: e.signature
})),
lastRejectedMessage: client._lastRejectedMessage
})
client._lastSeenMessages.pending = 0
}
return
}
// Chain integrity remains even if message is considered unverified due to expiry
const tsDelta = BigInt(Date.now()) - packet.timestamp
const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0
const verified = !packet.unsignedChatContent && updateAndValidateChat(packet.senderUuid, packet.previousSignature, packet.signature, hash.digest()) && !expired
const pubKey = client._players[packet.senderUuid]?.publicKey
client.emit('playerChat', {
plainMessage: packet.plainMessage,
unsignedContent: packet.unsignedChatContent,
formattedMessage: packet.formattedMessage,
formattedMessage: packet.signedChatContent || packet.unsignedChatContent,
type: packet.type,
sender: packet.senderUuid,
senderName: packet.networkName,
targetName: packet.networkTargetName,
verified
senderName: packet.senderName,
senderTeam: packet.senderTeam,
verified: (pubKey && !packet.unsignedChatContent) ? client.verifyMessage(pubKey, packet) : false
})
// We still accept a message (by pushing to seenMessages) even if the chain is broken. A vanilla client
// will reject a message if the client sets secure chat to be required and the message from the server
// isn't signed, or the client has blocked the sender.
// client1.19.1/client/net/minecraft/client/multiplayer/ClientPacketListener.java#L768
client._lastSeenMessages.push({ sender: packet.senderUuid, signature: packet.signature })
client._lastChatHistory.push({
previousSignature: packet.previousSignature,
signature: packet.signature,
message: {
plain: packet.plainMessage,
decorated: packet.formattedMessage
},
messageHash: packet.bodyDigest,
timestamp: packet.timestamp,
salt: packet.salt,
lastSeen: packet.previousMessages
})
if (client._lastSeenMessages.pending++ > 64) {
client.write('message_acknowledgement', {
previousMessages: client._lastSeenMessages.map((e) => ({
messageSender: e.sender,
messageSignature: e.signature
})),
lastRejectedMessage: client._lastRejectedMessage
})
client._lastSeenMessages.pending = 0
}
})
// Chat Sending
@ -175,6 +303,38 @@ module.exports = function (client, options) {
options.timestamp = options.timestamp || BigInt(Date.now())
options.salt = options.salt || 1n
if (mcData.supportFeature('useChatSessions')) {
let acc = 0
const acknowledgements = []
for (let i = 0; i < client._lastSeenMessages.capacity; i++) {
const idx = (client._lastSeenMessages.offset + i) % 20
const message = client._lastSeenMessages[idx]
if (message) {
acc |= 1 << i
acknowledgements.push(message.signature)
message.pending = false
}
}
const bitset = Buffer.allocUnsafe(3)
bitset[0] = acc & 0xFF
bitset[1] = (acc >> 8) & 0xFF
bitset[2] = (acc >> 16) & 0xFF
client.write('chat_message', {
message,
timestamp: options.timestamp,
salt: options.salt,
signature: client.profileKeys ? client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined,
offset: client._lastSeenMessages.pending,
acknowledged: bitset
})
client._lastSeenMessages.pending = 0
return
}
if (options.skipPreview || !client.serverFeatures.chatPreview) {
client.write('chat_message', {
message,
@ -207,10 +367,18 @@ module.exports = function (client, options) {
})
// Signing methods
client.signMessage = (message, timestamp, salt = 0, preview) => {
client.signMessage = (message, timestamp, salt = 0, preview, acknowledgements) => {
if (!client.profileKeys) throw Error("Can't sign message without profile keys, please set valid auth mode")
if (mcData.supportFeature('chainedChatWithHashing')) {
if (mcData.supportFeature('useChatSessions')) {
if (!client._session.uuid) throw Error("Chat session not initialized. Can't send chat")
const length = Buffer.byteLength(message, 'utf8')
const previousMessages = acknowledgements.length > 0 ? ['i32', acknowledgements.length, 'buffer', Buffer.concat(acknowledgements)] : ['i32', 0]
const signable = concat('i32', 1, 'UUID', client.uuid, 'UUID', client._session.uuid, 'i32', client._session.index++, 'i64', salt, 'i64', timestamp / 1000n, 'i32', length, 'pstring', message, ...previousMessages)
return crypto.sign('RSA-SHA256', signable, client.profileKeys.private)
} else if (mcData.supportFeature('chainedChatWithHashing')) {
// 1.19.2
const signer = crypto.createSign('RSA-SHA256')
if (client._lastChatSignature) signer.update(client._lastChatSignature)
@ -290,11 +458,39 @@ module.exports = function (client, options) {
}
}
class SignatureCache extends Array {
capacity = 128
index = 0
push (e) {
if (!e) return
this[this.index++] = e
this.index %= this.capacity
}
}
class LastSeenMessagesWithInvalidation extends Array {
capacity = 20
offset = 0
pending = 0
push (e) {
if (!e) return false
this[this.offset] = { pending: true, signature: e }
this.offset = (this.offset + 1) % this.capacity
this.pending++
return true
}
}
class LastSeenMessages extends Array {
capacity = 5
pending = 0
push (e) {
if (e.signature.length === 0) return // We do not acknowledge unsigned messages
if (!e) return false // We do not acknowledge unsigned messages
// Insert a new entry at the top and shift everything to the right
let last = this[0]
@ -308,5 +504,6 @@ class LastSeenMessages extends Array {
if (!current || (current.sender === e.sender)) break
}
}
return true
}
}

View file

@ -1,5 +1,6 @@
const states = require('../states')
const signedChatPlugin = require('./chat')
const uuid = require('uuid-1345')
module.exports = function (client, options) {
client.serverFeatures = {}
@ -18,6 +19,20 @@ module.exports = function (client, options) {
client.uuid = packet.uuid
client.username = packet.username
if (mcData.supportFeature('useChatSessions') && client.profileKeys) {
client._session = {
index: 0,
uuid: uuid.v4fast()
}
client.write('session', {
sessionUUID: client._session.uuid,
expireTime: client.profileKeys ? BigInt(client.profileKeys.expiresOn.getTime()) : undefined,
publicKey: client.profileKeys ? client.profileKeys.public.export({ type: 'spki', format: 'der' }) : undefined,
signature: client.profileKeys ? client.profileKeys.signatureV2 : undefined
})
}
if (mcData.supportFeature('signedChat')) {
if (options.disableChatSigning && client.serverFeatures.enforcesSecureChat) {
throw new Error('"disableChatSigning" was enabled in client options, but server is enforcing secure chat')

View file

@ -25,10 +25,9 @@ module.exports = function (client, options) {
nextState: 2
})
client.state = states.LOGIN
client.write('login_start', {
username: client.username,
signature: client.profileKeys
signature: (client.profileKeys && !mcData.supportFeature('useChatSessions'))
? {
timestamp: BigInt(client.profileKeys.expiresOn.getTime()), // should probably be called "expireTime"
// Remove padding on the public key: not needed in vanilla server but matches how vanilla client looks

2
src/index.d.ts vendored
View file

@ -36,7 +36,7 @@ declare module 'minecraft-protocol' {
registerChannel(name: string, typeDefinition: any, custom?: boolean): void
unregisterChannel(name: string): void
writeChannel(channel: any, params: any): void
signMessage(message: string, timestamp: BigInt, salt?: number, preview?: string): Buffer
signMessage(message: string, timestamp: BigInt, salt?: number, preview?: string, acknowledgements?: Buffer[]): Buffer
verifyMessage(publicKey: Buffer | KeyObject, packet: object): boolean
reportPlayer(uuid: string, reason: 'FALSE_REPORTING' | 'HATE_SPEECH' | 'TERRORISM_OR_VIOLENT_EXTREMISM' | 'CHILD_SEXUAL_EXPLOITATION_OR_ABUSE' | 'IMMINENT_HARM' | 'NON_CONSENSUAL_INTIMATE_IMAGERY' | 'HARASSMENT_OR_BULLYING' | 'DEFAMATION_IMPERSONATION_FALSE_INFORMATION' | 'SELF_HARM_OR_SUICIDE' | 'ALCOHOL_TOBACCO_DRUGS', signatures: Buffer[], comment?: string): Promise<true>
on(event: 'error', listener: (error: Error) => PromiseLike): this

View file

@ -2,6 +2,7 @@ const crypto = require('crypto')
const concat = require('../transforms/binaryStream').concat
const debug = require('debug')('minecraft-protocol')
const messageExpireTime = 300000 // 5 min (ms)
const { mojangPublicKeyPem } = require('./constants')
class VerificationError extends Error {}
function validateLastMessages (pending, lastSeen, lastRejected) {
@ -39,8 +40,9 @@ function validateLastMessages (pending, lastSeen, lastRejected) {
}
module.exports = function (client, server, options) {
const mojangPubKey = crypto.createPublicKey(mojangPublicKeyPem)
const raise = (translatableError) => client.end(translatableError, JSON.stringify({ translate: translatableError }))
const pending = new Pending()
const pending = client.supportFeature('useChatSessions') ? new LastSeenMessages() : new Pending()
if (!options.generatePreview) options.generatePreview = message => message
@ -57,6 +59,44 @@ module.exports = function (client, server, options) {
}
}
function validateSession (packet) {
try {
const unwrapped = pending.unwrap(packet.offset, packet.acknowledged)
const length = Buffer.byteLength(packet.message, 'utf8')
const acknowledgements = unwrapped.length > 0 ? ['i32', unwrapped.length, 'buffer', Buffer.concat(...unwrapped)] : ['i32', 0]
const signable = concat('i32', 1, 'UUID', client.uuid, 'UUID', client._session.uuid, 'i32', client._session.index++, 'i64', packet.salt, 'i64', packet.timestamp / 1000n, 'i32', length, 'pstring', packet.message, ...acknowledgements)
const valid = crypto.verify('RSA-SHA256', signable, client.profileKeys.public, packet.signature)
if (!valid) throw VerificationError('Invalid or missing message signature')
} catch (e) {
if (e instanceof VerificationError) {
raise('multiplayer.disconnect.chat_validation_failed')
if (!options.hideErrors) console.error(client.address, 'disconnected because', e)
} else {
client.emit('error', e)
}
}
}
client.on('session', (packet) => {
client._session = {
index: 0,
uuid: packet.sessionUuid
}
const publicKey = crypto.createPublicKey({ key: packet.publicKey, format: 'der', type: 'spki' })
const signable = concat('UUID', client.uuid, 'i64', packet.expireTime, 'buffer', publicKey.export({ type: 'spki', format: 'der' }))
// This makes sure 'signable' when signed with the mojang private key equals signature in this packet
if (!crypto.verify('RSA-SHA1', signable, mojangPubKey, packet.signature)) {
debug('Signature mismatch')
raise('multiplayer.disconnect.invalid_public_key_signature')
return
}
client.profileKeys = { public: publicKey }
})
// Listen to chat messages and verify the `lastSeen` and `lastRejected` messages chain
let lastTimestamp
client.on('chat_message', (packet) => {
@ -67,19 +107,30 @@ module.exports = function (client, server, options) {
}
lastTimestamp = packet.timestamp
// Checks here: 1) make sure client can chat, 2) chain is OK, 3) signature is OK, 4) log if expired
// 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
if (!client.verifyMessage(packet)) raise('multiplayer.disconnect.unsigned_chat')
if (client.supportFeature('useChatSessions')) validateSession(packet) // 1.19.3
else if (!client.verifyMessage(packet)) raise('multiplayer.disconnect.unsigned_chat')
if ((BigInt(Date.now()) - packet.timestamp) > messageExpireTime) debug(client.socket.address(), 'sent expired message TS', packet.timestamp)
})
// Client will occasionally send a list of seen messages to the server, here we listen & check chain validity
client.on('message_acknowledgement', validateMessageChain)
client.on('message_acknowledgement', (packet) => {
if (client.supportFeature('useChatSessions')) {
const valid = client._lastSeenMessages.applyOffset(packet.count)
if (!valid) {
raise('multiplayer.disconnect.chat_validation_failed')
if (!options.hideErrors) console.error(client.address, 'disconnected because', VerificationError('Failed to validate message acknowledgements'))
}
} else validateMessageChain(packet)
})
client.verifyMessage = (packet) => {
if (!client.profileKeys) return null
if (client.supportFeature('chainedChatWithHashing')) { // 1.19.1+
if (client.supportFeature('useChatSessions')) throw Error('client.verifyMessage is deprecated. Does not work for 1.19.3 and above')
if (client.supportFeature('chainedChatWithHashing')) { // 1.19.1
if (client._lastChatSignature === packet.signature) return true // Called twice
const verifier = crypto.createVerify('RSA-SHA256')
if (client._lastChatSignature) verifier.update(client._lastChatSignature)
@ -124,6 +175,53 @@ module.exports = function (client, server, options) {
}
}
class LastSeenMessages extends Array {
tracking = 20
constructor () {
super()
for (let i = 0; i < this.tracking; i++) this.push(null)
}
add (sender, signature) {
this.push({ signature, pending: true })
}
applyOffset (offset) {
const diff = this.length - this.tracking
if (offset >= 0 && offset <= diff) {
this.splice(0, offset)
return true
}
return false
}
unwrap (offset, acknowledged) {
if (!this.applyOffset(offset)) throw VerificationError('Failed to validate message acknowledgements')
const n = (acknowledged[2] << 16) | (acknowledged[1] << 8) | acknowledged[0]
const unwrapped = []
for (let i = 0; i < this.tracking; i++) {
const ack = n & (1 << i)
const tracked = this[i]
if (ack) {
if (tracked === null) throw VerificationError('Failed to validate message acknowledgements')
tracked.pending = false
unwrapped.push(tracked.signature)
} else {
if (tracked !== null && !tracked.pending) throw VerificationError('Failed to validate message acknowledgements')
this[i] = null
}
}
return unwrapped
}
}
class Pending extends Array {
m = {}
lastSeen = []

View file

@ -60,7 +60,7 @@ module.exports = function (client, server, options) {
try {
const publicKey = crypto.createPublicKey({ key: packet.signature.publicKey, format: 'der', type: 'spki' })
const signable = mcData.supportFeature('chainedChatWithHashing')
const signable = mcData.supportFeature('profileKeySignatureV2')
? concat('UUID', packet.playerUUID, 'i64', packet.signature.timestamp, 'buffer', publicKey.export({ type: 'spki', format: 'der' }))
: Buffer.from(packet.signature.timestamp + mcPubKeyToPem(packet.signature.publicKey), 'utf8') // (expires at + publicKey)

View file

@ -1,6 +1,6 @@
'use strict'
module.exports = {
defaultVersion: '1.19.2',
supportedVersions: ['1.7', '1.8', '1.9', '1.10', '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']
defaultVersion: '1.19.3',
supportedVersions: ['1.7', '1.8', '1.9', '1.10', '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']
}

View file

@ -105,7 +105,7 @@ for (const supportedVersion of mc.supportedVersions) {
}))
client.on('error', err => done(err))
const lineListener = function (line) {
const match = line.match(/\[Server thread\/INFO\]: <(.+?)> (.+)/)
const match = line.match(/\[Server thread\/INFO\]: (?:\[Not Secure\] )?<(.+?)> (.+)/)
if (!match) return
assert.strictEqual(match[1], 'Player')
assert.strictEqual(match[2], 'hello everyone; I have logged in.')
@ -119,11 +119,62 @@ for (const supportedVersion of mc.supportedVersions) {
client.chat('hello everyone; I have logged in.')
})
// 1.18 and below
client.on('chat', function (packet) {
client.on('playerChat', function (data) {
chatCount += 1
assert.ok(chatCount <= 2)
const message = JSON.parse(packet.message)
if (!mcData.supportFeature('clientsideChatFormatting')) {
const message = JSON.parse(data.formattedMessage)
if (chatCount === 1) {
assert.strictEqual(message.translate, 'chat.type.text')
assert.deepEqual(message.with[0].clickEvent, {
action: 'suggest_command',
value: mcData.version.version > 340 ? '/tell Player ' : '/msg Player '
})
assert.deepEqual(message.with[0].text, 'Player')
assert.strictEqual(message.with[1], 'hello everyone; I have logged in.')
} else if (chatCount === 2) {
assert.strictEqual(message.translate, 'chat.type.announcement')
assert.strictEqual(message.with[0].text ? message.with[0].text : message.with[0], 'Server')
assert.deepEqual(message.with[1].extra
? (message.with[1].extra[0].text
? message.with[1].extra[0].text
: message.with[1].extra[0])
: message.with[1].text, 'hello')
wrap.removeListener('line', lineListener)
client.end()
done()
}
} else {
// 1.19+
const message = JSON.parse(data.formattedMessage || JSON.stringify({ text: data.plainMessage }))
if (chatCount === 1) {
assert.strictEqual(message.text, 'hello everyone; I have logged in.')
const sender = JSON.parse(data.senderName)
assert.deepEqual(sender.clickEvent, {
action: 'suggest_command',
value: '/tell Player '
})
assert.strictEqual(sender.text, 'Player')
} else if (chatCount === 2) {
assert.strictEqual(message.text, 'hello')
const sender = JSON.parse(data.senderName)
assert.strictEqual(sender.text, 'Server')
wrap.removeListener('line', lineListener)
client.end()
done()
}
}
})
client.on('systemChat', function (data) {
// For 1.7.10
chatCount += 1
assert.ok(chatCount <= 2)
const message = JSON.parse(data.formattedMessage)
if (chatCount === 1) {
assert.strictEqual(message.translate, 'chat.type.text')
assert.deepEqual(message.with[0].clickEvent, {
@ -145,22 +196,6 @@ for (const supportedVersion of mc.supportedVersions) {
done()
}
})
// 1.19 and above
let gotClientMessage, gotServerMessage
client.on('player_chat', (packet) => {
const message = JSON.parse(packet.unsignedChatContent || packet.signedChatContent)
// const sender = JSON.parse(packet.senderName)
if (message.text === 'hello everyone; I have logged in.') gotClientMessage = true
if (message.text === 'hello') gotServerMessage = true
if (gotClientMessage && gotServerMessage) {
wrap.removeListener('line', lineListener)
client.end()
done()
}
})
})
it('does not crash for ' + SURVIVE_TIME + 'ms', function (done) {

View file

@ -1,21 +1,20 @@
module.exports = client => {
const mcData = require('minecraft-data')(client.version)
const hasSignedChat = mcData.supportFeature('signedChat')
client.nextMessage = (containing) => {
return new Promise((resolve) => {
function onChat (packet) {
const m = packet.message || packet.unsignedChatContent || packet.signedChatContent
const m = packet.formattedMessage || packet.unsignedChatContent || JSON.stringify({ text: packet.plainMessage })
if (containing) {
if (m.includes(containing)) return finish(m)
else return
}
return finish(m)
}
client.on(hasSignedChat ? 'player_chat' : 'chat', onChat)
client.on('playerChat', onChat)
client.on('systemChat', onChat) // For 1.7.10
function finish (m) {
client.off(hasSignedChat ? 'player_chat' : 'chat', onChat)
client.off('playerChat', onChat)
client.off('systemChat', onChat)
resolve(m)
}
})

View file

@ -44,7 +44,20 @@ const values = {
i8: -10,
u8: 8,
string: 'hi hi this is my client string',
buffer: Buffer.alloc(8),
buffer: function (typeArgs, context) {
let count
if (typeof typeArgs.count === 'number') {
count = typeArgs.count
} else if (typeof typeArgs.count === 'object') {
count = evalCount(typeArgs.count, context)
} else if (typeArgs.count !== undefined) {
count = getField(typeArgs.count, context)
} else if (typeArgs.countType !== undefined) {
count = 8
}
return Buffer.alloc(count)
},
array: function (typeArgs, context) {
let count
if (typeof typeArgs.count === 'number') {
@ -114,6 +127,7 @@ const values = {
test7: { type: 'intArray', value: [12, 42] }
}
},
previousMessages: [],
compressedNbt: {
type: 'compound',
name: 'test',

View file

@ -75,6 +75,7 @@ for (const supportedVersion of mc.supportedVersions) {
function sendBroadcastMessage(server, clients, message, sender) {
if (mcData.supportFeature('signedChat')) {
server.writeToClients(clients, 'player_chat', {
plainMessage: message,
signedChatContent: '',
unsignedChatContent: JSON.stringify({ text: message }),
type: 0,
@ -83,7 +84,10 @@ for (const supportedVersion of mc.supportedVersions) {
senderTeam: undefined,
timestamp: Date.now(),
salt: 0n,
signature: Buffer.alloc(0)
signature: mcData.supportFeature('useChatSessions') ? undefined : Buffer.alloc(0),
previousMessages: [],
filterType: 0,
networkName: JSON.stringify({ text: sender })
})
} else {
server.writeToClients(clients, 'chat', { message: JSON.stringify({ text: message }), position: 0, sender: sender || '0' })
@ -318,6 +322,7 @@ for (const supportedVersion of mc.supportedVersions) {
version: version.minecraftVersion,
port: PORT
}))
player1.on('login', async function (packet) {
assert.strictEqual(packet.gameMode, 1)
const player2 = applyClientHelpers(mc.createClient({
@ -328,6 +333,7 @@ for (const supportedVersion of mc.supportedVersions) {
}))
const p1Join = await player1.nextMessage('player2')
assert.strictEqual(p1Join, '{"text":"player2 joined the game."}')
player2.chat('hi')