Merge pull request #85 from extremeheat/tpf

This commit is contained in:
extremeheat 2021-05-15 00:04:16 -04:00 committed by GitHub
commit f644595b4b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 219 additions and 222 deletions

View file

@ -212,7 +212,7 @@
}
]
],
"BlockPalette": [
"BlockProperties": [
"array",
{
"countType": "varint",
@ -4287,8 +4287,8 @@
"type": "zigzag32"
},
{
"name": "block_palette",
"type": "BlockPalette"
"name": "block_properties",
"type": "BlockProperties"
},
{
"name": "itemstates",
@ -5967,6 +5967,31 @@
{
"compareTo": "type",
"fields": {
"show_bar": [
"container",
[
{
"name": "title",
"type": "string"
},
{
"name": "progress",
"type": "lf32"
},
{
"name": "screen_darkening",
"type": "li16"
},
{
"name": "color",
"type": "varint"
},
{
"name": "overlay",
"type": "varint"
}
]
],
"register_player": [
"container",
[
@ -5985,16 +6010,21 @@
}
]
],
"show": [
"set_bar_progress": [
"container",
[
{
"name": "progress",
"type": "lf32"
}
]
],
"set_bar_title": [
"container",
[
{
"name": "title",
"type": "string"
},
{
"name": "bar_progress",
"type": "lf32"
}
]
],
@ -6002,8 +6032,16 @@
"container",
[
{
"name": "darkness_factor",
"name": "screen_darkening",
"type": "li16"
},
{
"name": "color",
"type": "varint"
},
{
"name": "overlay",
"type": "varint"
}
]
],
@ -6019,24 +6057,6 @@
"type": "varint"
}
]
],
"set_bar_progress": [
"container",
[
{
"name": "bar_progress",
"type": "lf32"
}
]
],
"set_bar_title": [
"container",
[
{
"name": "title",
"type": "string"
}
]
]
},
"default": "void"

View file

