workingish client connection to server

Joining a vanilla server is still broken (related to encryption), but minet connects
This commit is contained in:
extremeheat 2021-02-13 18:42:43 -05:00
commit 233f0d33a6
13 changed files with 232 additions and 104 deletions

View file

@ -197,13 +197,13 @@ packet_start_game:
entity_id: zigzag64
# The runtime ID of the player. The runtime ID is unique for each world session,
# and entities are generally identified in packets using this runtime ID.
runtime_entity_id: varint
runtime_entity_id: varint64
player_gamemode: zigzag32
# The spawn position of the player in the world. In servers this is often the same as the
# world's spawn position found below.
spawn: vec3f
# The pitch and yaw of the player
rotation: vec3f
rotation: vec2f
# The seed used to generate the world. Unlike in Java edition, the seed is a 32bit Integer here.
seed: zigzag32
biome_type: li16
@ -302,8 +302,8 @@ packet_start_game:
# The version of the game from which Vanilla features will be used.
# The exact function of this field isn't clear.
game_version: string
limited_world_width_: li32
limited_world_length_: li32
limited_world_width: li32
limited_world_length: li32
is_new_nether_: bool
experimental_gameplay_override: bool
# A base64 encoded world ID that is used to identify the world.
@ -335,7 +335,7 @@ packet_start_game:
## This is not sent anymore in protocol versions > 419 (Bedrock Edition v1.16.100)
## A list of all blocks registered on the server.
## block_palette: BlockPalette
block_palette: BlockPalette
# A list of all items with their legacy IDs which are available in the game.
# Failing to send any of the items that are in the game will crash mobile clients.
itemstates: Itemstates
@ -525,9 +525,9 @@ packet_mob_effect:
packet_update_attributes:
!id: 0x1d
!bound: server
runtime_entity_id: varint
runtime_entity_id: varint64
attributes: PlayerAttributes
tick: varint
tick: varint64
packet_inventory_transaction:
!id: 0x1e

View file

@ -3,7 +3,7 @@
BehaviourPackInfos: []li16
uuid: string
version: string
length: lu64
size: lu64
content_key: string
sub_pack_name: string
content_identity: string
@ -12,7 +12,7 @@ BehaviourPackInfos: []li16
TexturePackInfos: []li16
uuid: string
version: string
length: lu64
size: lu64
content_key: string
sub_pack_name: string
content_identity: string
@ -27,13 +27,13 @@ ResourcePackIdVersions: []varint
# The subpack name of the resource pack.
name: string
ResourcePackIds: string[]varint
ResourcePackIds: string[]li16
Experiment:
name: string
enabled: bool
Experiments: Experiment[]varint
Experiments: Experiment[]li32
GameRule:
name: string
@ -43,7 +43,7 @@ GameRule:
3: float
value: type?
if bool: bool
if int: varint
if int: zigzag32
if float: lf32
GameRules: GameRule[]varint
@ -150,10 +150,10 @@ BlockCoordinates: # mojang...
z: zigzag32
PlayerAttributes: []varint
min_value: lf32
max_value: lf32
current_value: lf32
default_value: lf32
min: lf32
max: lf32
current: lf32
default: lf32
name: string
Transaction:
@ -190,7 +190,7 @@ Transaction:
slot: varint
old_item: Item
new_item: Item
new_item_stack_id: has_network_ids?
new_item_stack_id: ../has_network_ids?
if true: zigzag32
default: void
transaction_data: transaction_type?
@ -242,7 +242,7 @@ PotionContainerChangeRecipes: []varint
output_item_id: zigzag32
Recipes: []varint
type: varint =>
type: zigzag32 =>
'0': 'shapeless' #'ENTRY_SHAPELESS',
'1': 'shaped' #'ENTRY_SHAPED',
'2': 'furnace' # 'ENTRY_FURNACE',
@ -341,9 +341,6 @@ PlayerRecords:
is_host: bool
if remove:
uuid: uuid
uuid: type?
if add: uuid
default: void
verified: bool[]$records_count
ScoreEntries:

