diff --git a/package.json b/package.json index 589ad5e..1022e71 100644 --- a/package.json +++ b/package.json @@ -23,11 +23,9 @@ "dependencies": { "@azure/msal-node": "^1.0.0-beta.6", "@xboxreplay/xboxlive-auth": "^3.3.3", - "asn1": "^0.2.4", "debug": "^4.3.1", - "ec-pem": "^0.18.0", "jsonwebtoken": "^8.5.1", - "jsp-raknet": "^2.0.0", + "jsp-raknet": "^2.1.0", "minecraft-folder-path": "^1.1.0", "node-fetch": "^2.6.1", "prismarine-nbt": "^1.5.0", diff --git a/src/auth/encryption.js b/src/auth/encryption.js index 95d87b1..2656a99 100644 --- a/src/auth/encryption.js +++ b/src/auth/encryption.js @@ -1,27 +1,26 @@ -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' +const pem = { format: 'pem', type: 'sec1' } +const der = { format: 'der', type: 'spki' } function Encrypt (client, server, options) { - client.ecdhKeyPair = crypto.createECDH(curve) - client.ecdhKeyPair.generateKeys() - client.clientX509 = writeX509PublicKey(client.ecdhKeyPair.getPublicKey()) + client.ecdhKeyPair = crypto.generateKeyPairSync('ec', { namedCurve: curve }) + client.publicKeyDER = client.ecdhKeyPair.publicKey.export(der) + client.privateKeyPEM = client.ecdhKeyPair.privateKey.export(pem) + console.log(client.publicKeyPEM) + client.clientX509 = client.publicKeyDER.toString('base64') 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() + const pubKeyDer = crypto.createPublicKey({ key: Buffer.from(publicKey.key, 'base64'), ...der }) // Shared secret from the client's public key + our private key - client.sharedSecret = alice.computeSecret(pubKeyBuf) + client.sharedSecret = crypto.diffieHellman({ privateKey: client.ecdhKeyPair.privateKey, publicKey: pubKeyDer }) // Secret hash we use for packet encryption: // From the public key of the remote and the private key @@ -36,15 +35,12 @@ function Encrypt (client, server, options) { 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 } }) + signedToken: client.clientX509 + }, client.ecdhKeyPair.privateKey, { algorithm: 'ES384', header: { x5u: client.clientX509 } }) - client.write('server_to_client_handshake', { - token: token - }) + client.write('server_to_client_handshake', { token }) // The encryption scheme is AES/CFB8/NoPadding with the // secret key being the result of the sha256 above and @@ -57,28 +53,27 @@ function Encrypt (client, server, options) { 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 pubKeyDer = crypto.createPublicKey({ key: Buffer.from(head.x5u, 'base64'), ...der }) + // Shared secret from the client's public key + our private key + client.sharedSecret = crypto.diffieHellman({ privateKey: client.ecdhKeyPair.privateKey, publicKey: pubKeyDer }) 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) + const iv = client.secretKeyBytes.slice(0, 16) + client.startEncryption(iv) // It works! First encrypted packet :) client.write('client_to_server_handshake', {}) @@ -94,29 +89,4 @@ 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 -} +module.exports = { Encrypt } diff --git a/src/auth/login.js b/src/auth/login.js index afa74fb..c55665c 100644 --- a/src/auth/login.js +++ b/src/auth/login.js @@ -1,18 +1,15 @@ const fs = require('fs') const JWT = require('jsonwebtoken') const DataProvider = require('../../data/provider') -const ecPem = require('ec-pem') -const curve = 'secp384r1' const { nextUUID } = require('../datatypes/util') +const { PUBLIC_KEY } = require('./constants') +const algorithm = 'ES384' module.exports = (client, server, options) => { const skinGeom = fs.readFileSync(DataProvider(options.protocolVersion).getPath('skin_geom.txt'), 'utf-8') client.createClientChain = (mojangKey, offline) => { - mojangKey = mojangKey || require('./constants').PUBLIC_KEY - const alice = client.ecdhKeyPair - const alicePEM = ecPem(alice, curve) // https://github.com/nodejs/node/issues/15116#issuecomment-384790125 - const alicePEMPrivate = alicePEM.encodePrivateKey() + const privateKey = client.ecdhKeyPair.privateKey let token if (offline) { @@ -25,16 +22,16 @@ module.exports = (client, server, options) => { certificateAuthority: true, identityPublicKey: client.clientX509 } - token = JWT.sign(payload, alicePEMPrivate, { algorithm: 'ES384', notBefore: 0, issuer: 'self', expiresIn: 60 * 60, header: { x5u: client.clientX509 } }) + token = JWT.sign(payload, privateKey, { algorithm, notBefore: 0, issuer: 'self', expiresIn: 60 * 60, header: { x5u: client.clientX509 } }) } else { token = JWT.sign({ - identityPublicKey: mojangKey, + identityPublicKey: mojangKey || PUBLIC_KEY, certificateAuthority: true - }, alicePEMPrivate, { algorithm: 'ES384', header: { x5u: client.clientX509 } }) + }, privateKey, { algorithm, header: { x5u: client.clientX509 } }) } client.clientIdentityChain = token - client.createClientUserChain(alicePEMPrivate) + client.createClientUserChain(privateKey) } client.createClientUserChain = (privateKey) => { @@ -82,7 +79,6 @@ module.exports = (client, server, options) => { const customPayload = options.skinData || {} payload = { ...payload, ...customPayload } - client.clientUserChain = JWT.sign(payload, privateKey, - { algorithm: 'ES384', header: { x5u: client.clientX509 } }) + client.clientUserChain = JWT.sign(payload, privateKey, { algorithm, header: { x5u: client.clientX509 } }) } } diff --git a/src/auth/loginVerify.js b/src/auth/loginVerify.js index 233e729..722dd17 100644 --- a/src/auth/loginVerify.js +++ b/src/auth/loginVerify.js @@ -1,11 +1,14 @@ const JWT = require('jsonwebtoken') const constants = require('./constants') const debug = require('debug')('minecraft-protocol') +const crypto = require('crypto') module.exports = (client, server, options) => { // Refer to the docs: // https://web.archive.org/web/20180917171505if_/https://confluence.yawk.at/display/PEPROTOCOL/Game+Packets#GamePackets-Login + const getDER = b64 => crypto.createPublicKey({ key: Buffer.from(b64, 'base64'), format: 'der', type: 'spki' }) + function verifyAuth (chain) { let data = {} @@ -16,9 +19,9 @@ module.exports = (client, server, options) => { // signed by Mojang by checking the x509 public key in the JWT headers let didVerify = false - let pubKey = mcPubKeyToPem(getX5U(chain[0])) // the first one is client signed, allow it + let pubKey = getDER(getX5U(chain[0])) // the first one is client signed, allow it let finalKey = null - // console.log(pubKey) + for (const token of chain) { const decoded = JWT.verify(token, pubKey, { algorithms: ['ES384'] }) // console.log('Decoded', decoded) @@ -30,7 +33,7 @@ module.exports = (client, server, options) => { debug('Verified client with mojang key', x5u) } - pubKey = decoded.identityPublicKey ? mcPubKeyToPem(decoded.identityPublicKey) : x5u + pubKey = decoded.identityPublicKey ? getDER(decoded.identityPublicKey) : x5u finalKey = decoded.identityPublicKey || finalKey // non pem data = { ...data, ...decoded } } @@ -44,7 +47,7 @@ module.exports = (client, server, options) => { } function verifySkin (publicKey, token) { - const pubKey = mcPubKeyToPem(publicKey) + const pubKey = getDER(publicKey) const decoded = JWT.verify(token, pubKey, { algorithms: ['ES384'] }) return decoded } @@ -71,16 +74,3 @@ function getX5U (token) { const hjson = JSON.parse(hdec) return hjson.x5u } - -function mcPubKeyToPem (mcPubKeyBuffer) { - if (mcPubKeyBuffer[0] === '-') return mcPubKeyBuffer - let pem = '-----BEGIN PUBLIC KEY-----\n' - let base64PubKey = mcPubKeyBuffer.toString('base64') - const maxLineLength = 65 - while (base64PubKey.length > 0) { - pem += base64PubKey.substring(0, maxLineLength) + '\n' - base64PubKey = base64PubKey.substring(maxLineLength) - } - pem += '-----END PUBLIC KEY-----\n' - return pem -}