@ -21,30 +21,30 @@ nbt: native
# load the packet map file
!import: packet_map.yml
#todo: docs
!StartDocs: Packets
# # Login Sequence
# The login process is as follows:
#
# C→S: [Login](#packet_login)
# S→C: [Server To Client Handshake](#packet_server_to_client_handshake)
# C→S: [Client To Server Handshake](#packet_client_to_server_handshake)
# S→C: [Play Status (Login success)](#packet_play_status)
# To spawn, the following packets should be sent, in order, after the ones above:
# * C→S: [Login](#packet_login)
# * S→C: [Server To Client Handshake](#packet_server_to_client_handshake)
# * C→S: [Client To Server Handshake](#packet_client_to_server_handshake)
# * S→C: [Play Status (Login success)](#packet_play_status)
# * To spawn, the following packets should be sent, in order, after the ones above:
# * S→C: [Resource Packs Info](#packet_resource_packs_info)
# * C→S: [Resource Pack Client Response](#packet_resource_pack_client_response)
# * S→C: [Resource Pack Stack](#packet_resource_pack_stack)
# * C→S: [Resource Pack Client Response](#packet_resource_pack_client_response)
# * S→C: [Start Game](#packet_start_game)
# * S→C: [Creative Content](#packet_creative_content)
# * S→C: [Biome Definition List](#packet_biome_definition_list)
# * S→C: [Chunks](#packet_level_chunk)
# * S→C: [Play Status (Player spawn)](#packet_play_status)
#
# S→C: [Resource Packs Info](#packet_resource_packs_info)
# C→S: [Resource Pack Client Response](#packet_resource_pack_client_response)
# S→C: [Resource Pack Stack](#packet_resource_pack_stack)
# C→S: [Resource Pack Client Response](#packet_resource_pack_client_response)
# S→C: [Start Game](#packet_start_game)
# S→C: [Creative Content](#packet_creative_content)
# S→C: [Biome Definition List](#packet_biome_definition_list)
# S→C: [Chunks](#packet_level_chunk)
# S→C: [Play Status (Player spawn)](#packet_play_status)
# If there are no resource packs being sent, a Resource Pack Stack can be sent directly
# after Resource Packs Info to avoid the client responses.
#
# ===
packet_login:
!id: 0x01
@ -366,15 +366,17 @@ packet_start_game:
# results both client- and server-side.
enchantment_seed: zigzag32
## 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
# BlockProperties is a list of all the custom blocks registered on the server.
block_properties: BlockProperties
# 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
# A unique ID specifying the multi-player session of the player.
# A random UUID should be filled out for this field.
multiplayer_correlation_id: string
# ServerAuthoritativeInventory specifies if the server authoritative inventory system is enabled. This
# is a new system introduced in 1.16. Backwards compatibility with the inventory transactions has to
# some extent been preserved, but will eventually be removed.
server_authoritative_inventory: bool
@ -1387,20 +1389,41 @@ packet_boss_event:
# S2C: Not implemented :( Intended to alter bar appearance, but these currently produce no effect on client-side whatsoever.
7: texture
_: type?
if register_player or unregister_player:
player_id: zigzag64
if show:
if show_bar:
# BossBarTitle is the title shown above the boss bar. It currently does not function, and instead uses
# the name tag of the boss entity at all times. It is only set if the EventType is BossEventShow or
# BossEventTitle.
title: string
# HealthPercentage is the percentage of health that is shown in the boss bar. It currently does not
# function, and instead uses the health percentage of the boss entity at all times. It is only set if the
# EventType is BossEventShow or BossEventHealthPercentage.
progress: lf32
# ScreenDarkening currently seems not to do anything.
screen_darkening: li16
# Colour is the colour of the boss bar that is shown when a player is subscribed. It currently does not
# function. It is only set if the EventType is BossEventShow, BossEventAppearanceProperties or
# BossEventTexture.
# Format is ARGB
color: varint
# Overlay is the overlay of the boss bar that is shown on top of the boss bar when a player is
# subscribed. It currently does not function. It is only set if the EventType is BossEventShow,
# BossEventAppearanceProperties or BossEventTexture.
overlay: varint
if register_player or unregister_player:
# PlayerUniqueID is the unique ID of the player that is registered to or unregistered from the boss
# fight. It is set if EventType is either BossEventRegisterPlayer or BossEventUnregisterPlayer.
player_id: zigzag64
if set_bar_progress:
progress: lf32
if set_bar_title:
title: string
bar_progress: lf32
if update_properties:
darkness_factor: li16
screen_darkening: li16
color: varint
overlay: varint
if texture:
color: varint
overlay: varint
if set_bar_progress:
bar_progress: lf32
if set_bar_title:
title: string
packet_show_credits:
!id: 0x4b
@ -1418,8 +1441,9 @@ packet_available_commands:
# The length of the enums for all the command paramaters in this packet
values_len: varint
# Not read from stream: instead calculated from the `values_len` field
# If the values_len < 0xff => byte
# If the values_len < 0xffff => short
#
# If the values_len < 0xff => byte,
# If the values_len < 0xffff => short,
# If the values_len < 0xffffff => int
_enum_type: '["enum_size_based_on_values_len"]'
# Here all the enum values for all of the possible commands are stored to one array palette

View file

@ -1,4 +1,4 @@
# !StartDocs: Types
!StartDocs: Types
BehaviourPackInfos: []li16
uuid: string
@ -66,7 +66,7 @@ Blob:
# Payload in it.
payload: ByteArray
BlockPalette: []varint
BlockProperties: []varint
name: string
state: nbt
@ -75,7 +75,7 @@ Itemstates: []varint
runtime_id: li16
component_based: bool
# Start of item crap ...
ItemExtraDataWithBlockingTick:
has_nbt: lu16 =>
@ -102,7 +102,7 @@ ItemExtraDataWithoutBlockingTick:
can_place_on: ShortArray[]li32
can_destroy: ShortArray[]li32
# Same as below but without a "networkStackID" boolean ...
# Same as below but without a "networkStackID" boolean
ItemLegacy:
network_id: zigzag32
_: network_id?
@ -140,8 +140,6 @@ Item:
if 355: '["encapsulated", { "lengthType": "varint", "type": "ItemExtraDataWithBlockingTick" }]'
default: '["encapsulated", { "lengthType": "varint", "type": "ItemExtraDataWithoutBlockingTick" }]'
# end of item crap
vec3i:
x: zigzag32
y: zigzag32
@ -632,8 +630,7 @@ Recipes: []varint
recipe_id: string
width: zigzag32
height: zigzag32
# todo: can this become
# RecipeIngredient[$height][$width] or RecipeIngredient[]$height[]$width ?
# 2D input array, size of width*height
input: []$width
_: RecipeIngredient[]$height
output: ItemLegacy[]varint

View file