View file

@ -25,7 +25,7 @@
"type": "string"
},
{
"name": "length",
"name": "size",
"type": "lu64"
},
{
@ -64,7 +64,7 @@
"type": "string"
},
{
"name": "length",
"name": "size",
"type": "lu64"
},
{
@ -117,7 +117,7 @@
"ResourcePackIds": [
"array",
{
"countType": "varint",
"countType": "li16",
"type": "string"
}
],
@ -137,7 +137,7 @@
"Experiments": [
"array",
{
"countType": "varint",
"countType": "li32",
"type": "Experiment"
}
],
@ -170,7 +170,7 @@
"compareTo": "type",
"fields": {
"bool": "bool",
"int": "varint",
"int": "zigzag32",
"float": "lf32"
},
"default": "void"
@ -580,19 +580,19 @@
"container",
[
{
"name": "min_value",
"name": "min",
"type": "lf32"
},
{
"name": "max_value",
"name": "max",
"type": "lf32"
},
{
"name": "current_value",
"name": "current",
"type": "lf32"
},
{
"name": "default_value",
"name": "default",
"type": "lf32"
},
{
@ -775,7 +775,7 @@
"type": [
"switch",
{
"compareTo": "has_network_ids",
"compareTo": "../has_network_ids",
"fields": {
"true": "zigzag32"
},
@ -1010,7 +1010,7 @@
"type": [
"mapper",
{
"type": "varint",
"type": "zigzag32",
"mappings": {
"0": "shapeless",
"1": "shaped",
@ -1449,19 +1449,6 @@
}
]
},
{
"name": "uuid",
"type": [
"switch",
{
"compareTo": "type",
"fields": {
"add": "uuid"
},
"default": "void"
}
]
},
{
"name": "verified",
"type": [
@ -2671,7 +2658,7 @@
},
{
"name": "runtime_entity_id",
"type": "varint"
"type": "varint64"
},
{
"name": "player_gamemode",
@ -2683,7 +2670,7 @@
},
{
"name": "rotation",
"type": "vec3f"
"type": "vec2f"
},
{
"name": "seed",
@ -2834,11 +2821,11 @@
"type": "string"
},
{
"name": "limited_world_width_",
"name": "limited_world_width",
"type": "li32"
},
{
"name": "limited_world_length_",
"name": "limited_world_length",
"type": "li32"
},
{
@ -2887,6 +2874,10 @@
"name": "enchantment_seed",
"type": "zigzag32"
},
{
"name": "block_palette",
"type": "BlockPalette"
},
{
"name": "itemstates",
"type": "Itemstates"
@ -3390,7 +3381,7 @@
[
{
"name": "runtime_entity_id",
"type": "varint"
"type": "varint64"
},
{
"name": "attributes",
@ -3398,7 +3389,7 @@
},
{
"name": "tick",
"type": "varint"
"type": "varint64"
}
]
],

View file

@ -6,11 +6,11 @@ const ec_pem = require('ec-pem')
const SALT = '🧂'
const curve = 'secp384r1'
function Encrypt(client, options) {
function Encrypt(client, server, options) {
client.ecdhKeyPair = crypto.createECDH(curve)
client.ecdhKeyPair.generateKeys()
client.clientX509 = writeX509PublicKey(client.ecdhKeyPair.getPublicKey())
createClientChain(client)
function startClientboundEncryption(publicKey) {
console.warn('[encrypt] Pub key base64: ', publicKey)
@ -31,9 +31,11 @@ function Encrypt(client, options) {
const secretHash = crypto.createHash('sha256')
secretHash.update(SALT)
secretHash.update(client.sharedSecret)
console.log('---- SHARED SECRET', client.sharedSecret)
client.secretKeyBytes = secretHash.digest()
console.log('Hash', client.secretKeyBytes)
const x509 = writeX509PublicKey(alice.getPublicKey())
const token = JWT.sign({
salt: toBase64(SALT),
@ -51,30 +53,109 @@ function Encrypt(client, options) {
client.startEncryption(initial)
}
function startServerboundEncryption() {
function startServerboundEncryption(token) {
console.warn('Starting serverbound encryption', token)
const jwt = token?.token
if (!jwt) {
// TODO: allow connecting to servers without encryption
throw Error('Server did not return a valid JWT, cannot start encryption!')
}
// TODO: Should we do some JWT signature validation here? Seems pointless
const alice = client.ecdhKeyPair
const [header, payload, signature] = jwt.split('.').map(k => Buffer.from(k, 'base64'))
const head = JSON.parse(String(header))
const body = JSON.parse(String(payload))
const serverPublicKey = readX509PublicKey(head.x5u)
client.sharedSecret = alice.computeSecret(serverPublicKey)
console.log('------ SHARED SECRET', client.sharedSecret)
const salt = Buffer.from(body.salt, 'base64')
const secretHash = crypto.createHash('sha256')
secretHash.update(salt)
secretHash.update(client.sharedSecret)
client.secretKeyBytes = secretHash.digest()
console.log('Hash', client.secretKeyBytes)
const initial = client.secretKeyBytes.slice(0, 16)
client.startEncryption(initial)
// It works! First encrypted packet :)
client.write('client_to_server_handshake', {})
}
client.on('server.client_handshake', startClientboundEncryption)
}
function createClientChain(client) {
const alice = client.ecdhKeyPair
const alicePEM = ec_pem(alice, curve) // https://github.com/nodejs/node/issues/15116#issuecomment-384790125
const alicePEMPrivate = alicePEM.encodePrivateKey()
const x509 = writeX509PublicKey(alice.getPublicKey())
client.on('client.server_handshake', startServerboundEncryption)
const token = JWT.sign({
salt: toBase64(SALT),
signedToken: alice.getPublicKey('base64')
}, alicePEMPrivate, { algorithm: 'ES384', header: { x5u: x509 } })
client.createClientChain = (mojangKey) => {
mojangKey = mojangKey || require('./constants').PUBLIC_KEY
const alice = client.ecdhKeyPair
const alicePEM = ec_pem(alice, curve) // https://github.com/nodejs/node/issues/15116#issuecomment-384790125
const alicePEMPrivate = alicePEM.encodePrivateKey()
client.clientChain = token
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: require('./geom'),
"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) {
return Buffer.from(string).toString('base64')
}
}
function readX509PublicKey(key) {
var reader = new Ber.Reader(Buffer.from(key, "base64"));

View file

@ -4,6 +4,9 @@ const { createDeserializer, createSerializer } = require('./transforms/serialize
const { Encrypt } = require('./auth/encryption')
const auth = require('./client/auth')
const Options = require('./options')
const fs = require('fs')
const log = console.log
class Client extends Connection {
constructor(options) {
@ -41,7 +44,7 @@ class Client extends Connection {
if (this.raknet) return
this.raknet = new RakClient('localhost', 19132)
this.raknet = new RakClient('127.0.0.1', 19132)
await this.raknet.connect()
this.raknet.on('connecting', () => {
@ -61,16 +64,17 @@ class Client extends Connection {
}
sendLogin() {
this.createClientChain()
const chain = [
this.clientChain, // JWT we generated for auth
this.clientIdentityChain, // JWT we generated for auth
...this.accessToken // Mojang + Xbox JWT from auth
]
const encodedChain = JSON.stringify({ chain })
const skinChain = JSON.stringify({})
const bodyLength = skinChain.length + encodedChain.length + 8
const bodyLength = this.clientUserChain.length + encodedChain.length + 8
console.log('Auth chain', chain)
@ -78,7 +82,7 @@ class Client extends Connection {
protocol_version: this.options.version,
payload_size: bodyLength,
chain: encodedChain,
client_data: skinChain
client_data: this.clientUserChain
})
}
@ -90,19 +94,31 @@ class Client extends Connection {
this.emit('join')
}
onDisconnectRequest(packet) {
// 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)
}
readPacket(packet) {
console.log('packet', packet)
const des = this.server.deserializer.parsePacketBuffer(packet)
console.log('->', des)
const des = this.deserializer.parsePacketBuffer(packet)
console.info('->', des)
switch (des.data.name) {
case 'login':
console.log(des)
this.onLogin(des)
return
case 'client_to_server_handshake':
this.onHandshake()
case 'server_to_client_handshake':
this.emit('client.server_handshake', des.data.params)
break
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))
default:
console.log('ignoring, unhandled')
console.log('Sending to listeners')
}
this.emit(des.data.name, des.data.params)

View file

@ -63,8 +63,8 @@ async function authenticateDeviceCode (client, options) {
try {
const flow = new MsAuthFlow(options.username, options.profilesFolder, options.onMsaCode)
const chain = await flow.getMinecraftToken(client.clientChain)
const chain = await flow.getMinecraftToken(client.clientX509)
// console.log('Chain', chain)
await postAuthenticate(client, options, chain)
} catch (err) {
console.error(err)

View file

@ -109,7 +109,9 @@ class MsAuthFlow {
}
async getMinecraftToken (publicKey) {
if (await this.mca.verifyTokens()) {
// TODO: Fix cache, in order to do cache we also need to cache the ECDH keys so disable it
// is this even a good idea to cache?
if (await this.mca.verifyTokens() && false) {
debug('[mc] Using existing tokens')
return this.mca.getCachedAccessToken().chain
} else {
@ -118,7 +120,8 @@ class MsAuthFlow {
return await retry(async () => {
const xsts = await this.getXboxToken()
debug('[xbl] xsts data', xsts)
return this.mca.getAccessToken(publicKey, xsts).chain
const token = await this.mca.getAccessToken(publicKey, xsts)
return token.chain
}, () => { this.xbl.forceRefresh = true }, 2)
}
}

View file

@ -268,7 +268,7 @@ class MinecraftTokenManager {
}
async getAccessToken(clientPublicKey, xsts) {
debug('[mc] authing to minecraft', xsts)
debug('[mc] authing to minecraft', clientPublicKey, xsts)
const getFetchOptions = {
headers: {
'Content-Type': 'application/json',

View file

@ -1,10 +1,19 @@
process.env.DEBUG = 'minecraft-protocol raknet'
const { Client } = require('./client')
// console.log = () =>
async function test() {
const client = new Client({
hostname: '127.0.0.1',
port: 19132
})
client.once('resource_packs_info', (packet) => {
client.write('resource_pack_client_response', {
response_status: 'completed',
resourcepackids: []
})
})
}

View file

@ -8,7 +8,7 @@ const EncapsulatedPacket = require('@jsprismarine/raknet/protocol/encapsulated_p
class Connection extends EventEmitter {
startEncryption(iv) {
this.encryptionEnabled = true
console.log('Started encryption', this.sharedSecret, iv)
this.decrypt = cipher.createDecryptor(this, iv)
this.encrypt = cipher.createEncryptor(this, iv)
}
@ -74,7 +74,7 @@ class Connection extends EventEmitter {
}
onDecryptedPacket = (buf) => {
console.log('Decrypted', buf)
console.log('🟢 Decrypted', buf)
const stream = new BinaryStream(buf)
const packets = BatchPacket.getPackets(stream)

View file

@ -3,7 +3,7 @@ const minecraft = require('./minecraft')
module.exports = {
Read: {
UUID: ['native', (buffer, offset) => {
uuid: ['native', (buffer, offset) => {
return {
value: UUID.stringify(buffer.slice(offset, 16 + offset)),
size: 16
@ -18,7 +18,7 @@ module.exports = {
nbt: ['native', minecraft.nbt[0]]
},
Write: {
UUID: ['native', (value, buffer, offset) => {
uuid: ['native', (value, buffer, offset) => {
const buf = UUID.parse(value)
buf.copy(buffer, offset)
return offset + 16
@ -30,7 +30,7 @@ module.exports = {
nbt: ['native', minecraft.nbt[1]]
},
SizeOf: {
UUID: ['native', 16],
uuid: ['native', 16],
restBuffer: ['native', (value) => {
return value.length
}],

View file

@ -6,7 +6,7 @@ const fs = require('fs')
let server = new Server({
})
server.create('0.0.0.0', 19130)
server.create('0.0.0.0', 19132)
let ran = false

View file

@ -119,28 +119,46 @@ function createDecryptor(client, iv) {
const verifyChecksum = new Transform({ // verify checksum
transform(chunk, encoding, cb) {
console.log('Decryptor: checking checksum', chunk)
console.log('Decryptor: checking checksum', client.receiveCounter, chunk)
const packet = chunk.slice(0, chunk.length - 8);
const checksum = chunk.slice(chunk.length - 8);
const computedCheckSum = computeCheckSum(packet, client.receiveCounter, client.secretKeyBytes)
// console.log(computedCheckSum2, computedCheckSum3)
console.assert(checksum.toString("hex") == computedCheckSum.toString("hex"), 'checksum mismatch')
client.receiveCounter++
if (checksum.toString("hex") == computedCheckSum.toString("hex")) {
// if (checksum.toString("hex") == computedCheckSum.toString("hex")) {
this.push(packet)
} else {
throw Error(`Checksum mismatch ${checksum.toString("hex")} != ${computedCheckSum.toString("hex")}`)
}
// console.log('🔵 Decriphered', checksum)
// const inflated = Zlib.inflateRawSync(chunk, {
// chunkSize: 1024 * 1024 * 2
// })
// console.log('🔵 Inflated')
// client.onDecryptedPacket(inflated)
// } else {
// // console.log('🔴 Not OK')
// throw Error(`Checksum mismatch ${checksum.toString("hex")} != ${computedCheckSum.toString("hex")}`)
// }
cb()
}
})
const inflator = new Transform({
transform(chunk, enc, cb) {
Zlib.inflateRaw(chunk, { chunkSize: 1024 * 1024 * 2 }, (err, buf) => {
if (err) throw err
this.push(buf)
cb()
console.log('🔵 Inflating')
const inflated = Zlib.inflateRawSync(chunk, {
chunkSize: 1024 * 1024 * 2
})
console.log('🔵 Inflated')
this.push(inflated)
cb()
// Zlib.inflateRaw(chunk, { chunkSize: 1024 * 1024 * 2 }, (err, buf) => {
// console.log('🔵 INF')
// if (err) throw err
// this.push(buf)
// cb()
// })
}
})
@ -148,11 +166,24 @@ function createDecryptor(client, iv) {
.pipe(inflator)
// .pipe(Zlib.createInflateRaw({ chunkSize: 1024 * 1024 * 2 }))
.on('data', (...args) => client.onDecryptedPacket(...args))
.on('end', () => console.log('Decryptor: finish pipeline'))
// .on('end', () => console.log('Decryptor: finish pipeline'))
// Not sure why, but sending two packets to the decryption pipe before
// the other is completed breaks the checksum check.
// TODO: Refactor the logic here to be async so we can await a promise
// queue
let decQ = []
setInterval(() => {
if (decQ.length) {
let pak = decQ.shift()
console.log('🟡 DECRYPTING', pak)
client.decipher.write(pak)
}
}, 500)
return (blob) => {
client.decipher.write(blob)
decQ.push(blob)
// client.decipher.write(blob)
}
}