From 4abc04e9f6976c8400c012512e0a855ae30cbba9 Mon Sep 17 00:00:00 2001 From: Tau Date: Wed, 11 Mar 2020 17:29:48 -0400 Subject: [PATCH] idz: Port userdb/ onto common/ --- src/idz/userdb/bigint.ts | 33 --------- src/idz/userdb/decoder/index.ts | 120 ++++++++++++-------------------- src/idz/userdb/encoder/index.ts | 31 +++------ src/idz/userdb/index.ts | 22 +++--- src/idz/userdb/setup.ts | 75 -------------------- 5 files changed, 67 insertions(+), 214 deletions(-) delete mode 100644 src/idz/userdb/bigint.ts delete mode 100644 src/idz/userdb/setup.ts diff --git a/src/idz/userdb/bigint.ts b/src/idz/userdb/bigint.ts deleted file mode 100644 index 1d85a5f..0000000 --- a/src/idz/userdb/bigint.ts +++ /dev/null @@ -1,33 +0,0 @@ -export function byteString(n: bigint, length: number) { - const result = Buffer.alloc(length); - - for (let i = 0; i < length; i++) { - const shift = 8n * BigInt(i); - const byte = (n >> shift) & 0xffn; - - result[i] = Number(byte); - } - - return result; -} - -// i pick the one implementation language that doesn't have this built in - -export function modPow(b: bigint, e: bigint, m: bigint) { - // https://en.wikipedia.org/wiki/Modular_exponentiation#Right-to-left_binary_method - - let result = 1n; - - b = b % m; - - while (e > 0n) { - if ((e & 1n) === 1n) { - result = (result * b) % m; - } - - e = e >> 1n; - b = (b * b) % m; - } - - return result; -} diff --git a/src/idz/userdb/decoder/index.ts b/src/idz/userdb/decoder/index.ts index 4d539d9..2baea8b 100644 --- a/src/idz/userdb/decoder/index.ts +++ b/src/idz/userdb/decoder/index.ts @@ -1,5 +1,4 @@ import logger from "debug"; -import { Transform } from "stream"; import { checkTeamName } from "./checkTeamName"; import { createProfile } from "./createProfile"; @@ -49,6 +48,8 @@ import { updateTeamPoints } from "./updateTeamPoints"; import { updateUiReport } from "./updateUiReport"; import { updateUserLog } from "./updateUserLog"; import { lockProfileExtend } from "./lockProfileExtend"; +import { BLOCK_SIZE } from "../../common"; +import { ByteStream } from "../../../util/stream"; const debug = logger("app:idz:userdb:decoder"); @@ -119,88 +120,53 @@ for (const fn of funcList) { msgLengths.set(fn.msgCode, fn.msgLen); } -function readHeader(buf: Buffer) { - return { - blah: "blah", - }; -} +async function readRequest(stm: ByteStream): Promise { + const head = await stm.read(BLOCK_SIZE); -export class Decoder extends Transform { - state: Buffer; - - constructor() { - super({ - readableObjectMode: true, - writableObjectMode: true, - }); - - this.state = Buffer.alloc(0); + if (head.length === 0) { + // Connection closed + return undefined; } - _transform(chunk: Buffer, encoding, callback) { - this.state = Buffer.concat([this.state, chunk]); + const msgCode = head.readUInt16LE(0x0000); + const msgLen = msgLengths.get(msgCode); - // Read header + if (msgLen === undefined) { + throw new Error(`Message ${msgCode.toString(16)}: Unknown command code`); + } - if (this.state.length < 0x04) { - return callback(null); + const tail = await stm.read(msgLen - BLOCK_SIZE); + const msg = Buffer.concat([head, tail]); + + if (msg.length < msgLen) { + throw new Error(`Message ${msgCode.toString(16)}: Truncated read`); + } + + if (debug.enabled) { + debug("Raw: %s", msg.toString("hex")); + } + + const reader = readerFns.get(msgCode); + + if (reader === undefined) { + throw new Error(`Message ${msgCode.toString(16)}: No read handler`); + } + + const payload = reader(msg); + + debug("Payload: %j", payload); + + return payload; +} + +export default async function* readRequestStream(stm: ByteStream) { + while (true) { + const req = await readRequest(stm); + + if (req === undefined) { + return; } - const magic = this.state.readUInt32LE(0); - - if (magic !== 0x01020304) { - return callback( - new Error( - "Invalid magic number, cryptographic processing probably incorrect." - ) - ); - } - - if (this.state.length < 0x30) { - return callback(null); - } - - const header = readHeader(this.state); - - if (this.state.length < 0x32) { - return callback(null); - } - - const msgCode = this.state.readUInt16LE(0x30); - const msgLen = msgLengths.get(msgCode); - - if (msgLen === undefined) { - return callback( - new Error( - `Unknown command code ${msgCode.toString(16)}, cannot continue` - ) - ); - } - - if (this.state.length < 0x30 + msgLen) { - return callback(null); - } - - const reqBuf = this.state.slice(0, 0x30 + msgLen); - const payloadBuf = reqBuf.slice(0x30); - - if (debug.enabled) { - debug("Raw: %s", reqBuf.toString("hex")); - debug("Header: %j", header); - } - - const reader = readerFns.get(msgCode); - - if (reader === undefined) { - return callback( - new Error(`No reader for command code ${msgCode.toString(16)}`) - ); - } - - const payload = reader(payloadBuf); - - debug("Payload: %j", payload); - - return callback(null, payload); + yield req; } } diff --git a/src/idz/userdb/encoder/index.ts b/src/idz/userdb/encoder/index.ts index ee7108d..3b8edae 100644 --- a/src/idz/userdb/encoder/index.ts +++ b/src/idz/userdb/encoder/index.ts @@ -141,27 +141,18 @@ function encode(res: Response): Buffer { } } -export class Encoder extends Transform { - constructor() { - super({ - readableObjectMode: true, - writableObjectMode: true, - }); +export default function writeResponse(res: Response) { + debug("Object: %j", res); + + const buf = encode(res); + + if (debug.enabled) { + debug("Encoded: %s", buf.toString("hex")); } - _transform(res: Response, encoding, callback) { - debug("Object: %j", res); - - const buf = encode(res); - - if (debug.enabled) { - debug("Encoded: %s", buf.toString("hex")); - } - - if (buf.readInt16LE(0) === 0) { - throw new Error("Missing message type code"); - } - - return callback(null, buf); + if (buf.readInt16LE(0) === 0) { + throw new Error("Programming error: missing message type code"); } + + return buf; } diff --git a/src/idz/userdb/index.ts b/src/idz/userdb/index.ts index 2a9625c..6a0d7cb 100644 --- a/src/idz/userdb/index.ts +++ b/src/idz/userdb/index.ts @@ -1,8 +1,10 @@ import logger from "debug"; import { Socket } from "net"; +import readRequestStream from "./decoder"; +import writeResponse from "./encoder"; import { dispatch } from "./handler"; -import { setup } from "./setup"; +import setup from "../common"; import { DataSource } from "../../sql"; import { SqlRepositories } from "./sql"; @@ -10,26 +12,28 @@ const debug = logger("app:idz:userdb"); export default function idz(db: DataSource) { return async function(socket: Socket) { - const { input, output } = setup(socket); - - debug("Connection opened"); + debug("Connection established"); try { - for await (const req of input) { + const { clientHello, aesStream } = await setup(socket); + + debug("Handshake OK", clientHello); + + for await (const req of readRequestStream(aesStream)) { const res = await db.transaction(txn => dispatch(new SqlRepositories(txn), req) ); - output.write(res); + await aesStream.write(writeResponse(res)); } - } catch (e) { + } catch (error) { if (debug.enabled) { - debug("Error: %s", e.stack); + debug("Error: %s", error.stack); } } debug("Connection closed"); - input.end(); + socket.end(); }; } diff --git a/src/idz/userdb/setup.ts b/src/idz/userdb/setup.ts deleted file mode 100644 index 29c1107..0000000 --- a/src/idz/userdb/setup.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { createCipheriv, createDecipheriv } from "crypto"; -import { Socket } from "net"; -import { pipeline as pipelineWithCallback } from "stream"; -import { promisify } from "util"; - -import { byteString, modPow } from "./bigint"; -import { Decoder } from "./decoder"; -import { Encoder } from "./encoder"; -import { Request } from "./request"; -import { Response } from "./response"; - -// Drops the stupid mandatory callback parameter crap - -const pipeline = promisify(pipelineWithCallback); - -// Proof-of-concept, so we only ever use one of the ten RSA key pairs -// (SEGA shipped their central server private keys for god knows what reason) - -const key = { - N: 4922323266120814292574970172377860734034664704992758249880018618131907367614177800329506877981986877921220485681998287752778495334541127048495486311792061n, - d: 1163847742215766215216916151663017691387519688859977157498780867776436010396072628219119707788340687440419444081289736279466637153082223960965411473296473n, - e: 3961365081960959178294197133768419551060435043430437330799371731939550352626564261219865471710058480523874787120718634318364066605378505537556570049131337n, - hashN: 2662304617, -}; - -// Proof-of-concept, so we only use one fixed session key -const sessionKey = 0xffddeeccbbaa99887766554433221100n; - -interface Session { - input: AsyncIterable & { - end: () => void; - }; - output: { - write: (res: Response) => void; - }; -} - -function doNothing() {} - -export function setup(socket: Socket): Session { - // - // Construct and transmit setup message - // - - const keyEnc = modPow(sessionKey, key.e, key.N); - const msg = Buffer.alloc(0x48); - - msg.set(byteString(keyEnc, 0x40), 0x00); - msg.writeUInt32LE(0x01020304, 0x40); // Meaning of this field is unknown - msg.writeUInt32LE(key.hashN, 0x44); - - socket.write(msg); - - // - // Set up pipeline - // - - const input = new Decoder(); - const output = new Encoder(); - const keybuf = byteString(sessionKey, 0x10); - - pipeline( - socket, - createDecipheriv("aes-128-ecb", keybuf, null).setAutoPadding(false), - input - ).catch(doNothing); - - pipeline( - output, - createCipheriv("aes-128-ecb", keybuf, null).setAutoPadding(false), - socket - ).catch(doNothing); - - return { input, output }; -}