@ -23,11 +23,9 @@
"dependencies": {
"@azure/msal-node": "^1.0.0-beta.6",
"@xboxreplay/xboxlive-auth": "^3.3.3",
"asn1": "^0.2.4",
"debug": "^4.3.1",
"ec-pem": "^0.18.0",
"jsonwebtoken": "^8.5.1",
"jsp-raknet": "^2.0.0",
"jsp-raknet": "^2.1.0",
"minecraft-folder-path": "^1.1.0",
"node-fetch": "^2.6.1",
"prismarine-nbt": "^1.5.0",

View file

@ -1,122 +0,0 @@
const { Ber } = require('asn1')
const { ClientStatus } = require('../connection')
const JWT = require('jsonwebtoken')
const crypto = require('crypto')
const ecPem = require('ec-pem')
const debug = require('debug')('minecraft-protocol')
const SALT = '🧂'
const curve = 'secp384r1'
function Encrypt (client, server, options) {
client.ecdhKeyPair = crypto.createECDH(curve)
client.ecdhKeyPair.generateKeys()
client.clientX509 = writeX509PublicKey(client.ecdhKeyPair.getPublicKey())
function startClientboundEncryption (publicKey) {
debug('[encrypt] Client pub key base64: ', publicKey)
const pubKeyBuf = readX509PublicKey(publicKey.key)
const alice = client.ecdhKeyPair
const alicePEM = ecPem(alice, curve) // https://github.com/nodejs/node/issues/15116#issuecomment-384790125
const alicePEMPrivate = alicePEM.encodePrivateKey()
// Shared secret from the client's public key + our private key
client.sharedSecret = alice.computeSecret(pubKeyBuf)
// Secret hash we use for packet encryption:
// 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 secretHash = crypto.createHash('sha256')
secretHash.update(SALT)
secretHash.update(client.sharedSecret)
// console.log('[encrypt] Shared secret', client.sharedSecret)
client.secretKeyBytes = secretHash.digest()
// console.log('[encrypt] Shared hash', client.secretKeyBytes)
const x509 = writeX509PublicKey(alice.getPublicKey())
const token = JWT.sign({
salt: toBase64(SALT),
signedToken: alice.getPublicKey('base64')
}, alicePEMPrivate, { algorithm: 'ES384', header: { x5u: x509 } })
client.write('server_to_client_handshake', {
token: token
})
// The encryption scheme is AES/CFB8/NoPadding with the
// secret key being the result of the sha256 above and
// the IV being the first 16 bytes of this secret key.
const initial = client.secretKeyBytes.slice(0, 16)
client.startEncryption(initial)
}
function startServerboundEncryption (token) {
debug('[encrypt] Starting serverbound encryption', token)
const jwt = token?.token
if (!jwt) {
// TODO: allow connecting to servers without encryption
throw Error('Server did not return a valid JWT, cannot start encryption!')
}
// TODO: Should we do some JWT signature validation here? Seems pointless
const alice = client.ecdhKeyPair
const [header, payload] = jwt.split('.').map(k => Buffer.from(k, 'base64'))
const head = JSON.parse(String(header))
const body = JSON.parse(String(payload))
const serverPublicKey = readX509PublicKey(head.x5u)
client.sharedSecret = alice.computeSecret(serverPublicKey)
// console.log('[encrypt] Shared secret', client.sharedSecret)
const salt = Buffer.from(body.salt, 'base64')
const secretHash = crypto.createHash('sha256')
secretHash.update(salt)
secretHash.update(client.sharedSecret)
client.secretKeyBytes = secretHash.digest()
// console.log('[encrypt] Shared hash', client.secretKeyBytes)
const initial = client.secretKeyBytes.slice(0, 16)
client.startEncryption(initial)
// It works! First encrypted packet :)
client.write('client_to_server_handshake', {})
this.emit('join')
client.status = ClientStatus.Initializing
}
client.on('server.client_handshake', startClientboundEncryption)
client.on('client.server_handshake', startServerboundEncryption)
}
function toBase64 (string) {
return Buffer.from(string).toString('base64')
}
function readX509PublicKey (key) {
const reader = new Ber.Reader(Buffer.from(key, 'base64'))
reader.readSequence()
reader.readSequence()
reader.readOID() // Hey, I'm an elliptic curve
reader.readOID() // This contains the curve type, could be useful
return Buffer.from(reader.readString(Ber.BitString, true)).slice(1)
}
function writeX509PublicKey (key) {
const writer = new Ber.Writer()
writer.startSequence()
writer.startSequence()
writer.writeOID('1.2.840.10045.2.1')
writer.writeOID('1.3.132.0.34')
writer.endSequence()
writer.writeBuffer(Buffer.concat([Buffer.from([0x00]), key]), Ber.BitString)
writer.endSequence()
return writer.buffer.toString('base64')
}
module.exports = {
readX509PublicKey,
writeX509PublicKey,
Encrypt
}

View file

@ -7,9 +7,9 @@ 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 { KeyExchange } = require('./handshake/keyExchange')
const Login = require('./handshake/login')
const LoginVerify = require('./handshake/loginVerify')
const debugging = false
@ -25,7 +25,7 @@ class Client extends Connection {
this.serializer = createSerializer(this.options.version)
this.deserializer = createDeserializer(this.options.version)
Encrypt(this, null, this.options)
KeyExchange(this, null, this.options)
Login(this, null, this.options)
LoginVerify(this, null, this.options)

View file

@ -0,0 +1,94 @@
const { ClientStatus } = require('../connection')
const JWT = require('jsonwebtoken')
const crypto = require('crypto')
const debug = require('debug')('minecraft-protocol')
const SALT = '🧂'
const curve = 'secp384r1'
const pem = { format: 'pem', type: 'sec1' }
const der = { format: 'der', type: 'spki' }
function KeyExchange (client, server, options) {
// Generate a key pair at program start up
client.ecdhKeyPair = crypto.generateKeyPairSync('ec', { namedCurve: curve })
client.publicKeyDER = client.ecdhKeyPair.publicKey.export(der)
client.privateKeyPEM = client.ecdhKeyPair.privateKey.export(pem)
console.log(client.publicKeyPEM)
client.clientX509 = client.publicKeyDER.toString('base64')
function startClientboundEncryption (publicKey) {
debug('[encrypt] Client pub key base64: ', publicKey)
const pubKeyDer = crypto.createPublicKey({ key: Buffer.from(publicKey.key, 'base64'), ...der })
// Shared secret from the client's public key + our private key
client.sharedSecret = crypto.diffieHellman({ privateKey: client.ecdhKeyPair.privateKey, publicKey: pubKeyDer })
// Secret hash we use for packet encryption:
// From the public key of the remote and the private key
// 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 secretHash = crypto.createHash('sha256')
secretHash.update(SALT)
secretHash.update(client.sharedSecret)
client.secretKeyBytes = secretHash.digest()
const token = JWT.sign({
salt: toBase64(SALT),
signedToken: client.clientX509
}, client.ecdhKeyPair.privateKey, { algorithm: 'ES384', header: { x5u: client.clientX509 } })
client.write('server_to_client_handshake', { token })
// The encryption scheme is AES/CFB8/NoPadding with the
// secret key being the result of the sha256 above and
// the IV being the first 16 bytes of this secret key.
const initial = client.secretKeyBytes.slice(0, 16)
client.startEncryption(initial)
}
function startServerboundEncryption (token) {
debug('[encrypt] Starting serverbound encryption', token)
const jwt = token?.token
if (!jwt) {
throw Error('Server did not return a valid JWT, cannot start encryption!')
}
// No verification here, not needed
const [header, payload] = jwt.split('.').map(k => Buffer.from(k, 'base64'))
const head = JSON.parse(String(header))
const body = JSON.parse(String(payload))
const pubKeyDer = crypto.createPublicKey({ key: Buffer.from(head.x5u, 'base64'), ...der })
// Shared secret from the client's public key + our private key
client.sharedSecret = crypto.diffieHellman({ privateKey: client.ecdhKeyPair.privateKey, publicKey: pubKeyDer })
const salt = Buffer.from(body.salt, 'base64')
const secretHash = crypto.createHash('sha256')
secretHash.update(salt)
secretHash.update(client.sharedSecret)
client.secretKeyBytes = secretHash.digest()
const iv = client.secretKeyBytes.slice(0, 16)
client.startEncryption(iv)
// It works! First encrypted packet :)
client.write('client_to_server_handshake', {})
this.emit('join')
client.status = ClientStatus.Initializing
}
client.on('server.client_handshake', startClientboundEncryption)
client.on('client.server_handshake', startServerboundEncryption)
}
function toBase64 (string) {
return Buffer.from(string).toString('base64')
}
module.exports = { KeyExchange }

View file

@ -1,18 +1,15 @@
const fs = require('fs')
const JWT = require('jsonwebtoken')
const DataProvider = require('../../data/provider')
const ecPem = require('ec-pem')
const curve = 'secp384r1'
const { nextUUID } = require('../datatypes/util')
const { PUBLIC_KEY } = require('./constants')
const algorithm = 'ES384'
module.exports = (client, server, options) => {
const skinGeom = fs.readFileSync(DataProvider(options.protocolVersion).getPath('skin_geom.txt'), 'utf-8')
client.createClientChain = (mojangKey, offline) => {
mojangKey = mojangKey || require('./constants').PUBLIC_KEY
const alice = client.ecdhKeyPair
const alicePEM = ecPem(alice, curve) // https://github.com/nodejs/node/issues/15116#issuecomment-384790125
const alicePEMPrivate = alicePEM.encodePrivateKey()
const privateKey = client.ecdhKeyPair.privateKey
let token
if (offline) {
@ -25,16 +22,16 @@ module.exports = (client, server, options) => {
certificateAuthority: true,
identityPublicKey: client.clientX509
}
token = JWT.sign(payload, alicePEMPrivate, { algorithm: 'ES384', notBefore: 0, issuer: 'self', expiresIn: 60 * 60, header: { x5u: client.clientX509 } })
token = JWT.sign(payload, privateKey, { algorithm, notBefore: 0, issuer: 'self', expiresIn: 60 * 60, header: { x5u: client.clientX509 } })
} else {
token = JWT.sign({
identityPublicKey: mojangKey,
identityPublicKey: mojangKey || PUBLIC_KEY,
certificateAuthority: true
}, alicePEMPrivate, { algorithm: 'ES384', header: { x5u: client.clientX509 } })
}, privateKey, { algorithm, header: { x5u: client.clientX509 } })
}
client.clientIdentityChain = token
client.createClientUserChain(alicePEMPrivate)
client.createClientUserChain(privateKey)
}
client.createClientUserChain = (privateKey) => {
@ -82,7 +79,6 @@ module.exports = (client, server, options) => {
const customPayload = options.skinData || {}
payload = { ...payload, ...customPayload }
client.clientUserChain = JWT.sign(payload, privateKey,
{ algorithm: 'ES384', header: { x5u: client.clientX509 } })
client.clientUserChain = JWT.sign(payload, privateKey, { algorithm, header: { x5u: client.clientX509 } })
}
}

View file

@ -1,11 +1,14 @@
const JWT = require('jsonwebtoken')
const constants = require('./constants')
const debug = require('debug')('minecraft-protocol')
const crypto = require('crypto')
module.exports = (client, server, options) => {
// Refer to the docs:
// https://web.archive.org/web/20180917171505if_/https://confluence.yawk.at/display/PEPROTOCOL/Game+Packets#GamePackets-Login
const getDER = b64 => crypto.createPublicKey({ key: Buffer.from(b64, 'base64'), format: 'der', type: 'spki' })
function verifyAuth (chain) {
let data = {}
@ -16,9 +19,9 @@ module.exports = (client, server, options) => {
// signed by Mojang by checking the x509 public key in the JWT headers
let didVerify = false
let pubKey = mcPubKeyToPem(getX5U(chain[0])) // the first one is client signed, allow it
let pubKey = getDER(getX5U(chain[0])) // the first one is client signed, allow it
let finalKey = null
// console.log(pubKey)
for (const token of chain) {
const decoded = JWT.verify(token, pubKey, { algorithms: ['ES384'] })
// console.log('Decoded', decoded)
@ -30,7 +33,7 @@ module.exports = (client, server, options) => {
debug('Verified client with mojang key', x5u)
}
pubKey = decoded.identityPublicKey ? mcPubKeyToPem(decoded.identityPublicKey) : x5u
pubKey = decoded.identityPublicKey ? getDER(decoded.identityPublicKey) : x5u
finalKey = decoded.identityPublicKey || finalKey // non pem
data = { ...data, ...decoded }
}
@ -44,7 +47,7 @@ module.exports = (client, server, options) => {
}
function verifySkin (publicKey, token) {
const pubKey = mcPubKeyToPem(publicKey)
const pubKey = getDER(publicKey)
const decoded = JWT.verify(token, pubKey, { algorithms: ['ES384'] })
return decoded
}
@ -71,16 +74,3 @@ function getX5U (token) {
const hjson = JSON.parse(hdec)
return hjson.x5u
}
function mcPubKeyToPem (mcPubKeyBuffer) {
if (mcPubKeyBuffer[0] === '-') return mcPubKeyBuffer
let pem = '-----BEGIN PUBLIC KEY-----\n'
let base64PubKey = mcPubKeyBuffer.toString('base64')
const maxLineLength = 65
while (base64PubKey.length > 0) {
pem += base64PubKey.substring(0, maxLineLength) + '\n'
base64PubKey = base64PubKey.substring(maxLineLength)
}
pem += '-----END PUBLIC KEY-----\n'
return pem
}

View file

@ -3,9 +3,9 @@ const fs = require('fs')
const Options = require('./options')
const debug = require('debug')('minecraft-protocol')
const { Encrypt } = require('./auth/encryption')
const Login = require('./auth/login')
const LoginVerify = require('./auth/loginVerify')
const { KeyExchange } = require('./handshake/keyExchange')
const Login = require('./handshake/login')
const LoginVerify = require('./handshake/loginVerify')
class Player extends Connection {
constructor (server, connection) {
@ -16,7 +16,7 @@ class Player extends Connection {
this.connection = connection
this.options = server.options
Encrypt(this, server, server.options)
KeyExchange(this, server, server.options)
Login(this, server, server.options)
LoginVerify(this, server, server.options)