Skip to content

Commit

Permalink
Reorganise Minecraft network-facing code.
Browse files Browse the repository at this point in the history
  • Loading branch information
retrixe committed May 13, 2022
1 parent 6039a8f commit 316234b
Show file tree
Hide file tree
Showing 12 changed files with 144 additions and 123 deletions.
File renamed without changes.
File renamed without changes.
77 changes: 47 additions & 30 deletions src/minecraft/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,14 @@ import {
parseCompressedPacket,
parsePacket
} from './packet'
import { resolveHostname } from './utils'
import { readVarInt, writeVarInt } from './packetUtils'
import { authUrl, generateSharedSecret, mcHexDigest } from './onlineMode'

export declare interface ServerConnection {
on: ((event: 'packet', listener: (packet: Packet) => void) => this) &
((event: 'error', listener: (error: Error) => void) => this) &
((event: 'data', listener: (data: Buffer) => void) => this) &
((event: 'close', listener: () => void) => this) &
((event: string, listener: Function) => this)
}
import {
readVarInt,
writeVarInt,
resolveHostname,
generateSharedSecret,
mcHexDigest,
authUrl
} from './utils'

export interface ConnectionOptions {
host: string
Expand All @@ -38,6 +35,14 @@ export interface ConnectionOptions {
accessToken?: string
}

export declare interface ServerConnection {
on: ((event: 'packet', listener: (packet: Packet) => void) => this) &
((event: 'error', listener: (error: Error) => void) => this) &
((event: 'data', listener: (data: Buffer) => void) => this) &
((event: 'close', listener: () => void) => this) &
((event: string, listener: Function) => this)
}

export class ServerConnection extends events.EventEmitter {
bufferedData: Buffer = Buffer.from([])
compressionThreshold = -1
Expand Down Expand Up @@ -155,22 +160,10 @@ const initiateConnection = async (opts: ConnectionOptions) => {
conn.close()
} else if (packet.id === 0x01 && !conn.loggedIn) {
// https://wiki.vg/Protocol_Encryption
const [serverIdLen, serverIdLenLen] = readVarInt(packet.data)
// ASCII encoding of the server id string from Encryption Request
const serverId = packet.data.slice(
serverIdLenLen,
serverIdLen + serverIdLenLen
)
const data = packet.data.slice(serverIdLen + serverIdLenLen)
const [pkLen, pkLenLen] = readVarInt(data)
// Server's encoded public key from Encryption Request
const publicKey = data.slice(pkLenLen, pkLen + pkLenLen)
const verifyTokenData = data.slice(pkLen + pkLenLen)
const [, verifyTokenLengthLength] = readVarInt(verifyTokenData)
const verifyToken = verifyTokenData.slice(verifyTokenLengthLength)
const [serverId, publicKey, verifyToken] =
parseEncryptionRequestPacket(packet)
;(async () => {
// Generate random 16-byte shared secret.
const sharedSecret = await generateSharedSecret()
const sharedSecret = await generateSharedSecret() // Generate random 16-byte shared secret.
// Generate hash.
const sha1 = createHash('sha1')
sha1.update(serverId) // ASCII encoding of the server id string from Encryption Request
Expand Down Expand Up @@ -200,6 +193,12 @@ const initiateConnection = async (opts: ConnectionOptions) => {
const encryptedSharedSecret = publicEncrypt(ePrms, sharedSecret)
const encryptedVerifyToken = publicEncrypt(ePrms, verifyToken)
// Send encryption response packet.
// From this point forward, everything is encrypted, including the Login Success packet.
conn.aesDecipher = createDecipheriv(
'aes-128-cfb8',
sharedSecret,
sharedSecret
)
await conn.writePacket(
0x01,
concatPacketData([
Expand All @@ -209,10 +208,11 @@ const initiateConnection = async (opts: ConnectionOptions) => {
encryptedVerifyToken
])
)
// From this point forward, everything is encrypted, including the Login Success packet.
const ss = sharedSecret
conn.aesDecipher = createDecipheriv('aes-128-cfb8', ss, ss)
conn.aesCipher = createCipheriv('aes-128-cfb8', ss, ss)
conn.aesCipher = createCipheriv(
'aes-128-cfb8',
sharedSecret,
sharedSecret
)
})().catch(e => {
console.error(e)
conn.disconnectReason =
Expand Down Expand Up @@ -242,3 +242,20 @@ const initiateConnection = async (opts: ConnectionOptions) => {
}

export default initiateConnection

const parseEncryptionRequestPacket = (packet: Packet) => {
// ASCII encoding of the server id string
let data = packet.data
const [sidLen, sidLenLen] = readVarInt(data)
const serverId = data.slice(sidLenLen, sidLenLen + sidLen)
// Server's encoded public key
data = data.slice(sidLen + sidLenLen)
const [pkLen, pkLenLen] = readVarInt(data)
const publicKey = data.slice(pkLenLen, pkLen + pkLenLen)
// Server's randomly generated verify token
data = data.slice(pkLen + pkLenLen)
const [vtLen, vtLenLen] = readVarInt(data)
const verifyToken = data.slice(vtLenLen, vtLenLen + vtLen)

return [serverId, publicKey, verifyToken]
}
39 changes: 0 additions & 39 deletions src/minecraft/onlineMode.ts

This file was deleted.

3 changes: 1 addition & 2 deletions src/minecraft/packet.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import zlib from 'zlib'
import { Buffer } from 'buffer'
import { toggleEndian } from './utils'
import { encodeString, readVarInt, writeVarInt } from './packetUtils'
import { toggleEndian, encodeString, readVarInt, writeVarInt } from './utils'

export const makeBasePacket = (packetId: number, data: Buffer) => {
const finalData = Buffer.concat([writeVarInt(packetId), data])
Expand Down
46 changes: 0 additions & 46 deletions src/minecraft/packetUtils.ts

This file was deleted.

9 changes: 7 additions & 2 deletions src/minecraft/pingServer.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import net from 'react-native-tcp'
import { Buffer } from 'buffer'

import {
toggleEndian,
padBufferToLength,
resolveHostname,
readVarInt,
writeVarInt
} from './utils'
import { makeBasePacket, concatPacketData, parsePacket, Packet } from './packet'
import { toggleEndian, padBufferToLength, resolveHostname } from './utils'
import { parseValidJson, PlainTextChat } from './chatToJsx'
import { readVarInt, writeVarInt } from './packetUtils'

export interface LegacyPing {
ff: number
Expand Down
85 changes: 85 additions & 0 deletions src/minecraft/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Other crypto libraries: simple-crypto, sha256, aes-crypto and rsa-native
import { randomBytes } from 'react-native-randombytes'
import { Buffer } from 'buffer'

export const protocolMap = {
Expand Down Expand Up @@ -56,3 +58,86 @@ export const resolveHostname = async (
return [record[3], +record[2]]
} else return [hostname, port]
}

// Online-mode cryptography utilities.
export const authUrl = 'https://sessionserver.mojang.com/session/minecraft/join'

export const generateSharedSecret = async () =>
await new Promise<Buffer>((resolve, reject) => {
randomBytes(16, (err, buf) => {
if (err) reject(err)
else resolve(buf)
})
})

// Credits for following 2 functions: https://gist.github.com/andrewrk/4425843
export function mcHexDigest(hash: Buffer) {
// check for negative hashes
const negative = hash.readInt8(0) < 0
if (negative) performTwosCompliment(hash)
let digest = hash.toString('hex')
// trim leading zeroes
digest = digest.replace(/^0+/g, '')
if (negative) digest = '-' + digest
return digest
}

function performTwosCompliment(buffer: Buffer) {
let carry = true
let newByte, value
for (let i = buffer.length - 1; i >= 0; --i) {
value = buffer.readUInt8(i)
newByte = ~value & 0xff
if (carry) {
carry = newByte === 0xff
buffer.writeUInt8(carry ? 0 : newByte + 1, i)
} else {
buffer.writeUInt8(newByte, i)
}
}
}

// Minecraft data type utilities.
export const encodeString = (str: string): Buffer => {
const buffer = Buffer.from(str, 'utf8')
return Buffer.concat([writeVarInt(buffer.byteLength), buffer])
}

// Adapted from https://wiki.vg/Protocol which is licensed under CC-BY-SA-3.0.
// VarLong is unsupported as JavaScript cannot fit VarLong except with BigInt.
// BigInt is unsupported in React Native at the moment.
export const readVarInt = (
buffer: Buffer,
offset: number = 0,
varlong: boolean = false
): [number, number] => {
let numRead = 0
let result = 0
let read: number
do {
read = buffer.readUInt8(offset + numRead)
const value = read & 0b01111111
result |= value << (7 * numRead)
numRead++
if (!varlong && numRead > 5) {
throw new Error('VarInt is too big')
} else if (varlong && numRead > 10) {
throw new Error('VarLong is too big')
}
} while ((read & 0b10000000) !== 0)
return [result, numRead]
}

export const writeVarInt = (value: number): Buffer => {
let result = Buffer.alloc(0)
do {
let temp = value & 0b01111111
// Note: >>> means that the sign bit is shifted with the rest of the number rather than being left alone
value >>>= 7
if (value !== 0) {
temp |= 0b10000000
}
result = Buffer.concat([result, Buffer.from([temp])])
} while (value !== 0)
return result
}
2 changes: 1 addition & 1 deletion src/screens/ChatScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
ClickEvent,
ColorMap
} from '../minecraft/chatToJsx'
import { readVarInt, writeVarInt } from '../minecraft/packetUtils'
import { readVarInt, writeVarInt } from '../minecraft/utils'
import { concatPacketData } from '../minecraft/packet'
import TextField from '../components/TextField'
import Text from '../components/Text'
Expand Down
2 changes: 1 addition & 1 deletion src/screens/accounts/AccountScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Dialog, { dialogStyles } from '../../components/Dialog'
import useDarkMode from '../../context/useDarkMode'
import UsersContext from '../../context/accountsContext'
import ElevatedView from '../../components/ElevatedView'
import { invalidate } from '../../minecraft/yggdrasil'
import { invalidate } from '../../minecraft/authentication/yggdrasil'

// LOW-TODO: Reload to update account info for online mode using /refresh.
// Also, to reload all the skin images?
Expand Down
2 changes: 1 addition & 1 deletion src/screens/accounts/AddAccountDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Dialog, { dialogStyles } from '../../components/Dialog'
import TextField from '../../components/TextField'
import useDarkMode from '../../context/useDarkMode'
import UsersContext from '../../context/accountsContext'
import { authenticate } from '../../minecraft/yggdrasil'
import { authenticate } from '../../minecraft/authentication/yggdrasil'

const AddAccountDialog = ({
open,
Expand Down
2 changes: 1 addition & 1 deletion src/screens/accounts/MicrosoftLogin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
getXstsTokenAndUserHash,
authenticateWithXsts,
getGameProfile
} from '../../minecraft/microsoft'
} from '../../minecraft/authentication/microsoft'

const MicrosoftLogin = ({ close }: { close: () => void }) => {
const darkMode = useDarkMode()
Expand Down

0 comments on commit 316234b

Please sign in to comment.