Start work on multi-version support, test cleanup (#43)

* some cleanup

* start work on multi-version support

* undelete some old examples, can update them later

* move old examples
This commit is contained in:
extremeheat 2021-03-12 14:20:25 -05:00 committed by GitHub
commit df8612e355
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 368 additions and 3517 deletions

View file

@ -52,6 +52,8 @@ function verifyAuth(chain) {
console.log('verified with mojang key!', x5u)
}
// TODO: Handle `didVerify` = false
pubKey = decoded.identityPublicKey ? mcPubKeyToPem(decoded.identityPublicKey) : x5u
finalKey = decoded.identityPublicKey || finalKey // non pem
data = { ...data, ...decoded }
@ -87,22 +89,22 @@ function encodeLoginJWT(localChain, mojangChain) {
module.exports = { encodeLoginJWT, decodeLoginJWT }
function testServer() {
const loginPacket = require('./login.json')
// function testServer() {
// const loginPacket = require('./login.json')
// console.log(loginPacket)
const authChains = JSON.parse(loginPacket.data.chain)
const skinChain = loginPacket.data.clientData
// // console.log(loginPacket)
// const authChains = JSON.parse(loginPacket.data.chain)
// const skinChain = loginPacket.data.clientData
try {
var { data, chain } = decodeLoginJWT(authChains.chain, skinChain)
} catch (e) {
console.error(e)
throw new Error('Failed to verify user')
}
// try {
// var { data, chain } = decodeLoginJWT(authChains.chain, skinChain)
// } catch (e) {
// console.error(e)
// throw new Error('Failed to verify user')
// }
console.log('Authed')
// console.log(loginPacket)
}
// console.log('Authed')
// // console.log(loginPacket)
// }
// testServer()
// // testServer()

View file

@ -2,11 +2,15 @@ const JWT = require('jsonwebtoken')
const crypto = require('crypto')
const { Ber } = require('asn1')
const ec_pem = require('ec-pem')
const fs = require('fs')
const DataProvider = require('../../data/provider')
const SALT = '🧂'
const curve = 'secp384r1'
function Encrypt(client, server, options) {
const skinGeom = fs.readFileSync(DataProvider(options.protocolVersion).getPath('skin_geom.txt'), 'utf-8')
client.ecdhKeyPair = crypto.createECDH(curve)
client.ecdhKeyPair.generateKeys()
client.clientX509 = writeX509PublicKey(client.ecdhKeyPair.getPublicKey())
@ -84,7 +88,6 @@ function Encrypt(client, server, options) {
}
client.on('server.client_handshake', startClientboundEncryption)
client.on('client.server_handshake', startServerboundEncryption)
client.createClientChain = (mojangKey) => {
@ -118,7 +121,7 @@ function Encrypt(client, server, options) {
SkinId: '5eb65f73-af11-448e-82aa-1b7b165316ad.persona-e199672a8c1a87e0-0',
SkinData: 'AAAAAA==',
SkinResourcePatch: 'ewogICAiZ2VvbWV0cnkiIDogewogICAgICAiYW5pbWF0ZWRfMTI4eDEyOCIgOiAiZ2VvbWV0cnkuYW5pbWF0ZWRfMTI4eDEyOF9wZXJzb25hLWUxOTk2NzJhOGMxYTg3ZTAtMCIsCiAgICAgICJhbmltYXRlZF9mYWNlIiA6ICJnZW9tZXRyeS5hbmltYXRlZF9mYWNlX3BlcnNvbmEtZTE5OTY3MmE4YzFhODdlMC0wIiwKICAgICAgImRlZmF1bHQiIDogImdlb21ldHJ5LnBlcnNvbmFfZTE5OTY3MmE4YzFhODdlMC0wIgogICB9Cn0K',
SkinGeometryData: require('./geom'),
SkinGeometryData: skinGeom,
"SkinImageHeight": 1,
"SkinImageWidth": 1,
"ArmSize": "wide",

View file

@ -1,232 +0,0 @@
const crypto = require('crypto')
const JWT = require('jsonwebtoken')
const constants = require('./constants')
const { Ber } = require('asn1')
const ec_pem = require('ec-pem');
// function Encrypt(client, options) {
// this.startClientboundEncryption = (pubKeyBuf) => {
// }
// client.on('start_encrypt', this.startClientboundEncryption)
// }
// module.exports = Encrypt
// Server -> Client : sent right after the client sends us a LOGIN_PACKET so
// we can start the encryption process
// @param {key} - The key from the client Login Packet final JWT chain
function startClientboundEncryption(pubKeyBuf) {
// create our ecdh keypair
const type = 'secp256k1'
const alice = crypto.createECDH(type)
const aliceKey = alice.generateKeys()
const alicePublicKey = aliceKey.toString('base64')
const alicePrivateKey = mcPubKeyToPem(alice.getPrivateKey('base64'))
// get our secret key hex encoded
// const aliceSecret = alice.computeSecret(pubKeyBuf, null, 'hex')
// (yawkat:)
// 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 salt = Buffer.from('', 'utf-8')
let secret = crypto.createHash('sha256').update(Buffer.concat([salt, pubKeyBuf])).digest()
console.log('alice', alicePrivateKey)
const pem = mcPubKeyToPem(alice.getPrivateKey().toString('base64'))
console.log('pem', pem)
const token = JWT.sign({
salt,
signedToken: alicePublicKey
}, pem, { algorithm: 'ES384' })
console.log('Token', token)
// get our Secret Bytes from the secret key
// alice.setPrivateKey(
// crypto.createHash('sha256').update('alice', 'utf8').digest()
// )
// using (var sha = SHA256.Create())
// {
// secret = sha.ComputeHash(secretPrepend.Concat(agreement.CalculateAgreement(remotePublicKey).ToByteArrayUnsigned()).ToArray());
// }
const bob = crypto.createECDH('secp256k1');
// URI x5u = URI.create(Base64.getEncoder().encodeToString(serverKeyPair.getPublic().getEncoded()));
// JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().claim("salt", Base64.getEncoder().encodeToString(token)).build();
// SignedJWT jwt = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.ES384).x509CertURL(x5u).build(), claimsSet);
// signJwt(jwt, (ECPrivateKey) serverKeyPair.getPrivate());
// return jwt;
}
function testECDH() {
const crypto = require('crypto')
const alice = crypto.createECDH('secp256k1')
const bob = crypto.createECDH('secp256k1')
// Note: This is a shortcut way to specify one of Alice's previous private
// keys. It would be unwise to use such a predictable private key in a real
// application.
alice.setPrivateKey(
crypto.createHash('sha256').update('alice', 'utf8').digest()
);
// Bob uses a newly generated cryptographically strong
// pseudorandom key pair bob.generateKeys();
const alice_secret = alice.computeSecret(bob.getPublicKey(), null, 'hex')
const bob_secret = bob.computeSecret(alice.getPublicKey(), null, 'hex')
// alice_secret and bob_secret should be the same shared secret value
console.log(alice_secret === bob_secret)
}
function testECDH2() {
const type = 'secp256k1'
const alice = crypto.createECDH(type);
const aliceKey = alice.generateKeys();
// Generate Bob's keys...
const bob = crypto.createECDH(type);
const bobKey = bob.generateKeys();
console.log("\nAlice private key:\t", alice.getPrivateKey().toString('hex'));
console.log("Alice public key:\t", aliceKey.toString('hex'))
console.log("\nBob private key:\t", bob.getPrivateKey().toString('hex'));
console.log("Bob public key:\t", bobKey.toString('hex'));
// Exchange and generate the secret...
const aliceSecret = alice.computeSecret(bobKey);
const bobSecret = bob.computeSecret(aliceKey);
console.log("\nAlice shared key:\t", aliceSecret.toString('hex'))
console.log("Bob shared key:\t\t", bobSecret.toString('hex'));
//wow it actually works?!
}
function mcPubKeyToPem(mcPubKeyBuffer) {
console.log(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
}
function readX509PublicKey(key) {
var 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 testMC() {
// const pubKeyBuf = Buffer.from(constants.PUBLIC_KEY, 'base64')
// const pem = mcPubKeyToPem(pubKeyBuf)
// console.log(mcPubKeyToPem(pubKeyBuf))
// const publicKey = crypto.createPublicKey({ key: pem, format: 'der' })
const pubKeyBuf = readX509PublicKey(constants.PUBLIC_KEY)
// console.log('Mojang pub key', pubKeyBuf.toString('hex'), publicKey)
startClientboundEncryption(pubKeyBuf)
}
function testMC2() {
// const mojangPubKeyBuf = Buffer.from('MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8ELkixyLcwlZryUQcu1TvPOmI2B7vX83ndnWRUaXm74wFfa5f/lwQNTfrLVHa2PmenpGI6JhIMUJaWZrjmMj90NoKNFSNBuKdm8rYiXsfaz3K36x/1U26HpG0ZxK/V1V', 'base64')
// const pem = mcPubKeyToPem(mojangPubKeyBuf)
// const publicKey = crypto.createPublicKey({ key: pem })
const publicKey = readX509PublicKey(constants.PUBLIC_KEY)
const curve = 'secp384r1'
const alice = crypto.createECDH(curve)
// const keys = crypto.generateKeyPair('ec',)
// const bob = crypto.generateKeyPairSync('ec', {
// namedCurve: type
// })
// alice.setPrivateKey(bob.privateKey.export({ type: 'pkcs8', format: 'pem' }))
// alice.setPublicKey(bob.publicKey.export({ type: 'spki', format: 'pem' }))
// console.log(bob)
const aliceKey = alice.generateKeys()
const alicePEM = ec_pem(alice, curve)
const alicePEMPrivate = alicePEM.encodePrivateKey()
const alicePEMPublic = alicePEM.encodePublicKey()
// const alicePublicKey = aliceKey.toString('base64')
// const alicePrivateKey = alice.getPrivateKey().toString('base64')
const aliceSecret = alice.computeSecret(publicKey, null, 'hex')
console.log('Alice private key PEM', alicePEMPrivate)
console.log('Alice public key PEM', alicePEMPublic)
console.log('Alice public key', alice.getPublicKey('base64'))
console.log('Alice secret key', aliceSecret)
var sign = crypto.createSign('RSA-SHA256')
sign.write('something')
sign.end()
// // const pem2 =
// // `-----BEGIN PRIVATE KEY-----
// // ${alice.getPrivateKey('base64')}
// // -----END PRIVATE KEY-----`
// console.log('PEM', bob.privateKey)
const sig = sign.sign(alicePEMPrivate, 'hex')
console.log('Signature', sig)
const token = JWT.sign({
salt: 'HELLO',
signedToken: alice.getPublicKey('base64')
}, alicePEMPrivate, { algorithm: 'ES384' })
console.log('Token', token)
const verified = JWT.verify(token, alicePEMPublic, { algorithms: 'ES384' })
console.log('Verified!', verified)
}
function testMC3() {
var Ber = require('asn1').Ber;
var reader = new Ber.Reader(new Buffer(constants.PUBLIC_KEY, "base64"));
reader.readSequence();
reader.readSequence();
reader.readOID(); // Hey, I'm an elliptic curve
reader.readOID(); // This contains the curve type, could be useful
var pubKey = reader.readString(Ber.BitString, true).slice(1);
var server = crypto.createECDH('secp384r1');
server.generateKeys();
console.log(server.computeSecret(pubKey));
}
// testECDH2()
testMC2()

View file

@ -1,88 +0,0 @@
const crypto = require('crypto')
const JWT = require('jsonwebtoken')
const constants = require('./constants')
const { Ber } = require('asn1')
const ec_pem = require('ec-pem')
function readX509PublicKey(key) {
var 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) {
var 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");
}
function test(pubKey = constants.PUBLIC_KEY) {
const publicKey = readX509PublicKey(pubKey)
const curve = 'secp384r1'
const alice = crypto.createECDH(curve)
const aliceKey = alice.generateKeys()
const alicePEM = ec_pem(alice, curve)
const alicePEMPrivate = alicePEM.encodePrivateKey()
const alicePEMPublic = alicePEM.encodePublicKey()
const aliceSecret = alice.computeSecret(publicKey, null, 'hex')
console.log('Alice private key PEM', alicePEMPrivate)
console.log('Alice public key PEM', alicePEMPublic)
console.log('Alice public key', alice.getPublicKey('hex'))
console.log('Alice secret key', aliceSecret)
// Test signing manually
const sign = crypto.createSign('RSA-SHA256')
sign.write('🧂')
sign.end()
const sig = sign.sign(alicePEMPrivate, 'hex')
console.log('Signature', sig)
// Test JWT sign+verify
const x509 = writeX509PublicKey(alice.getPublicKey())
const token = JWT.sign({
salt: 'HELLO',
signedToken: alice.getPublicKey('base64')
}, alicePEMPrivate, { algorithm: 'ES384', header: { x5u: x509 } })
console.log('Encoded JWT', token)
// send the jwt to the client...
const verified = JWT.verify(token, alicePEMPublic, { algorithms: 'ES384' })
console.log('Decoded JWT', verified)
// Good
}
/**
* Alice private key PEM -----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDBGgHZwH3BzieyJrdrVTVLmrEoUxpDUSqSzS98lobTXeUxJR/OmywPV
57I8YtnsJlCgBwYFK4EEACKhZANiAATjvTRgjsxKruO7XbduSQoHeR/6ouIm4Rmc
La9EkSpLFpuYZfsdtq9Vcf2t3Q3+jIbXjD/wNo97P4Hr5ghXG8sCVV7jpqadOF8j
SzyfajLGfX9mkS5WWLAg+dpi/KiEo/g=
-----END EC PRIVATE KEY-----
Alice public key PEM -----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4700YI7MSq7ju123bkkKB3kf+qLiJuEZ
nC2vRJEqSxabmGX7HbavVXH9rd0N/oyG14w/8DaPez+B6+YIVxvLAlVe46amnThf
I0s8n2oyxn1/ZpEuVliwIPnaYvyohKP4
-----END PUBLIC KEY-----
Alice public key 04e3bd34608ecc4aaee3bb5db76e490a07791ffaa2e226e1199c2daf44912a4b169b9865fb1db6af5571fdaddd0dfe8c86d78c3ff0368f7b3f81ebe608571bcb02555ee3a6a69d385f234b3c9f6a32c67d7f66912e5658b020f9da62fca884a3f8
Alice secret key 76feb5d420b33907c4841a74baa707b717a29c021b17b6662fd46dba3227cac3e256eee9e890edb0308f66a3119b4914
Signature 3066023100d5ea70b8fc5e441c5e93d9f7dcde031f54291011c950a4aa8625ea9b27f7c798a8bc4de40baf35d487a05db6b5c628c6023100ae06cc2ea65db77138163c546ccf13933faae3d91bd6aa7108b99539cdb1c86f1e8a3704cb099f0b00eebed4ee75ccb2
Encoded JWT eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzYWx0IjoiSEVMTE8iLCJzaWduZWRUb2tlbiI6IkJPTzlOR0NPekVxdTQ3dGR0MjVKQ2dkNUgvcWk0aWJoR1p3dHIwU1JLa3NXbTVobCt4MjJyMVZ4L2EzZERmNk1odGVNUC9BMmozcy9nZXZtQ0ZjYnl3SlZYdU9tcHAwNFh5TkxQSjlxTXNaOWYyYVJMbFpZc0NENTJtTDhxSVNqK0E9PSIsImlhdCI6MTYxMTc4MDYwNX0._g8k086U7nD-Tthn8jGWuuM3Q4EfhgqCfFA1Q5ePmjqhqMHOJvmrCz6tWsCytr2i-a2M51fb9K_YDAHbZ66Kos9ZkjF4Tqz5fPS880fM9woZ_1xjh7nGcOQ6sbY81zyi
Decoded JWT {
salt: 'HELLO',
signedToken: 'BOO9NGCOzEqu47tdt25JCgd5H/qi4ibhGZwtr0SRKksWm5hl+x22r1Vx/a3dDf6MhteMP/A2j3s/gevmCFcbywJVXuOmpp04XyNLPJ9qMsZ9f2aRLlZYsCD52mL8qISj+A==',
iat: 1611780605
}
*/
test()

View file

@ -1,49 +0,0 @@
function test() {
const chain = require('./sampleChain.json').chain
let data = {}
// There are three JWT tokens sent to us, one signed by the client
// one signed by Mojang with the Mojang token we have and another one
// from Xbox with addition user profile data
// We verify that at least one of the tokens in the chain has been properly
// signed by Mojang by checking the x509 public key in the JWT headers
let didVerify = false
let pubKey = mcPubKeyToPem(constants.PUBLIC_KEY_NEW)
console.log(pubKey)
for (var token of chain) {
// const decoded = jwt.decode(token, pubKey, 'ES384')
console.log('Decoding...', token)
const decoded = JWT.verify(token, pubKey, { algorithms: 'ES384' })
console.log('Decoded...')
console.log('Decoded', decoded)
// Check if signed by Mojang key
const [header] = token.split('.')
const hdec = Buffer.from(header, 'base64').toString('utf-8')
const hjson = JSON.parse(hdec)
if (hjson.x5u == constants.PUBLIC_KEY && !data.extraData?.XUID) {
didVerify = true
console.log('verified with mojang key!', hjson.x5u)
}
pubKey = mcPubKeyToPem(decoded.identityPublicKey)
data = { ...data, ...decoded }
}
console.log('Result', data)
}
function test2() {
const chain = require('./login.json')
const token = chain.data.clientData
// console.log(token)
const pubKey = mcPubKeyToPem(constants.CDATA_PUBLIC_KEY)
const decoded = JWT.verify(token, pubKey, { algorithms: 'ES384' })
// console.log('Decoded', decoded)
fs.writeFileSync('clientData.json', JSON.stringify(decoded))
}

View file

@ -1,22 +1,25 @@
const fs = require('fs')
const debug = require('debug')('minecraft-protocol')
const auth = require('./client/auth')
const Options = require('./options')
const { Connection } = require('./connection')
const { createDeserializer, createSerializer } = require('./transforms/serializer')
const { Encrypt } = require('./auth/encryption')
const auth = require('./client/auth')
const Options = require('./options')
const { RakClient } = require('./Rak')
const { serialize } = require('./datatypes/util')
const debugging = false
class Client extends Connection {
/** @param {{ version: number, hostname: string, port: number }} options */
constructor(options) {
super()
this.options = { ...Options.defaultOptions, ...options }
this.serializer = createSerializer()
this.deserializer = createDeserializer()
this.validateOptions()
this.serializer = createSerializer(this.options.version)
this.deserializer = createDeserializer(this.options.version)
Encrypt(this, null, options)
Encrypt(this, null, this.options)
if (options.password) {
auth.authenticatePassword(this, options)
@ -28,26 +31,29 @@ class Client extends Connection {
this.startQueue()
this.inLog = (...args) => console.info('C ->', ...args)
this.outLog = (...args) => console.info('C <-', ...args)
// this.on('decrypted', this.onDecryptedPacket)
}
validateOptions() {
// console.log('Options', this.options)
if (!this.options.hostname || this.options.port == null) throw Error('Invalid hostname/port')
if (this.options.version < Options.MIN_VERSION) {
throw new Error(`Unsupported protocol version < ${Options.MIN_VERSION} : ${this.options.version}`)
if (!Options.Versions[this.options.version]) {
console.warn('Supported versions: ', Options.Versions)
throw Error(`Unsupported version ${this.options.version}`)
}
this.options.protocolVersion = Options.Versions[this.options.version]
if (this.options.protocolVersion < Options.MIN_VERSION) {
throw new Error(`Protocol version < ${Options.MIN_VERSION} : ${this.options.protocolVersion}, too old`)
}
}
onEncapsulated = (encapsulated, inetAddr) => {
// log(inetAddr.address, ': Encapsulated', encapsulated)
const buffer = Buffer.from(encapsulated.buffer)
this.handle(buffer)
}
connect = async (sessionData) => {
const hostname = this.options.hostname || '127.0.0.1'
const port = this.options.port || 19132
const hostname = this.options.hostname
const port = this.options.port
this.connection = new RakClient({ useWorkers: true, hostname, port })
this.connection.onConnected = () => this.sendLogin()
@ -64,14 +70,13 @@ class Client extends Connection {
]
const encodedChain = JSON.stringify({ chain })
// const skinChain = JSON.stringify({})
const bodyLength = this.clientUserChain.length + encodedChain.length + 8
debug('Auth chain', chain)
this.write('login', {
protocol_version: this.options.version,
protocol_version: this.options.protocolVersion,
payload_size: bodyLength,
chain: encodedChain,
client_data: this.clientUserChain
@ -83,7 +88,7 @@ class Client extends Connection {
// We're talking over UDP, so there is no connection to close, instead
// we stop communicating with the server
console.warn(`Server requested ${packet.hide_disconnect_reason ? 'silent disconnect' : 'disconnect'}: ${packet.message}`)
process.exit(1)
process.exit(1) // TODO: handle
}
close() {
@ -109,32 +114,28 @@ class Client extends Connection {
}
readPacket(packet) {
// console.log('packet', packet)
const des = this.deserializer.parsePacketBuffer(packet)
const pakData = { name: des.data.name, params: des.data.params }
this.inLog('-> C', pakData.name/*, serialize(pakData.params).slice(0, 100)*/)
// No idea what this exotic 0xA0 packet is, it's not implemented anywhere
// and seems empty. Possible gibberish from the raknet impl
if (pakData.name == '160' || !pakData.name) { // eslint-ignore-line
console.warn('?? Ignoring extraneous packet ', des)
return
}
// Packet verifying (decode + re-encode + match test)
if (pakData.name) {
this.tryRencode(pakData.name, pakData.params, packet)
}
// console.info('->', JSON.stringify(pakData, (k,v) => typeof v == 'bigint' ? v.toString() : v))
// Packet dumping
try {
if (!fs.existsSync(`./packets/${pakData.name}.json`)) {
fs.writeFileSync(`./packets/${pakData.name}.json`, serialize(pakData.params, 2))
fs.writeFileSync(`./packets/${pakData.name}.txt`, packet.toString('hex'))
if (debugging) {
// Packet verifying (decode + re-encode + match test)
if (pakData.name) {
this.tryRencode(pakData.name, pakData.params, packet)
}
} catch { }
// console.info('->', JSON.stringify(pakData, (k,v) => typeof v == 'bigint' ? v.toString() : v))
// Packet dumping
try {
const root = __dirname + `../data/${this.options.version}/sample/`
if (!fs.existsSync(root + `packets/${pakData.name}.json`)) {
fs.writeFileSync(root + `packets/${pakData.name}.json`, serialize(pakData.params, 2))
fs.writeFileSync(root + `packets/${pakData.name}.txt`, packet.toString('hex'))
}
} catch { }
}
// Abstract some boilerplate before sending to listeners
switch (des.data.name) {
case 'server_to_client_handshake':
this.emit('client.server_handshake', des.data.params)
@ -142,27 +143,22 @@ class Client extends Connection {
case 'disconnect': // Client kicked
this.onDisconnectRequest(des.data.params)
break
case 'crafting_data':
fs.writeFileSync('crafting.json', JSON.stringify(des.data.params, (k, v) => typeof v == 'bigint' ? v.toString() : v))
break
case 'start_game':
fs.writeFileSync('start_game.json', JSON.stringify(des.data.params, (k, v) => typeof v == 'bigint' ? v.toString() : v))
break
case 'level_chunk':
// fs.writeFileSync(`./chunks/chunk-${chunks++}.txt`, packet.toString('hex'))
break
// case 'crafting_data':
// fs.writeFileSync('crafting.json', JSON.stringify(des.data.params, (k, v) => typeof v == 'bigint' ? v.toString() : v))
// break
// case 'start_game':
// fs.writeFileSync('start_game.json', JSON.stringify(des.data.params, (k, v) => typeof v == 'bigint' ? v.toString() : v))
// break
// case 'level_chunk':
// // fs.writeFileSync(`./chunks/chunk-${chunks++}.txt`, packet.toString('hex'))
// break
default:
// console.log('Sending to listeners')
}
this.emit(des.data.name, des.data.params)
// Emit packet
this.emit(des.data.name, des.data.params)
}
}
var chunks = 0;
function serialize(obj = {}, fmt) {
return JSON.stringify(obj, (k, v) => typeof v == 'bigint' ? v.toString() : v, fmt)
}
module.exports = { Client }

View file

@ -1,16 +1,5 @@
const XboxLiveAuth = require('@xboxreplay/xboxlive-auth')
const debug = require('debug')('minecraft-protocol')
const fetch = require('node-fetch')
const authConstants = require('./authConstants')
const { MsAuthFlow } = require('./authFlow.js')
const getFetchOptions = {
headers: {
'Content-Type': 'application/json',
'User-Agent': 'node-minecraft-protocol'
}
}
/**
* Obtains Minecaft profile data using a Minecraft access token and starts the join sequence
* @param {object} client - The client passed to protocol
@ -71,33 +60,25 @@ async function authenticateDeviceCode (client, options) {
}
}
function checkStatus (res) {
if (res.ok) { // res.status >= 200 && res.status < 300
return res.json()
} else {
throw Error(res.statusText)
}
}
module.exports = {
authenticatePassword,
authenticateDeviceCode
}
async function msaTest () {
// MsAuthFlow.resetTokenCaches()
// async function msaTest () {
// // MsAuthFlow.resetTokenCaches()
await authenticateDeviceCode({
connect(...args) {
console.log('Connecting', args)
},
emit(...e) {
console.log('Event', e)
}
}, {})
}
// await authenticateDeviceCode({
// connect(...args) {
// console.log('Connecting', args)
// },
// emit(...e) {
// console.log('Event', e)
// }
// }, {})
// }
// debug with node microsoftAuth.js
if (!module.parent) {
msaTest()
}
// // debug with node microsoftAuth.js
// if (!module.parent) {
// msaTest()
// }

View file

@ -2,11 +2,26 @@ const BinaryStream = require('@jsprismarine/jsbinaryutils').default
const BatchPacket = require('./datatypes/BatchPacket')
const cipher = require('./transforms/encryption')
const { EventEmitter } = require('events')
const Reliability = require('jsp-raknet/protocol/reliability')
const Versions = require('./options')
const debug = require('debug')('minecraft-protocol')
class Connection extends EventEmitter {
versionLessThan(version) {
if (typeof version === 'string') {
return Versions[version] < this.options.version
} else {
return version < this.options.version
}
}
versionGreaterThan(version) {
if (typeof version === 'string') {
return Versions[version] > this.options.version
} else {
return version > this.options.version
}
}
startEncryption(iv) {
this.encryptionEnabled = true
this.inLog('Started encryption', this.sharedSecret, iv)
@ -15,14 +30,10 @@ class Connection extends EventEmitter {
this.q2 = []
}
write(name, params) { // TODO: Batch
// console.log('Need to encode', name, params)
var s = this.connect ? 'C' : 'S'
if (this.downQ) s += 'P'
this.outLog('NB <- ' + s, name,params)
write(name, params) {
this.outLog('sending', name, params)
const batch = new BatchPacket()
const packet = this.serializer.createPacketBuffer({ name, params })
// console.log('Sending buf', packet.toString('hex').)
batch.addEncodedPacket(packet)
if (this.encryptionEnabled) {
@ -51,8 +62,6 @@ class Connection extends EventEmitter {
//TODO: can we just build Batch before the queue loop?
const batch = new BatchPacket()
this.outLog('<- BATCH', this.q2)
// For now, we're over conservative so send max 3 packets
// per batch and hold the rest for the next tick
const sending = []
for (let i = 0; i < this.q.length; i++) {
const packet = this.q.shift()
@ -65,28 +74,10 @@ class Connection extends EventEmitter {
} else {
this.sendDecryptedBatch(batch)
}
// this.q2 = []
}
}, 20)
}
writeRaw(name, buffer) { // skip protodef serializaion
// temporary hard coded stuff
const batch = new BatchPacket()
if (name == 'biome_definition_list') {
// so we can send nbt straight from file without parsing
const stream = new BinaryStream()
stream.writeUnsignedVarInt(0x7a)
stream.append(buffer)
batch.addEncodedPacket(stream.getBuffer())
}
if (this.encryptionEnabled) {
this.sendEncryptedBatch(batch)
} else {
this.sendDecryptedBatch(batch)
}
}
/**
* Sends a MCPE packet buffer
@ -121,21 +112,11 @@ class Connection extends EventEmitter {
// TODO: Rename this to sendEncapsulated
sendMCPE(buffer, immediate) {
this.connection.sendReliable(buffer, immediate)
// if (this.worker) {
// this.outLog('-> buf', buffer)
// this.worker.postMessage({ type: 'queueEncapsulated', packet: buffer, immediate })
// } else {
// const sendPacket = new EncapsulatedPacket()
// sendPacket.reliability = Reliability.ReliableOrdered
// sendPacket.buffer = buffer
// this.connection.addEncapsulatedToQueue(sendPacket)
// if (immediate) this.connection.sendQueue()
// }
}
// These are callbacks called from encryption.js
onEncryptedPacket = (buf) => {
this.outLog('ENC BUF', buf)
this.outLog('Enc buf', buf)
const packet = Buffer.concat([Buffer.from([0xfe]), buf]) // add header
this.outLog('Sending wrapped encrypted batch', packet)
@ -143,8 +124,6 @@ class Connection extends EventEmitter {
}
onDecryptedPacket = (buf) => {
// console.log('🟢 Decrypted', buf)
const stream = new BinaryStream(buf)
const packets = BatchPacket.getPackets(stream)
@ -168,10 +147,7 @@ class Connection extends EventEmitter {
}
}
}
// console.log('[client] handled incoming ', buffer)
}
}
function serialize(obj = {}, fmt) {
return JSON.stringify(obj, (k, v) => typeof v == 'bigint' ? v.toString() : v, fmt)
}
module.exports = { Connection }

View file

@ -1,12 +0,0 @@
module.exports = {
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
},
waitFor(cb, withTimeout) {
return Promise.race([
new Promise((res, rej) => cb(res)),
sleep(withTimeout)
])
}
}

37
src/datatypes/util.js Normal file
View file

@ -0,0 +1,37 @@
const fs = require('fs');
function getFiles(dir) {
var results = [];
var list = fs.readdirSync(dir);
list.forEach(function (file) {
file = dir + '/' + file;
var stat = fs.statSync(file);
if (stat && stat.isDirectory()) {
/* Recurse into a subdirectory */
results = results.concat(getFiles(file));
} else {
/* Is a file */
results.push(file);
}
});
return results;
}
module.exports = {
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
},
waitFor(cb, withTimeout) {
return Promise.race([
new Promise((res, rej) => cb(res)),
sleep(withTimeout)
])
},
serialize(obj = {}, fmt) {
return JSON.stringify(obj, (k, v) => typeof v == 'bigint' ? v.toString() : v, fmt)
},
getFiles
}

View file

@ -1,12 +1,16 @@
// Minimum supported version (< will be kicked)
const MIN_VERSION = 422
const MIN_VERSION = '1.16.201'
// Currently supported verson
const CURRENT_VERSION = 422
const CURRENT_VERSION = '1.16.201'
const defaultOptions = {
// https://minecraft.gamepedia.com/Protocol_version#Bedrock_Edition_2
version: CURRENT_VERSION
}
module.exports = { defaultOptions, MIN_VERSION, CURRENT_VERSION }
const Versions = {
'1.16.210': 428,
'1.16.201': 422
}
module.exports = { defaultOptions, MIN_VERSION, CURRENT_VERSION, Versions }

View file

@ -3,6 +3,7 @@ const Listener = require('jsp-raknet/listener')
const EncapsulatedPacket = require('jsp-raknet/protocol/encapsulated_packet')
const RakClient = require('jsp-raknet/client')
const ConnWorker = require('./rakWorker')
const { waitFor } = require('./datatypes/util')
try {
var { Client, Server, PacketPriority, PacketReliability, McPingMessage } = require('raknet-native')
} catch (e) {
@ -12,14 +13,14 @@ try {
class RakNativeClient extends EventEmitter {
constructor(options) {
super()
this.onConnected = () => {}
this.onCloseConnection = () => {}
this.onEncapsulated = () => {}
this.onConnected = () => { }
this.onCloseConnection = () => { }
this.onEncapsulated = () => { }
this.raknet = new Client(options.hostname, options.port, 'minecraft')
this.raknet.on('encapsulated', thingy => {
// console.log('Encap',thingy)
const { buffer, address, guid }=thingy
const { buffer, address, guid } = thingy
this.onEncapsulated(buffer, address)
})
this.raknet.on('connected', () => {
@ -51,18 +52,17 @@ class RakNativeClient extends EventEmitter {
class RakNativeServer extends EventEmitter {
constructor(options = {}) {
super()
console.log('opts',options)
this.onOpenConnection = () => {}
this.onCloseConnection = () => {}
this.onEncapsulated = () => {}
this.onOpenConnection = () => { }
this.onCloseConnection = () => { }
this.onEncapsulated = () => { }
this.raknet = new Server(options.hostname, options.port, {
maxConnections: options.maxConnections || 3,
minecraft: { },
minecraft: {},
message: new McPingMessage().toBuffer()
})
this.raknet.on('openConnection', (client) => {
client.sendReliable = function(buffer, immediate) {
client.sendReliable = function (buffer, immediate) {
const priority = immediate ? PacketPriority.IMMEDIATE_PRIORITY : PacketPriority.MEDIUM_PRIORITY
return this.send(buffer, priority, PacketReliability.RELIABLE_ORDERED, 0)
}
@ -70,12 +70,13 @@ class RakNativeServer extends EventEmitter {
})
this.raknet.on('closeConnection', (client) => {
console.log('!!! Client CLOSED CONNECTION!')
console.warn('! Client closed connection')
// TODO: need to handle this properly..
this.onCloseConnection(client)
})
this.raknet.on('encapsulated', (thingy) => {
const { buffer, address, guid }=thingy
const { buffer, address, guid } = thingy
// console.log('ENCAP',thingy)
this.onEncapsulated(buffer, address)
})
@ -89,8 +90,8 @@ class RakNativeServer extends EventEmitter {
class RakJsClient extends EventEmitter {
constructor(options = {}) {
super()
this.onConnected = () => {}
this.onEncapsulated = () => {}
this.onConnected = () => { }
this.onEncapsulated = () => { }
if (options.useWorkers) {
this.connect = this.workerConnect
this.sendReliable = this.workerSendReliable
@ -145,9 +146,9 @@ class RakJsServer extends EventEmitter {
constructor(options = {}) {
super()
this.options = options
this.onOpenConnection = () => {}
this.onCloseConnection = () => {}
this.onEncapsulated = () => {}
this.onOpenConnection = () => { }
this.onCloseConnection = () => { }
this.onEncapsulated = () => { }
if (options.useWorkers) {
throw Error('nyi')
@ -159,13 +160,13 @@ class RakJsServer extends EventEmitter {
async plainListen() {
this.raknet = new Listener()
await this.raknet.listen(this.options.hostname, this.options.port)
this.raknet.on('openConnection', (conn) => {
conn.sendReliable = function(buffer, immediate) {
this.raknet.on('openConnection', (conn) => {
conn.sendReliable = function (buffer, immediate) {
const sendPacket = new EncapsulatedPacket()
sendPacket.reliability = Reliability.ReliableOrdered
sendPacket.buffer = buffer
this.connection.addEncapsulatedToQueue(sendPacket)
if (immediate) this.raknet.sendQueue()
if (immediate) this.raknet.sendQueue()
}
this.onOpenConnection(conn)
})
@ -174,7 +175,7 @@ class RakJsServer extends EventEmitter {
}
}
module.exports = {
RakClient: Client ? RakNativeClient : RakJsClient,
module.exports = {
RakClient: Client ? RakNativeClient : RakJsClient,
RakServer: Server ? RakNativeServer : RakJsServer
}

View file

@ -4,6 +4,7 @@ const { Client } = require("./client")
const { Server } = require("./server")
const { Player } = require("./serverPlayer")
const debug = require('debug')('minecraft-protocol relay')
const { serialize } = require('./datatypes/util')
/** @typedef {{ hostname: string, port: number, auth: 'client' | 'server' | null, destination?: { hostname: string, port: number } }} Options */
@ -67,7 +68,6 @@ class RelayPlayer extends Player {
console.warn('Old', packet.toString('hex'))
console.log('Failed to re-encode', name, params)
process.exit(1)
throw Error('re-encoding fail for' + name + ' - ' + JSON.stringify(params))
}
}
@ -159,7 +159,7 @@ class Relay extends Server {
client.once('join', () => { // Intercept once handshaking done
ds.upstream = client
ds.flushUpQueue()
console.log('UPSTREAM HAS JOINED')
console.log('Connected to upstream server')
client.readPacket = (packet) => ds.readUpstream(packet)
})
this.upstreams.set(clientAddr.hash, client)
@ -167,7 +167,7 @@ class Relay extends Server {
closeUpstreamConnection(clientAddr) {
const up = this.upstreams.get(clientAddr.hash)
if (!up) throw Error(`unable to close non-existant connection ${clientAddr.hash}`)
if (!up) throw Error(`unable to close non-open connection ${clientAddr.hash}`)
up.close()
this.upstreams.delete(clientAddr.hash)
debug('relay closed connection', clientAddr)
@ -188,43 +188,5 @@ class Relay extends Server {
}
}
function serialize(obj = {}, fmt) {
return JSON.stringify(obj, (k, v) => typeof v == 'bigint' ? v.toString() : v, fmt)
}
function createRelay() {
console.log('Creating relay')
/**
* Example to create a non-transparent proxy (or 'Relay') connection to destination server
* In Relay we de-code and re-encode packets
*/
const relay = new Relay({
/* Hostname and port for clients to listen to */
hostname: '0.0.0.0',
port: 19130,
/**
* Who does the authentication
* If set to `client`, all connecting clients will be sent a message with a link to authenticate
* If set to `server`, the server will authenticate and only one client will be able to join
* (Default) If set to `none`, no authentication will be done
*/
auth: 'server',
/**
* Sets if packets will automatically be forwarded. If set to false, you must listen for on('packet')
* events and
*/
auto: true,
/* Where to send upstream packets to */
destination: {
hostname: '127.0.0.1',
port: 19132,
// encryption: true
}
})
relay.create()
}
createRelay()
// Too many things called 'Proxy' ;)
module.exports = { Relay }

View file

@ -9,18 +9,23 @@ class Server extends EventEmitter {
constructor(options) {
super()
this.options = { ...Options.defaultOptions, ...options }
this.serializer = createSerializer()
this.deserializer = createDeserializer()
this.validateOptions()
this.serializer = createSerializer(this.options.version)
this.deserializer = createDeserializer(this.options.version)
this.clients = {}
this.clientCount = 0
this.validateOptions()
this.inLog = (...args) => console.debug('S', ...args)
this.outLog = (...args) => console.debug('S', ...args)
this.inLog = (...args) => console.debug('C -> S', ...args)
this.outLog = (...args) => console.debug('S -> C', ...args)
}
validateOptions() {
if (this.options.version < Options.MIN_VERSION) {
throw new Error(`Unsupported protocol version < ${Options.MIN_VERSION} : ${this.options.version}`)
if (!Options.Versions[this.options.version]) {
console.warn('Supported versions: ', Options.Versions)
throw Error(`Unsupported version ${this.options.version}`)
}
this.options.protocolVersion = Options.Versions[this.options.version]
if (this.options.protocolVersion < Options.MIN_VERSION) {
throw new Error(`Protocol version < ${Options.MIN_VERSION} : ${this.options.protocolVersion}, too old`)
}
}
@ -39,7 +44,7 @@ class Server extends EventEmitter {
}
onEncapsulated = (buffer, address) => {
debug(address, 'Encapsulated', buffer)
this.inLog('encapsulated', address, buffer)
const client = this.clients[address]
if (!client) {
throw new Error(`packet from unknown inet addr: ${address}`)
@ -57,6 +62,4 @@ class Server extends EventEmitter {
}
}
const hash = (inetAddr) => inetAddr.address + '/' + inetAddr.port
module.exports = { Server }

View file

@ -16,15 +16,14 @@ class Player extends Connection {
this.server = server
this.serializer = server.serializer
this.deserializer = server.deserializer
// console.log('serializer/des',this.serializer,this.deserializer)
this.connection = connection
this.options = server.options
Encrypt(this, server, this.options)
this.startQueue()
this.status = ClientStatus.Authenticating
this.inLog = (...args) => console.info('S ->', ...args)
this.outLog = (...args) => console.info('S <-', ...args)
this.inLog = (...args) => console.info('S -> C', ...args)
this.outLog = (...args) => console.info('C -> S', ...args)
}
getData() {
@ -33,17 +32,17 @@ class Player extends Connection {
onLogin(packet) {
let body = packet.data
debug('Body', body)
// debug('Login body', body)
this.emit('loggingIn', body)
const clientVer = body.protocol_version
if (this.server.options.version) {
if (this.server.options.version < clientVer) {
this.sendDisconnectStatus(failed_client)
if (this.server.options.protocolVersion) {
if (this.server.options.protocolVersion < clientVer) {
this.sendDisconnectStatus('failed_client')
return
}
} else if (clientVer < MIN_VERSION) {
this.sendDisconnectStatus(failed_client)
this.sendDisconnectStatus('failed_client')
return
}
@ -55,6 +54,7 @@ class Player extends Connection {
var { key, userData, chain } = decodeLoginJWT(authChain.chain, skinChain)
} catch (e) {
console.error(e)
// TODO: disconnect user
throw new Error('Failed to verify user')
}
console.log('Verified user', 'got pub key', key, userData)
@ -66,7 +66,6 @@ class Player extends Connection {
this.version = clientVer
}
/**
* Disconnects a client before it has joined
* @param {string} play_status

View file

@ -1,20 +1,20 @@
const { PassThrough, Transform } = require('readable-stream')
const { Transform } = require('readable-stream')
const crypto = require('crypto')
const aesjs = require('aes-js')
const Zlib = require('zlib')
const CIPHER = 'aes-256-cfb8'
const CIPHER_ALG = 'aes-256-cfb8'
function createCipher(secret, initialValue) {
if (crypto.getCiphers().includes(CIPHER)) {
return crypto.createCipheriv(CIPHER, secret, initialValue)
if (crypto.getCiphers().includes(CIPHER_ALG)) {
return crypto.createCipheriv(CIPHER_ALG, secret, initialValue)
}
return new Cipher(secret, initialValue)
}
function createDecipher(secret, initialValue) {
if (crypto.getCiphers().includes(CIPHER)) {
return crypto.createDecipheriv(CIPHER, secret, initialValue)
if (crypto.getCiphers().includes(CIPHER_ALG)) {
return crypto.createDecipheriv(CIPHER_ALG, secret, initialValue)
}
return new Decipher(secret, initialValue)
}
@ -54,14 +54,11 @@ class Decipher extends Transform {
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);
}
@ -74,21 +71,14 @@ function createEncryptor(client, iv) {
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)
}
}
@ -99,6 +89,8 @@ function createDecryptor(client, iv) {
client.receiveCounter = client.receiveCounter || 0n
function verify(chunk) {
// TODO: remove the extra logic here, probably fixed with new raknet impl
// console.log('Decryptor: checking checksum', client.receiveCounter, chunk)
// client.outLog('🔵 Inflating', chunk)
// First try to zlib decompress, then see how much bytes get read
@ -139,8 +131,6 @@ function createDecryptor(client, iv) {
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)
}
}

View file

@ -1,9 +1,9 @@
const { ProtoDefCompiler, CompiledProtodef } = require('protodef').Compiler
const { FullPacketParser, Serializer } = require('protodef')
function createProtocol() {
const protocol = require('../../data/newproto.json').types
console.log('Proto', protocol)
// Compiles the ProtoDef schema at runtime
function createProtocol(version) {
const protocol = require(`../../data/${version}/protocol.json`).types
var compiler = new ProtoDefCompiler()
compiler.addTypesToCompile(protocol)
compiler.addTypes(require('../datatypes/compiler-minecraft'))
@ -13,8 +13,8 @@ function createProtocol() {
return compiledProto
}
function getProtocol() {
// Loads already generated read/write/sizeof code
function getProtocol(version) {
const compiler = new ProtoDefCompiler()
compiler.addTypes(require('../datatypes/compiler-minecraft'))
compiler.addTypes(require('prismarine-nbt/compiler-zigzag'))
@ -26,22 +26,19 @@ function getProtocol() {
}
return new CompiledProtodef(
compile(compiler.sizeOfCompiler, '../../data/size.js'),
compile(compiler.writeCompiler, '../../data/write.js'),
compile(compiler.readCompiler, '../../data/read.js')
// compiler.sizeOfCompiler.compile(fs.readFileSync(__dirname + '/../../data/size.js', 'utf-8')),
// compiler.writeCompiler.compile(fs.readFileSync(__dirname + '/../../data/write.js', 'utf-8')),
// compiler.readCompiler.compile(fs.readFileSync(__dirname + '/../../data/read.js', 'utf-8'))
compile(compiler.sizeOfCompiler, `../../data/${version}/size.js`),
compile(compiler.writeCompiler, `../../data/${version}/write.js`),
compile(compiler.readCompiler, `../../data/${version}/read.js`)
)
}
function createSerializer() {
var proto = getProtocol()
function createSerializer(version) {
var proto = getProtocol(version)
return new Serializer(proto, 'mcpe_packet');
}
function createDeserializer() {
var proto = getProtocol()
function createDeserializer(version) {
var proto = getProtocol(version)
return new FullPacketParser(proto, 'mcpe_packet');
}