Login refactoring (#45)
* Refactor auth + encryption Fix a bug with client account token caching * move some files
This commit is contained in:
parent
ff787ba8ad
commit
58e011e06d
13 changed files with 218 additions and 333 deletions
|
|
@ -945,8 +945,7 @@ packet_inventory_slot:
|
|||
slot: varint
|
||||
# NewItem is the item to be put in the slot at Slot. It will overwrite any item that may currently
|
||||
# be present in that slot.
|
||||
uniqueid: zigzag32
|
||||
item: Item
|
||||
item: ItemStack
|
||||
|
||||
# ContainerSetData is sent by the server to update specific data of a single container, meaning a block such
|
||||
# as a furnace or a brewing stand. This data is usually used by the client to display certain features
|
||||
|
|
@ -1325,97 +1324,6 @@ packet_available_commands:
|
|||
43: raw_text
|
||||
46: json
|
||||
53: command
|
||||
# 15: unknown15
|
||||
# 16: unknown16
|
||||
# 125: unknown125
|
||||
# 126: unknown126
|
||||
# 22: unknown22
|
||||
# 10: unknown10
|
||||
# 39: unknown39
|
||||
# 18: unknown18
|
||||
# 20: unknown20
|
||||
# 19: unknown19
|
||||
# 7: unknown7
|
||||
# 23: unknown23
|
||||
# 24: unknown24
|
||||
# 13: unknown13
|
||||
# 25: unknown25
|
||||
# 40: unknown40
|
||||
# 56: unknown56
|
||||
# 26: unknown26
|
||||
# 27: unknown27
|
||||
# 28: unknown28
|
||||
# 31: unknown31
|
||||
# 30: unknown30
|
||||
# 0: unknown0
|
||||
# 32: unknown32
|
||||
# 49: unknown49
|
||||
# 12: unknown12
|
||||
# 35: unknown35
|
||||
# 36: unknown36
|
||||
# 38: unknown38
|
||||
# 44: unknown44
|
||||
# 47: unknown47
|
||||
# 45: unknown45
|
||||
# 48: unknown48
|
||||
# 55: unknown55
|
||||
# 54: unknown54
|
||||
# 50: unknown50
|
||||
# 51: unknown51
|
||||
# 52: unknown52
|
||||
# 9: unknown9
|
||||
# 123: unknown123
|
||||
# 57: unknown57
|
||||
# 58: unknown58
|
||||
# 59: unknown59
|
||||
# 60: unknown60
|
||||
# 61: unknown61
|
||||
# 63: unknown63
|
||||
# 75: unknown75
|
||||
# 64: unknown64
|
||||
# 67: unknown67
|
||||
# 66: unknown66
|
||||
# 73: unknown73
|
||||
# 72: unknown72
|
||||
# 74: unknown74
|
||||
# 62: unknown62
|
||||
# 68: unknown68
|
||||
# 69: unknown69
|
||||
# 65: unknown65
|
||||
# 70: unknown70
|
||||
# 71: unknown71
|
||||
# 76: unknown76
|
||||
# 77: unknown77
|
||||
# 79: unknown79
|
||||
# 78: unknown78
|
||||
# 80: unknown80
|
||||
# 81: unknown81
|
||||
# 82: unknown82
|
||||
# 83: unknown83
|
||||
# 84: unknown84
|
||||
# 87: unknown87
|
||||
# 88: unknown88
|
||||
# 92: unknown92
|
||||
# 89: unknown89
|
||||
# 90: unknown90
|
||||
# 91: unknown91
|
||||
# 93: unknown93
|
||||
# 95: unknown95
|
||||
# 94: unknown94
|
||||
# 98: unknown98
|
||||
# 96: unknown96
|
||||
# 97: unknown97
|
||||
# 99: unknown99
|
||||
# 100: unknown100
|
||||
# 101: unknown101
|
||||
# 102: unknown102
|
||||
# 103: unknown103
|
||||
# 104: unknown104
|
||||
# 105: unknown105
|
||||
# 106: unknown106
|
||||
# 107: unknown107
|
||||
# 108: unknown108
|
||||
# 124: unknown124
|
||||
# In MC, this + prior field are combined to one 32bit bitfield
|
||||
enum_type: lu16 =>
|
||||
0x10: valid
|
||||
|
|
@ -408,10 +408,18 @@ Transaction:
|
|||
# mainly for purposes such as spawning eating particles at that position.
|
||||
head_pos: vec3f
|
||||
|
||||
ItemStacks: []varint
|
||||
# An "ItemStack" here represents an Item instance. You can think about it like a pointer
|
||||
# to an item class. The data for the class gets updated with the data in the `item` field
|
||||
ItemStack:
|
||||
# StackNetworkID is the network ID of the item stack. If the stack is empty, 0 is always written for this
|
||||
# field. If not, the field should be set to 1 if the server authoritative inventories are disabled in the
|
||||
# StartGame packet, or to a unique stack ID if it is enabled.
|
||||
runtime_id: zigzag32
|
||||
# Stack is the actual item stack of the item instance.
|
||||
item: Item
|
||||
|
||||
ItemStacks: ItemStack[]varint
|
||||
|
||||
RecipeIngredient:
|
||||
network_id: zigzag32
|
||||
_: network_id?
|
||||
|
|
@ -14,7 +14,7 @@ function loadVersions () {
|
|||
files = getFiles(join(__dirname, '/', version))
|
||||
} catch {}
|
||||
for (const file of files) {
|
||||
const rfile = file.replace(join(__dirname, '/', version), '')
|
||||
const rfile = file.replace(join(__dirname, '/', version) + '/', '')
|
||||
fileMap[rfile] ??= []
|
||||
fileMap[rfile].push([Versions[version], file])
|
||||
fileMap[rfile].sort().reverse()
|
||||
|
|
@ -42,5 +42,3 @@ module.exports = (protocolVersion) => {
|
|||
}
|
||||
|
||||
loadVersions()
|
||||
// console.log('file map', fileMap)
|
||||
// module.exports(Versions['1.16.210']).open('creativeitems.json')
|
||||
|
|
|
|||
|
|
@ -1,110 +0,0 @@
|
|||
const JWT = require('jsonwebtoken')
|
||||
const constants = require('./constants')
|
||||
|
||||
// Refer to the docs:
|
||||
// https://web.archive.org/web/20180917171505if_/https://confluence.yawk.at/display/PEPROTOCOL/Game+Packets#GamePackets-Login
|
||||
|
||||
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 getX5U (token) {
|
||||
const [header] = token.split('.')
|
||||
const hdec = Buffer.from(header, 'base64').toString('utf-8')
|
||||
const hjson = JSON.parse(hdec)
|
||||
return hjson.x5u
|
||||
}
|
||||
|
||||
function verifyAuth (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(getX5U(chain[0])) // the first one is client signed, allow it
|
||||
let finalKey = null
|
||||
console.log(pubKey)
|
||||
for (const token of chain) {
|
||||
// const decoded = jwt.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 x5u = getX5U(token)
|
||||
if (x5u === constants.PUBLIC_KEY && !data.extraData?.XUID) {
|
||||
// didVerify = true
|
||||
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 }
|
||||
}
|
||||
// console.log('Result', data)
|
||||
|
||||
return { key: finalKey, data }
|
||||
}
|
||||
|
||||
function verifySkin (publicKey, token) {
|
||||
// console.log('token', token)
|
||||
const pubKey = mcPubKeyToPem(publicKey)
|
||||
|
||||
const decoded = JWT.verify(token, pubKey, { algorithms: 'ES384' })
|
||||
|
||||
return decoded
|
||||
}
|
||||
|
||||
function decodeLoginJWT (authTokens, skinTokens) {
|
||||
const { key, data } = verifyAuth(authTokens)
|
||||
const skinData = verifySkin(key, skinTokens)
|
||||
return { key, userData: data, skinData }
|
||||
}
|
||||
|
||||
function encodeLoginJWT (localChain, mojangChain) {
|
||||
const chains = []
|
||||
chains.push(localChain)
|
||||
for (const chain of mojangChain) {
|
||||
chains.push(chain)
|
||||
}
|
||||
return chains
|
||||
}
|
||||
|
||||
module.exports = { encodeLoginJWT, decodeLoginJWT }
|
||||
|
||||
// function testServer() {
|
||||
// const loginPacket = require('./login.json')
|
||||
|
||||
// // 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')
|
||||
// }
|
||||
|
||||
// console.log('Authed')
|
||||
// // console.log(loginPacket)
|
||||
// }
|
||||
|
||||
// // testServer()
|
||||
|
|
@ -1,28 +1,25 @@
|
|||
const { Ber } = require('asn1')
|
||||
const JWT = require('jsonwebtoken')
|
||||
const crypto = require('crypto')
|
||||
const { Ber } = require('asn1')
|
||||
const ecPem = require('ec-pem')
|
||||
const fs = require('fs')
|
||||
const DataProvider = require('../../data/provider')
|
||||
const debug = require('debug')('minecraft-protocol')
|
||||
|
||||
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())
|
||||
|
||||
function startClientboundEncryption (publicKey) {
|
||||
console.warn('[encrypt] Pub key base64: ', publicKey)
|
||||
debug('[encrypt] Client pub key base64: ', publicKey)
|
||||
const pubKeyBuf = readX509PublicKey(publicKey.key)
|
||||
|
||||
const alice = client.ecdhKeyPair
|
||||
const alicePEM = ecPem(alice, curve) // https://github.com/nodejs/node/issues/15116#issuecomment-384790125
|
||||
const alicePEMPrivate = alicePEM.encodePrivateKey()
|
||||
// Shared secret from bob's public key + our private key
|
||||
// Shared secret from the client's public key + our private key
|
||||
client.sharedSecret = alice.computeSecret(pubKeyBuf)
|
||||
|
||||
// Secret hash we use for packet encryption:
|
||||
|
|
@ -34,10 +31,10 @@ function Encrypt (client, server, options) {
|
|||
const secretHash = crypto.createHash('sha256')
|
||||
secretHash.update(SALT)
|
||||
secretHash.update(client.sharedSecret)
|
||||
console.log('[encrypt] Shared secret', client.sharedSecret)
|
||||
// console.log('[encrypt] Shared secret', client.sharedSecret)
|
||||
|
||||
client.secretKeyBytes = secretHash.digest()
|
||||
console.log('[encrypt] Shared hash', client.secretKeyBytes)
|
||||
// console.log('[encrypt] Shared hash', client.secretKeyBytes)
|
||||
const x509 = writeX509PublicKey(alice.getPublicKey())
|
||||
const token = JWT.sign({
|
||||
salt: toBase64(SALT),
|
||||
|
|
@ -56,7 +53,7 @@ function Encrypt (client, server, options) {
|
|||
}
|
||||
|
||||
function startServerboundEncryption (token) {
|
||||
console.warn('[encrypt] Starting serverbound encryption', token)
|
||||
debug('[encrypt] Starting serverbound encryption', token)
|
||||
const jwt = token?.token
|
||||
if (!jwt) {
|
||||
// TODO: allow connecting to servers without encryption
|
||||
|
|
@ -69,7 +66,7 @@ function Encrypt (client, server, options) {
|
|||
const body = JSON.parse(String(payload))
|
||||
const serverPublicKey = readX509PublicKey(head.x5u)
|
||||
client.sharedSecret = alice.computeSecret(serverPublicKey)
|
||||
console.log('[encrypt] Shared secret', client.sharedSecret)
|
||||
// console.log('[encrypt] Shared secret', client.sharedSecret)
|
||||
|
||||
const salt = Buffer.from(body.salt, 'base64')
|
||||
|
||||
|
|
@ -78,7 +75,7 @@ function Encrypt (client, server, options) {
|
|||
secretHash.update(client.sharedSecret)
|
||||
|
||||
client.secretKeyBytes = secretHash.digest()
|
||||
console.log('[encrypt] Shared hash', client.secretKeyBytes)
|
||||
// console.log('[encrypt] Shared hash', client.secretKeyBytes)
|
||||
const initial = client.secretKeyBytes.slice(0, 16)
|
||||
client.startEncryption(initial)
|
||||
|
||||
|
|
@ -89,70 +86,6 @@ function Encrypt (client, server, options) {
|
|||
|
||||
client.on('server.client_handshake', startClientboundEncryption)
|
||||
client.on('client.server_handshake', startServerboundEncryption)
|
||||
|
||||
client.createClientChain = (mojangKey) => {
|
||||
mojangKey = mojangKey || require('./constants').PUBLIC_KEY
|
||||
const alice = client.ecdhKeyPair
|
||||
const alicePEM = ecPem(alice, curve) // https://github.com/nodejs/node/issues/15116#issuecomment-384790125
|
||||
const alicePEMPrivate = alicePEM.encodePrivateKey()
|
||||
|
||||
const token = JWT.sign({
|
||||
identityPublicKey: mojangKey,
|
||||
certificateAuthority: true
|
||||
}, alicePEMPrivate, { algorithm: 'ES384', header: { x5u: client.clientX509 } })
|
||||
|
||||
client.clientIdentityChain = token
|
||||
client.createClientUserChain(alicePEMPrivate)
|
||||
}
|
||||
|
||||
client.createClientUserChain = (privateKey) => {
|
||||
let payload = {
|
||||
ServerAddress: options.hostname,
|
||||
ThirdPartyName: client.profile.name,
|
||||
DeviceOS: client.session?.deviceOS || 1,
|
||||
GameVersion: options.version || '1.16.201',
|
||||
ClientRandomId: Date.now(), // TODO make biggeer
|
||||
DeviceId: '2099de18-429a-465a-a49b-fc4710a17bb3', // TODO random
|
||||
LanguageCode: 'en_GB', // TODO locale
|
||||
AnimatedImageData: [],
|
||||
PersonaPieces: [],
|
||||
PieceTintColours: [],
|
||||
SelfSignedId: '78eb38a6-950e-3ab9-b2cf-dd849e343701',
|
||||
SkinId: '5eb65f73-af11-448e-82aa-1b7b165316ad.persona-e199672a8c1a87e0-0',
|
||||
SkinData: 'AAAAAA==',
|
||||
SkinResourcePatch: 'ewogICAiZ2VvbWV0cnkiIDogewogICAgICAiYW5pbWF0ZWRfMTI4eDEyOCIgOiAiZ2VvbWV0cnkuYW5pbWF0ZWRfMTI4eDEyOF9wZXJzb25hLWUxOTk2NzJhOGMxYTg3ZTAtMCIsCiAgICAgICJhbmltYXRlZF9mYWNlIiA6ICJnZW9tZXRyeS5hbmltYXRlZF9mYWNlX3BlcnNvbmEtZTE5OTY3MmE4YzFhODdlMC0wIiwKICAgICAgImRlZmF1bHQiIDogImdlb21ldHJ5LnBlcnNvbmFfZTE5OTY3MmE4YzFhODdlMC0wIgogICB9Cn0K',
|
||||
SkinGeometryData: skinGeom,
|
||||
SkinImageHeight: 1,
|
||||
SkinImageWidth: 1,
|
||||
ArmSize: 'wide',
|
||||
CapeData: '',
|
||||
CapeId: '',
|
||||
CapeImageHeight: 0,
|
||||
CapeImageWidth: 0,
|
||||
CapeOnClassicSkin: false,
|
||||
PlatformOfflineId: '',
|
||||
PlatformOnlineId: '', // chat
|
||||
// a bunch of meaningless junk
|
||||
CurrentInputMode: 1,
|
||||
DefaultInputMode: 1,
|
||||
DeviceModel: '',
|
||||
GuiScale: -1,
|
||||
UIProfile: 0,
|
||||
TenantId: '',
|
||||
PremiumSkin: false,
|
||||
PersonaSkin: false,
|
||||
PieceTintColors: [],
|
||||
SkinAnimationData: '',
|
||||
ThirdPartyNameOnly: false,
|
||||
SkinColor: '#ffffcd96'
|
||||
}
|
||||
payload = require('./logPack.json')
|
||||
const customPayload = options.userData || {}
|
||||
payload = { ...payload, ...customPayload }
|
||||
|
||||
client.clientUserChain = JWT.sign(payload, privateKey,
|
||||
{ algorithm: 'ES384', header: { x5u: client.clientX509 } })
|
||||
}
|
||||
}
|
||||
|
||||
function toBase64 (string) {
|
||||
|
|
|
|||
73
src/auth/login.js
Normal file
73
src/auth/login.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
const fs = require('fs')
|
||||
const JWT = require('jsonwebtoken')
|
||||
const DataProvider = require('../../data/provider')
|
||||
const ecPem = require('ec-pem')
|
||||
const curve = 'secp384r1'
|
||||
|
||||
module.exports = (client, server, options) => {
|
||||
const skinGeom = fs.readFileSync(DataProvider(options.protocolVersion).getPath('skin_geom.txt'), 'utf-8')
|
||||
|
||||
client.createClientChain = (mojangKey) => {
|
||||
mojangKey = mojangKey || require('./constants').PUBLIC_KEY
|
||||
const alice = client.ecdhKeyPair
|
||||
const alicePEM = ecPem(alice, curve) // https://github.com/nodejs/node/issues/15116#issuecomment-384790125
|
||||
const alicePEMPrivate = alicePEM.encodePrivateKey()
|
||||
|
||||
const token = JWT.sign({
|
||||
identityPublicKey: mojangKey,
|
||||
certificateAuthority: true
|
||||
}, alicePEMPrivate, { algorithm: 'ES384', header: { x5u: client.clientX509 } })
|
||||
|
||||
client.clientIdentityChain = token
|
||||
client.createClientUserChain(alicePEMPrivate)
|
||||
}
|
||||
|
||||
client.createClientUserChain = (privateKey) => {
|
||||
let payload = {
|
||||
ServerAddress: options.hostname,
|
||||
ThirdPartyName: client.profile.name,
|
||||
DeviceOS: client.session?.deviceOS || 1,
|
||||
GameVersion: options.version || '1.16.201',
|
||||
ClientRandomId: Date.now(), // TODO make biggeer
|
||||
DeviceId: '2099de18-429a-465a-a49b-fc4710a17bb3', // TODO random
|
||||
LanguageCode: 'en_GB', // TODO locale
|
||||
AnimatedImageData: [],
|
||||
PersonaPieces: [],
|
||||
PieceTintColours: [],
|
||||
SelfSignedId: '78eb38a6-950e-3ab9-b2cf-dd849e343701',
|
||||
SkinId: '5eb65f73-af11-448e-82aa-1b7b165316ad.persona-e199672a8c1a87e0-0',
|
||||
SkinData: 'AAAAAA==',
|
||||
SkinResourcePatch: 'ewogICAiZ2VvbWV0cnkiIDogewogICAgICAiYW5pbWF0ZWRfMTI4eDEyOCIgOiAiZ2VvbWV0cnkuYW5pbWF0ZWRfMTI4eDEyOF9wZXJzb25hLWUxOTk2NzJhOGMxYTg3ZTAtMCIsCiAgICAgICJhbmltYXRlZF9mYWNlIiA6ICJnZW9tZXRyeS5hbmltYXRlZF9mYWNlX3BlcnNvbmEtZTE5OTY3MmE4YzFhODdlMC0wIiwKICAgICAgImRlZmF1bHQiIDogImdlb21ldHJ5LnBlcnNvbmFfZTE5OTY3MmE4YzFhODdlMC0wIgogICB9Cn0K',
|
||||
SkinGeometryData: skinGeom,
|
||||
SkinImageHeight: 1,
|
||||
SkinImageWidth: 1,
|
||||
ArmSize: 'wide',
|
||||
CapeData: '',
|
||||
CapeId: '',
|
||||
CapeImageHeight: 0,
|
||||
CapeImageWidth: 0,
|
||||
CapeOnClassicSkin: false,
|
||||
PlatformOfflineId: '',
|
||||
PlatformOnlineId: '', // chat
|
||||
// a bunch of meaningless junk
|
||||
CurrentInputMode: 1,
|
||||
DefaultInputMode: 1,
|
||||
DeviceModel: '',
|
||||
GuiScale: -1,
|
||||
UIProfile: 0,
|
||||
TenantId: '',
|
||||
PremiumSkin: false,
|
||||
PersonaSkin: false,
|
||||
PieceTintColors: [],
|
||||
SkinAnimationData: '',
|
||||
ThirdPartyNameOnly: false,
|
||||
SkinColor: '#ffffcd96'
|
||||
}
|
||||
payload = require('./logPack.json')
|
||||
const customPayload = options.userData || {}
|
||||
payload = { ...payload, ...customPayload }
|
||||
|
||||
client.clientUserChain = JWT.sign(payload, privateKey,
|
||||
{ algorithm: 'ES384', header: { x5u: client.clientX509 } })
|
||||
}
|
||||
}
|
||||
83
src/auth/loginVerify.js
Normal file
83
src/auth/loginVerify.js
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
const JWT = require('jsonwebtoken')
|
||||
const constants = require('./constants')
|
||||
|
||||
module.exports = (client, server, options) => {
|
||||
// Refer to the docs:
|
||||
// https://web.archive.org/web/20180917171505if_/https://confluence.yawk.at/display/PEPROTOCOL/Game+Packets#GamePackets-Login
|
||||
|
||||
function verifyAuth (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(getX5U(chain[0])) // the first one is client signed, allow it
|
||||
let finalKey = null
|
||||
console.log(pubKey)
|
||||
for (const token of chain) {
|
||||
const decoded = JWT.verify(token, pubKey, { algorithms: 'ES384' })
|
||||
console.log('Decoded', decoded)
|
||||
|
||||
// Check if signed by Mojang key
|
||||
const x5u = getX5U(token)
|
||||
if (x5u === constants.PUBLIC_KEY && !data.extraData?.XUID) {
|
||||
// didVerify = true
|
||||
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 }
|
||||
}
|
||||
// console.log('Result', data)
|
||||
|
||||
return { key: finalKey, data }
|
||||
}
|
||||
|
||||
function verifySkin (publicKey, token) {
|
||||
const pubKey = mcPubKeyToPem(publicKey)
|
||||
const decoded = JWT.verify(token, pubKey, { algorithms: 'ES384' })
|
||||
return decoded
|
||||
}
|
||||
|
||||
client.decodeLoginJWT = (authTokens, skinTokens) => {
|
||||
const { key, data } = verifyAuth(authTokens)
|
||||
const skinData = verifySkin(key, skinTokens)
|
||||
return { key, userData: data, skinData }
|
||||
}
|
||||
|
||||
client.encodeLoginJWT = (localChain, mojangChain) => {
|
||||
const chains = []
|
||||
chains.push(localChain)
|
||||
for (const chain of mojangChain) {
|
||||
chains.push(chain)
|
||||
}
|
||||
return chains
|
||||
}
|
||||
}
|
||||
|
||||
function getX5U (token) {
|
||||
const [header] = token.split('.')
|
||||
const hdec = Buffer.from(header, 'base64').toString('utf-8')
|
||||
const hjson = JSON.parse(hdec)
|
||||
return hjson.x5u
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -1,12 +1,15 @@
|
|||
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 { RakClient } = require('./Rak')
|
||||
const { serialize } = require('./datatypes/util')
|
||||
const fs = require('fs')
|
||||
const debug = require('debug')('minecraft-protocol')
|
||||
const Options = require('./options')
|
||||
const auth = require('./client/auth')
|
||||
|
||||
const { Encrypt } = require('./auth/encryption')
|
||||
const Login = require('./auth/login')
|
||||
const LoginVerify = require('./auth/loginVerify')
|
||||
|
||||
const debugging = false
|
||||
|
||||
|
|
@ -20,6 +23,8 @@ class Client extends Connection {
|
|||
this.deserializer = createDeserializer(this.options.version)
|
||||
|
||||
Encrypt(this, null, this.options)
|
||||
Login(this, null, this.options)
|
||||
LoginVerify(this, null, this.options)
|
||||
|
||||
if (options.password) {
|
||||
auth.authenticatePassword(this, options)
|
||||
|
|
|
|||
|
|
@ -13,12 +13,7 @@ class MsaTokenManager {
|
|||
this.scopes = scopes
|
||||
this.cacheLocation = cacheLocation || path.join(__dirname, './msa-cache.json')
|
||||
|
||||
try {
|
||||
this.msaCache = require(this.cacheLocation)
|
||||
} catch (e) {
|
||||
this.msaCache = {}
|
||||
fs.writeFileSync(this.cacheLocation, JSON.stringify(this.msaCache))
|
||||
}
|
||||
this.reloadCache()
|
||||
|
||||
const beforeCacheAccess = async (cacheContext) => {
|
||||
cacheContext.tokenCache.deserialize(await fs.promises.readFile(this.cacheLocation, 'utf-8'))
|
||||
|
|
@ -42,6 +37,15 @@ class MsaTokenManager {
|
|||
this.msalConfig = msalConfig
|
||||
}
|
||||
|
||||
reloadCache () {
|
||||
try {
|
||||
this.msaCache = require(this.cacheLocation)
|
||||
} catch (e) {
|
||||
this.msaCache = {}
|
||||
fs.writeFileSync(this.cacheLocation, JSON.stringify(this.msaCache))
|
||||
}
|
||||
}
|
||||
|
||||
getUsers () {
|
||||
const accounts = this.msaCache.Account
|
||||
const users = []
|
||||
|
|
@ -89,6 +93,7 @@ class MsaTokenManager {
|
|||
return new Promise((resolve, reject) => {
|
||||
this.msalApp.acquireTokenByRefreshToken(refreshTokenRequest).then((response) => {
|
||||
debug('[msa] refreshed token', JSON.stringify(response))
|
||||
this.reloadCache()
|
||||
resolve(response)
|
||||
}).catch((error) => {
|
||||
debug('[msa] failed to refresh', JSON.stringify(error))
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ class Server extends EventEmitter {
|
|||
}
|
||||
|
||||
onEncapsulated = (buffer, address) => {
|
||||
this.inLog('encapsulated', address, buffer)
|
||||
// this.inLog('encapsulated', address, buffer)
|
||||
const client = this.clients[address]
|
||||
if (!client) {
|
||||
throw new Error(`packet from unknown inet addr: ${address}`)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
const { Encrypt } = require('./auth/encryption')
|
||||
const { decodeLoginJWT } = require('./auth/chains')
|
||||
const { Connection } = require('./connection')
|
||||
const fs = require('fs')
|
||||
// const debug = require('debug')('minecraft-protocol')
|
||||
const { MIN_VERSION } = require('./options')
|
||||
const Options = require('./options')
|
||||
|
||||
const { Encrypt } = require('./auth/encryption')
|
||||
const Login = require('./auth/login')
|
||||
const LoginVerify = require('./auth/loginVerify')
|
||||
|
||||
const ClientStatus = {
|
||||
Authenticating: 0,
|
||||
|
|
@ -19,7 +20,10 @@ class Player extends Connection {
|
|||
this.deserializer = server.deserializer
|
||||
this.connection = connection
|
||||
this.options = server.options
|
||||
Encrypt(this, server, this.options)
|
||||
|
||||
Encrypt(this, server, server.options)
|
||||
Login(this, server, server.options)
|
||||
LoginVerify(this, server, server.options)
|
||||
|
||||
this.startQueue()
|
||||
this.status = ClientStatus.Authenticating
|
||||
|
|
@ -42,7 +46,7 @@ class Player extends Connection {
|
|||
this.sendDisconnectStatus('failed_client')
|
||||
return
|
||||
}
|
||||
} else if (clientVer < MIN_VERSION) {
|
||||
} else if (clientVer < Options.MIN_VERSION) {
|
||||
this.sendDisconnectStatus('failed_client')
|
||||
return
|
||||
}
|
||||
|
|
@ -52,7 +56,7 @@ class Player extends Connection {
|
|||
const skinChain = body.params.client_data
|
||||
|
||||
try {
|
||||
var { key, userData, chain } = decodeLoginJWT(authChain.chain, skinChain) // eslint-disable-line
|
||||
var { key, userData, chain } = this.decodeLoginJWT(authChain.chain, skinChain) // eslint-disable-line
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
// TODO: disconnect user
|
||||
|
|
|
|||
|
|
@ -88,43 +88,21 @@ 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
|
||||
const { buffer, engine } = Zlib.inflateRawSync(chunk, {
|
||||
chunkSize: 1024 * 1024 * 2,
|
||||
info: true
|
||||
})
|
||||
|
||||
// Holds how much bytes we read, also where the checksum (should) start
|
||||
const inflatedLen = engine.bytesRead
|
||||
// It appears that mc sends extra bytes past the checksum. I don't think this is a raknet
|
||||
// issue (as we are able to decipher properly, zlib works and should also have a checksum) so
|
||||
// there needs to be more investigation done. If you know what's wrong here, please make an issue :)
|
||||
const extraneousLen = chunk.length - inflatedLen - 8
|
||||
if (extraneousLen > 0) { // Extra bytes
|
||||
// Info for debugging, todo: use debug()
|
||||
const extraneousBytes = chunk.slice(inflatedLen + 8)
|
||||
console.debug('Extraneous bytes!', extraneousLen, extraneousBytes.toString('hex'))
|
||||
} else if (extraneousLen < 0) {
|
||||
// No checksum or decompression failed
|
||||
console.warn('Failed to decrypt', chunk.toString('hex'))
|
||||
throw new Error('Decrypted packet is missing checksum')
|
||||
}
|
||||
|
||||
const packet = chunk.slice(0, inflatedLen)
|
||||
const checksum = chunk.slice(inflatedLen, inflatedLen + 8)
|
||||
const packet = chunk.slice(0, chunk.length - 8)
|
||||
const checksum = chunk.slice(chunk.length - 8, chunk.length)
|
||||
const computedCheckSum = computeCheckSum(packet, client.receiveCounter, client.secretKeyBytes)
|
||||
client.receiveCounter++
|
||||
|
||||
if (checksum.toString('hex') === computedCheckSum.toString('hex')) {
|
||||
client.onDecryptedPacket(buffer)
|
||||
} else {
|
||||
console.log('Inflated', inflatedLen, chunk.length, extraneousLen, chunk.toString('hex'))
|
||||
if (Buffer.compare(checksum, computedCheckSum) !== 0) {
|
||||
// console.log('Inflated', inflatedLen, chunk.length, extraneousLen, chunk.toString('hex'))
|
||||
throw Error(`Checksum mismatch ${checksum.toString('hex')} != ${computedCheckSum.toString('hex')}`)
|
||||
}
|
||||
|
||||
Zlib.inflateRaw(chunk, { chunkSize: 1024 * 1024 * 2 }, (err, buffer) => {
|
||||
if (err) throw err
|
||||
client.onDecryptedPacket(buffer)
|
||||
})
|
||||
}
|
||||
|
||||
client.decipher.on('data', verify)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue