pocket-minecraft-protocol
Version:
Parse and serialize Minecraft Bedrock Edition packets
123 lines (104 loc) • 4.5 kB
JavaScript
const { Ber } = require('asn1')
const { ClientStatus } = require('../connection')
const JWT = require('jsonwebtoken')
const crypto = require('crypto')
const ecPem = require('ec-pem')
const debug = require('debug')('minecraft-protocol')
const SALT = '🧂'
const curve = 'secp384r1'
function Encrypt (client, server, options) {
client.ecdhKeyPair = crypto.createECDH(curve)
client.ecdhKeyPair.generateKeys()
client.clientX509 = writeX509PublicKey(client.ecdhKeyPair.getPublicKey())
function startClientboundEncryption (publicKey) {
debug('[encrypt] Client pub key base64: ', publicKey)
const pubKeyBuf = readX509PublicKey(publicKey.key)
const alice = client.ecdhKeyPair
const alicePEM = ecPem(alice, curve) // https://github.com/nodejs/node/issues/15116#issuecomment-384790125
const alicePEMPrivate = alicePEM.encodePrivateKey()
// Shared secret from the client's public key + our private key
client.sharedSecret = alice.computeSecret(pubKeyBuf)
// Secret hash we use for packet encryption:
// From the public key of the remote and the private key
// of the local, a shared secret is generated using ECDH.
// The secret key bytes are then computed as
// sha256(server_token + shared_secret). These secret key
// bytes are 32 bytes long.
const secretHash = crypto.createHash('sha256')
secretHash.update(SALT)
secretHash.update(client.sharedSecret)
// console.log('[encrypt] Shared secret', client.sharedSecret)
client.secretKeyBytes = secretHash.digest()
// console.log('[encrypt] Shared hash', client.secretKeyBytes)
const x509 = writeX509PublicKey(alice.getPublicKey())
const token = JWT.sign({
salt: toBase64(SALT),
signedToken: alice.getPublicKey('base64')
}, alicePEMPrivate, { algorithm: 'ES384', header: { x5u: x509 } })
client.write('server_to_client_handshake', {
token: token
})
// The encryption scheme is AES/CFB8/NoPadding with the
// secret key being the result of the sha256 above and
// the IV being the first 16 bytes of this secret key.
const initial = client.secretKeyBytes.slice(0, 16)
client.startEncryption(initial)
}
function startServerboundEncryption (token) {
debug('[encrypt] Starting serverbound encryption', token)
const jwt = token?.token
if (!jwt) {
// TODO: allow connecting to servers without encryption
throw Error('Server did not return a valid JWT, cannot start encryption!')
}
// TODO: Should we do some JWT signature validation here? Seems pointless
const alice = client.ecdhKeyPair
const [header, payload] = jwt.split('.').map(k => Buffer.from(k, 'base64'))
const head = JSON.parse(String(header))
const body = JSON.parse(String(payload))
const serverPublicKey = readX509PublicKey(head.x5u)
client.sharedSecret = alice.computeSecret(serverPublicKey)
// console.log('[encrypt] Shared secret', client.sharedSecret)
const salt = Buffer.from(body.salt, 'base64')
const secretHash = crypto.createHash('sha256')
secretHash.update(salt)
secretHash.update(client.sharedSecret)
client.secretKeyBytes = secretHash.digest()
// console.log('[encrypt] Shared hash', client.secretKeyBytes)
const initial = client.secretKeyBytes.slice(0, 16)
client.startEncryption(initial)
// It works! First encrypted packet :)
client.write('client_to_server_handshake', {})
this.emit('join')
client.status = ClientStatus.Initializing
}
client.on('server.client_handshake', startClientboundEncryption)
client.on('client.server_handshake', startServerboundEncryption)
}
function toBase64 (string) {
return Buffer.from(string).toString('base64')
}
function readX509PublicKey (key) {
const reader = new Ber.Reader(Buffer.from(key, 'base64'))
reader.readSequence()
reader.readSequence()
reader.readOID() // Hey, I'm an elliptic curve
reader.readOID() // This contains the curve type, could be useful
return Buffer.from(reader.readString(Ber.BitString, true)).slice(1)
}
function writeX509PublicKey (key) {
const writer = new Ber.Writer()
writer.startSequence()
writer.startSequence()
writer.writeOID('1.2.840.10045.2.1')
writer.writeOID('1.3.132.0.34')
writer.endSequence()
writer.writeBuffer(Buffer.concat([Buffer.from([0x00]), key]), Ber.BitString)
writer.endSequence()
return writer.buffer.toString('base64')
}
module.exports = {
readX509PublicKey,
writeX509PublicKey,
Encrypt
}