From 316234bf60c8dd5d25ad9cbe56119084798ee3f3 Mon Sep 17 00:00:00 2001 From: Ibrahim Ansari Date: Sat, 14 May 2022 00:59:50 +0530 Subject: [PATCH] Reorganise Minecraft network-facing code. --- .../{ => authentication}/microsoft.ts | 0 .../{ => authentication}/yggdrasil.ts | 0 src/minecraft/connection.ts | 77 ++++++++++------- src/minecraft/onlineMode.ts | 39 --------- src/minecraft/packet.ts | 3 +- src/minecraft/packetUtils.ts | 46 ---------- src/minecraft/pingServer.ts | 9 +- src/minecraft/utils.ts | 85 +++++++++++++++++++ src/screens/ChatScreen.tsx | 2 +- src/screens/accounts/AccountScreen.tsx | 2 +- src/screens/accounts/AddAccountDialog.tsx | 2 +- src/screens/accounts/MicrosoftLogin.tsx | 2 +- 12 files changed, 144 insertions(+), 123 deletions(-) rename src/minecraft/{ => authentication}/microsoft.ts (100%) rename src/minecraft/{ => authentication}/yggdrasil.ts (100%) delete mode 100644 src/minecraft/onlineMode.ts delete mode 100644 src/minecraft/packetUtils.ts diff --git a/src/minecraft/microsoft.ts b/src/minecraft/authentication/microsoft.ts similarity index 100% rename from src/minecraft/microsoft.ts rename to src/minecraft/authentication/microsoft.ts diff --git a/src/minecraft/yggdrasil.ts b/src/minecraft/authentication/yggdrasil.ts similarity index 100% rename from src/minecraft/yggdrasil.ts rename to src/minecraft/authentication/yggdrasil.ts diff --git a/src/minecraft/connection.ts b/src/minecraft/connection.ts index 3e95333..259828a 100644 --- a/src/minecraft/connection.ts +++ b/src/minecraft/connection.ts @@ -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 @@ -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 @@ -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 @@ -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([ @@ -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 = @@ -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] +} diff --git a/src/minecraft/onlineMode.ts b/src/minecraft/onlineMode.ts deleted file mode 100644 index 7fd45a6..0000000 --- a/src/minecraft/onlineMode.ts +++ /dev/null @@ -1,39 +0,0 @@ -// Other crypto libraries: simple-crypto, sha256, aes-crypto and rsa-native -import { randomBytes } from 'react-native-randombytes' - -export const authUrl = 'https://sessionserver.mojang.com/session/minecraft/join' - -export const generateSharedSecret = async () => - await new Promise((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) - } - } -} diff --git a/src/minecraft/packet.ts b/src/minecraft/packet.ts index 595d5ae..667be31 100644 --- a/src/minecraft/packet.ts +++ b/src/minecraft/packet.ts @@ -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]) diff --git a/src/minecraft/packetUtils.ts b/src/minecraft/packetUtils.ts deleted file mode 100644 index 00a5816..0000000 --- a/src/minecraft/packetUtils.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Buffer } from '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 -} - -// Code from here onwards is my own. -export const encodeString = (str: string): Buffer => { - const buffer = Buffer.from(str, 'utf8') - return Buffer.concat([writeVarInt(buffer.byteLength), buffer]) -} diff --git a/src/minecraft/pingServer.ts b/src/minecraft/pingServer.ts index 7efb24e..69bcbcd 100644 --- a/src/minecraft/pingServer.ts +++ b/src/minecraft/pingServer.ts @@ -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 diff --git a/src/minecraft/utils.ts b/src/minecraft/utils.ts index 2463fac..7e07ee7 100644 --- a/src/minecraft/utils.ts +++ b/src/minecraft/utils.ts @@ -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 = { @@ -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((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 +} diff --git a/src/screens/ChatScreen.tsx b/src/screens/ChatScreen.tsx index d1f438d..294cb8a 100644 --- a/src/screens/ChatScreen.tsx +++ b/src/screens/ChatScreen.tsx @@ -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' diff --git a/src/screens/accounts/AccountScreen.tsx b/src/screens/accounts/AccountScreen.tsx index 3e87d34..3c61129 100644 --- a/src/screens/accounts/AccountScreen.tsx +++ b/src/screens/accounts/AccountScreen.tsx @@ -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? diff --git a/src/screens/accounts/AddAccountDialog.tsx b/src/screens/accounts/AddAccountDialog.tsx index ee77517..ba1c7fd 100644 --- a/src/screens/accounts/AddAccountDialog.tsx +++ b/src/screens/accounts/AddAccountDialog.tsx @@ -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, diff --git a/src/screens/accounts/MicrosoftLogin.tsx b/src/screens/accounts/MicrosoftLogin.tsx index b1926a1..767f7e5 100644 --- a/src/screens/accounts/MicrosoftLogin.tsx +++ b/src/screens/accounts/MicrosoftLogin.tsx @@ -13,7 +13,7 @@ import { getXstsTokenAndUserHash, authenticateWithXsts, getGameProfile -} from '../../minecraft/microsoft' +} from '../../minecraft/authentication/microsoft' const MicrosoftLogin = ({ close }: { close: () => void }) => { const darkMode = useDarkMode()