Skip to content

Commit 367c015

Browse files
extremeheatrom1504
andauthored
Initial 1.19.1/2 signed chat support (#1050)
* Initial 1.19.1/2 signed chat impl * lint * remove node 15 nullish operators * fix undefined uuid error * handle player left * fix * add some feature flags * fix * Fix test to use new client.chat() wrapper method * refactoring * corrections, working client example * refactoring * message expiry checking * Fix UUID write serialization * Remove padding from client login to match vanilla client * Fix server verification * update packet field terminology * Add some tech docs Rename `map` field in Pending to not conflict with Array.map method * update tech doc * lint * Bump mcdata and pauth * add doc on playerChat event, .chat function * update doc * use supportFeature, update doc Co-authored-by: Romain Beaumont <[email protected]>
1 parent 1efbde1 commit 367c015

16 files changed

+615
-95
lines changed

docs/API.md

+21-2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ Write a packet to all `clients` but encode it only once.
4545

4646
Verifies if player's chat message packet was signed with their Mojang provided key
4747

48+
### client.logSentMessageFromPeer(packet)
49+
(1.19.1+) You must call this function when the server receives a message from a player and that message gets
50+
broadcast to other players in player_chat packets. This function stores these packets so the server can then verify a player's lastSeenMessages field in inbound chat packets to ensure chain integrity. For more information, see [chat.md](chat.md).
51+
4852
### server.onlineModeExceptions
4953

5054
This is a plain old JavaScript object. Add a key with the username you want to
@@ -253,6 +257,18 @@ parameters.
253257

254258
Called when an error occurs within the client. Takes an Error as parameter.
255259

260+
### `playerChat` event
261+
262+
Called when a chat message from another player arrives. The emitted object contains:
263+
* formattedMessage -- the chat message preformatted, if done on server side
264+
* message -- the chat message without formatting (for example no `<username> message` ; instead `message`), on version 1.19+
265+
* type -- the message type - on 1.19, which format string to use to render message ; below, the place where the message is displayed (for example chat or action bar)
266+
* sender -- the UUID of the player sending the message
267+
* senderTeam -- scoreboard team of the player (pre 1.19)
268+
* verified -- true if message is signed, false if not signed, undefined on versions prior to 1.19
269+
270+
See the [chat example](https://github.com/PrismarineJS/node-minecraft-protocol/blob/master/examples/client_chat/client_chat.js#L1) for usage.
271+
256272
### per-packet events
257273

258274
Check out the [minecraft-data docs](https://prismarinejs.github.io/minecraft-data/?v=1.8&d=protocol) to know the event names and data field names.
@@ -273,13 +289,16 @@ Start emitting channel events of the given name on the client object.
273289

274290
Unregister a channel `name` and send the unregister packet if `custom` is true.
275291

292+
### client.chat(message)
293+
Send a chat message to the server, with signing on 1.19+.
294+
276295
### client.signMessage(message: string, timestamp: BigInt, salt?: number) : Buffer
277296

278-
Generate a signature for a chat message to be sent to server
297+
(1.19) Generate a signature for a chat message to be sent to server
279298

280299
### client.verifyMessage(publicKey: Buffer | KeyObject, packet) : boolean
281300

282-
Verifies a player chat packet sent by another player against their public key
301+
(1.19) Verifies a player chat packet sent by another player against their public key
283302

284303
## Not Immediately Obvious Data Type Formats
285304

docs/chat.md

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
## About chat signing
2+
3+
Starting in Minecraft 1.19, client messages sent to the server are signed and then broadcasted to other players.
4+
Other clients receiving a signed message can verify that a message was written by a particular player as opposed
5+
to being modified by the server. The way this is achieved is by the client asking Mojang's servers for signing keys,
6+
and the server responding with a private key that can be used to sign messages, and a public key that can be used to
7+
verify the messages.
8+
9+
When a client connects to the server, it sends its public key to the server, which then sends that to other players
10+
that are on the server. The server also does some checks during the login procedure to authenticate the validity of
11+
the public key, to ensure it came from Mojang. This is achieved by the client sending along a signature from Mojang's
12+
servers in the login step which is the output of concatenating and signing the public key, player UUID and timestamp
13+
with a special Mojang private key specifically for signature validation. The public key used to verify this
14+
signature is public and is stored statically inside node-minecraft-protocol (src/server/constants.js).
15+
16+
Back to the client, when other players join the server they also get a copy of the players' public key for chat verification.
17+
The clients can then verify that a message came from a client as well as do secondary checks like verifying timestamps.
18+
This feature is designed to allow players to report chat messages from other players to Mojang. When the client reports a
19+
message the contents, the sender UUID, timestamp, and signature are all sent so the Mojang server can verify the message
20+
and send it for moderator review.
21+
22+
Note: Since the server sends the public key, it's possible that the server can spoof the key and return a fake one, so
23+
only Mojang can truly know if a message came from a client (as it stores its own copy of the clients' chat key pair).
24+
25+
## 1.19.1
26+
27+
Starting with 1.19.1, instead of signing the message itself, a SHA256 hash of the message and last seen messages are
28+
signed instead. In addition, the payload of the hash is prepended with the signature of the previous message sent by the same client,
29+
creating a signed chain of chat messages. See publicly available documentation for more detailed information on this.
30+
31+
Since chat verification happens on the client-side (as well as server side), all clients need to be kept up to date
32+
on messages from other users. Since not all messages are public (for example, a player may send a signed private message),
33+
the server can send a `chat_header` packet containing the aforementioned SHA256 hash of the message which the client
34+
can generate a signature from, and store as the last signature for that player (maintaining chain integrity).
35+
36+
In the client, inbound player chat history is now stored in chat logs (in a 1000 length array). This allows players
37+
to search through last seen messages when reporting messages.
38+
39+
When reporting chat messages, the chained chat functionality and chat history also securely lets Mojang get
40+
authentic message context before and after a reported message.
41+
42+
## Extra details
43+
44+
### 1.19.1
45+
46+
When a server sends a player a message from another player, the server saves the outbound message and expects
47+
that the client will acknowledge that message, either in a outbound `chat_message` packet's lastSeen field,
48+
or in a `message_acknowledgement` packet. (If the client doesn't seen any chat_message's to the server and
49+
lots of messages pending ACK queue up, a serverbound `message_acknowledgement` packet will be sent to flush the queue.)
50+
51+
In the server, upon reviewal of the ACK, those messages removed from the servers' pending array. If too many
52+
pending messages pile up, the client will get kicked.
53+
54+
In nmp server, you must call `client.logSentMessageFromPeer(packet)` when the server receives a message from a player and that message gets broadcast to other players in player_chat packets. This function stores these packets so the server can then verify a player's lastSeenMessages field in inbound chat packets to ensure chain integrity (as described above).

examples/client_chat/client_chat.js

+6-38
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ const readline = require('readline')
33
const rl = readline.createInterface({
44
input: process.stdin,
55
output: process.stdout,
6-
terminal: false
6+
terminal: false,
7+
prompt: 'Enter a message> '
78
})
89

910
const [,, host, port, username] = process.argv
@@ -37,47 +38,14 @@ client.on('error', function (err) {
3738
})
3839

3940
client.on('connect', () => {
40-
const mcData = require('minecraft-data')(client.version)
4141
const ChatMessage = require('prismarine-chat')(client.version)
42-
const players = {} // 1.19+
4342

4443
console.log('Connected to server')
44+
rl.prompt()
4545

46-
client.chat = (message) => {
47-
if (mcData.supportFeature('signedChat')) {
48-
const timestamp = BigInt(Date.now())
49-
client.write('chat_message', {
50-
message,
51-
timestamp,
52-
salt: 0,
53-
signature: client.signMessage(message, timestamp)
54-
})
55-
} else {
56-
client.write('chat', { message })
57-
}
58-
}
59-
60-
function onChat (packet) {
61-
const message = packet.message || packet.unsignedChatContent || packet.signedChatContent
62-
const j = JSON.parse(message)
63-
const chat = new ChatMessage(j)
64-
65-
if (packet.signature) {
66-
const verified = client.verifyMessage(players[packet.senderUuid].publicKey, packet)
67-
console.info(verified ? 'Verified: ' : 'UNVERIFIED: ', chat.toAnsi())
68-
} else {
69-
console.info(chat.toAnsi())
70-
}
71-
}
72-
73-
client.on('chat', onChat)
74-
client.on('player_chat', onChat)
75-
client.on('player_info', (packet) => {
76-
if (packet.action === 0) { // add player
77-
for (const player of packet.data) {
78-
players[player.UUID] = player.crypto
79-
}
80-
}
46+
client.on('playerChat', function ({ senderName, message, formattedMessage, verified }) {
47+
const chat = new ChatMessage(formattedMessage ? JSON.parse(formattedMessage) : message)
48+
console.log(senderName, { true: 'Verified:', false: 'UNVERIFIED:' }[verified] || '', chat.toAnsi())
8149
})
8250
})
8351

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@
5151
"endian-toggle": "^0.0.0",
5252
"lodash.get": "^4.1.2",
5353
"lodash.merge": "^4.3.0",
54-
"minecraft-data": "^3.8.0",
54+
"minecraft-data": "^3.21.0",
5555
"minecraft-folder-path": "^1.2.0",
5656
"node-fetch": "^2.6.1",
5757
"node-rsa": "^0.4.2",
58-
"prismarine-auth": "^2.0.0",
58+
"prismarine-auth": "^2.2.0",
5959
"prismarine-nbt": "^2.0.0",
6060
"protodef": "^1.8.0",
6161
"readable-stream": "^4.1.0",

0 commit comments

Comments
 (0)