Merge pull request #85 from extremeheat/tpf
This commit is contained in:
commit
f644595b4b
11 changed files with 219 additions and 222 deletions
74
data/1.16.220/protocol.json
generated
74
data/1.16.220/protocol.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
94
src/handshake/keyExchange.js
Normal file
94
src/handshake/keyExchange.js
Normal 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 }
|
||||
|
|
@ -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 } })
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue