const { PassThrough, Transform } = require('readable-stream') const crypto = require('crypto') const aesjs = require('aes-js') const Zlib = require('zlib') const CIPHER = 'aes-256-cfb8' function createCipher(secret, initialValue) { if (crypto.getCiphers().includes(CIPHER)) { return crypto.createCipheriv(CIPHER, secret, initialValue) } return new Cipher(secret, initialValue) } function createDecipher(secret, initialValue) { if (crypto.getCiphers().includes(CIPHER)) { return crypto.createDecipheriv(CIPHER, secret, initialValue) } return new Decipher(secret, initialValue) } class Cipher extends Transform { constructor(secret, iv) { super() this.aes = new aesjs.ModeOfOperation.cfb(secret, iv, 1) // eslint-disable-line new-cap } _transform(chunk, enc, cb) { try { const res = this.aes.encrypt(chunk) cb(null, res) } catch (e) { cb(e) } } } class Decipher extends Transform { constructor(secret, iv) { super() this.aes = new aesjs.ModeOfOperation.cfb(secret, iv, 1) // eslint-disable-line new-cap } _transform(chunk, enc, cb) { try { const res = this.aes.decrypt(chunk) cb(null, res) } catch (e) { cb(e) } } } function computeCheckSum(packetPlaintext, sendCounter, secretKeyBytes) { let digest = crypto.createHash('sha256'); let counter = Buffer.alloc(8) // writeLI64(sendCounter, counter, 0); counter.writeBigInt64LE(sendCounter, 0) // console.log('Send counter', counter) digest.update(counter); digest.update(packetPlaintext); digest.update(secretKeyBytes); let hash = digest.digest(); // console.log('Hash', hash.toString('hex')) return hash.slice(0, 8); } function createEncryptor(client, iv) { client.cipher = createCipher(client.secretKeyBytes, iv) client.sendCounter = client.sendCounter || 0n // A packet is encrypted via AES256(plaintext + SHA256(send_counter + plaintext + secret_key)[0:8]). // The send counter is represented as a little-endian 64-bit long and incremented after each packet. function process(chunk) { const buffer = Zlib.deflateRawSync(chunk, { level: 7 }) // client.outLog('🟡 Compressed', buffer, client.sendCounter) const packet = Buffer.concat([buffer, computeCheckSum(buffer, client.sendCounter, client.secretKeyBytes)]) client.sendCounter++ // client.outLog('writing to cipher...', packet, client.secretKeyBytes, iv) client.cipher.write(packet) } // const stream = new PassThrough() client.cipher.on('data', client.onEncryptedPacket) return (blob) => { // client.outLog(client.options ? 'C':'S', '🟡 Encrypting', client.sendCounter, blob) // stream.write(blob) process(blob) } } function createDecryptor(client, iv) { client.decipher = createDecipher(client.secretKeyBytes, iv) client.receiveCounter = client.receiveCounter || 0n function verify(chunk) { // console.log('Decryptor: checking checksum', client.receiveCounter, chunk) // client.outLog('🔵 Inflating', chunk) // First try to zlib decompress, then see how much bytes get read const { buffer, engine } = Zlib.inflateRawSync(chunk, { chunkSize: 1024 * 1024 * 2, info: true }) // Holds how much bytes we read, also where the checksum (should) start const inflatedLen = engine.bytesRead // It appears that mc sends extra bytes past the checksum. I don't think this is a raknet // issue (as we are able to decipher properly, zlib works and should also have a checksum) so // there needs to be more investigation done. If you know what's wrong here, please make an issue :) const extraneousLen = chunk.length - inflatedLen - 8 if (extraneousLen > 0) { // Extra bytes // Info for debugging, todo: use debug() const extraneousBytes = chunk.slice(inflatedLen + 8) console.debug('Extraneous bytes!', extraneousLen, extraneousBytes.toString('hex')) } else if (extraneousLen < 0) { // No checksum or decompression failed console.warn('Failed to decrypt', chunk.toString('hex')) throw new Error('Decrypted packet is missing checksum') } const packet = chunk.slice(0, inflatedLen); const checksum = chunk.slice(inflatedLen, inflatedLen + 8); const computedCheckSum = computeCheckSum(packet, client.receiveCounter, client.secretKeyBytes) client.receiveCounter++ if (checksum.toString("hex") == computedCheckSum.toString("hex")) { client.onDecryptedPacket(buffer) } else { console.log('Inflated', inflatedLen, chunk.length, extraneousLen, chunk.toString('hex')) throw Error(`Checksum mismatch ${checksum.toString("hex")} != ${computedCheckSum.toString("hex")}`) } } client.decipher.on('data', verify) return (blob) => { // client.inLog(client.options ? 'C':'S', ' 🔵 Decrypting', client.receiveCounter, blob) // client.inLog('Using shared key', client.secretKeyBytes, iv) client.decipher.write(blob) } } module.exports = { createCipher, createDecipher, createEncryptor, createDecryptor } function testDecrypt() { const client = { secretKeyBytes: Buffer.from('ZOBpyzki/M8UZv5tiBih048eYOBVPkQE3r5Fl0gmUP4=', 'base64'), onDecryptedPacket: (...data) => console.log('Decrypted', data) } const iv = Buffer.from('ZOBpyzki/M8UZv5tiBih0w==', 'base64') const decrypt = createDecryptor(client, iv) console.log('Dec', decrypt(Buffer.from('4B4FCA0C2A4114155D67F8092154AAA5EF', 'hex'))) console.log('Dec 2', decrypt(Buffer.from('DF53B9764DB48252FA1AE3AEE4', 'hex'))) } // testDecrypt()