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:
parent
3ffcf841ea
commit
df8612e355
35 changed files with 368 additions and 3517 deletions
|
|
@ -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()
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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()
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
@ -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
37
src/datatypes/util.js
Normal 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
|
||||
}
|
||||
|
|
@ -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 }
|
||||
47
src/rak.js
47
src/rak.js
|
|
@ -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
|
||||
}
|
||||
48
src/relay.js
48
src/relay.js
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue