Fix chat previews not working (#1054)

* Fix chat previews not working

* Update signMessage to account for chat previews

* Fix lint

* Fix signMessage function

* Server side chat preview verification

* Fix default value overriding provided value

* Only sign previews when they are formatted

* Sign undecorated messages when text is changed

* Update docs
This commit is contained in:
Frej Alexander Nielsen 2023-01-15 18:39:48 +01:00 committed by GitHub
parent 28093fb1fb
commit 2a5ec378c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 24 additions and 5 deletions

View file

@ -32,6 +32,7 @@ automatically logged in and validated against mojang's auth.
* agent : a http agent that can be used to set proxy settings for yggdrasil authentication confirmation (see proxy-agent on npm)
* validateChannelProtocol (optional) : whether or not to enable protocol validation for custom protocols using plugin channels for the connected clients. Defaults to true
* enforceSecureProfile (optional) : Kick clients that do not have chat signing keys from Mojang (1.19+)
* generatePreview (optional) : Function to generate chat previews. Takes the raw message string and should return the message preview as a string. (1.19-1.19.2)
## mc.Server(version,[customPackets])

View file

@ -2,6 +2,20 @@ const crypto = require('crypto')
const concat = require('../transforms/binaryStream').concat
const messageExpireTime = 420000 // 7 minutes (ms)
function isFormatted (message) {
// This should match the ChatComponent.isDecorated function from Vanilla
try {
const comp = JSON.parse(message)
for (const key in comp) {
if (key !== 'text') return true
}
if (comp.text && comp.text !== message) return true
return false
} catch {
return false
}
}
module.exports = function (client, options) {
const mcData = require('minecraft-data')(client.version)
client._players = {}
@ -166,7 +180,7 @@ module.exports = function (client, options) {
message,
timestamp: options.timestamp,
salt: options.salt,
signature: client.profileKeys ? client.signMessage(message, options.timestamp, options.salt) : Buffer.alloc(0),
signature: client.profileKeys ? client.signMessage(message, options.timestamp, options.salt, options.preview) : Buffer.alloc(0),
signedPreview: options.didPreview,
previousMessages: client._lastSeenMessages.map((e) => ({
messageSender: e.sender,
@ -186,14 +200,14 @@ module.exports = function (client, options) {
}
client.on('chat_preview', (packet) => {
if (pendingChatRequest && pendingChatRequest.id === packet.query) {
client._signedChat(packet.message, { ...pendingChatRequest.options, skipPreview: true, didPreview: true })
if (pendingChatRequest && pendingChatRequest.id === packet.queryId) {
client._signedChat(pendingChatRequest.message, { ...pendingChatRequest.options, skipPreview: true, didPreview: true, preview: isFormatted(packet.message) ? packet.message : undefined })
pendingChatRequest = null
}
})
// Signing methods
client.signMessage = (message, timestamp, salt = 0) => {
client.signMessage = (message, timestamp, salt = 0, preview) => {
if (!client.profileKeys) throw Error("Can't sign message without profile keys, please set valid auth mode")
if (mcData.supportFeature('chainedChatWithHashing')) {
@ -209,6 +223,7 @@ module.exports = function (client, options) {
} else {
const hash = crypto.createHash('sha256')
hash.update(concat('i64', salt, 'i64', timestamp / 1000n, 'pstring', message, 'i8', 70))
if (preview) hash.update(preview)
for (const previousMessage of client._lastSeenMessages) {
hash.update(concat('i8', 70, 'UUID', previousMessage.sender))
hash.update(previousMessage.signature)

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): Buffer
signMessage(message: string, timestamp: BigInt, salt?: number, preview?: string): 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

@ -42,6 +42,8 @@ module.exports = function (client, server, options) {
const raise = (translatableError) => client.end(translatableError, JSON.stringify({ translate: translatableError }))
const pending = new Pending()
if (!options.generatePreview) options.generatePreview = message => message
function validateMessageChain (packet) {
try {
validateLastMessages(pending, packet.previousMessages, packet.lastRejectedMessage)
@ -92,6 +94,7 @@ module.exports = function (client, server, options) {
// Player Chat
const hash = crypto.createHash('sha256')
hash.update(concat('i64', packet.salt, 'i64', packet.timestamp / 1000n, 'pstring', packet.message, 'i8', 70))
if (packet.signedPreview) hash.update(options.generatePreview(packet.message))
for (const { messageSender, messageSignature } of packet.previousMessages) {
hash.update(concat('i8', 70, 'UUID', messageSender))
hash.update(messageSignature)