idz: Port userdb/ onto common/
This commit is contained in:
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import logger from "debug";
|
import logger from "debug";
|
||||||
import { Transform } from "stream";
|
|
||||||
|
|
||||||
import { checkTeamName } from "./checkTeamName";
|
import { checkTeamName } from "./checkTeamName";
|
||||||
import { createProfile } from "./createProfile";
|
import { createProfile } from "./createProfile";
|
||||||
@@ -49,6 +48,8 @@ import { updateTeamPoints } from "./updateTeamPoints";
|
|||||||
import { updateUiReport } from "./updateUiReport";
|
import { updateUiReport } from "./updateUiReport";
|
||||||
import { updateUserLog } from "./updateUserLog";
|
import { updateUserLog } from "./updateUserLog";
|
||||||
import { lockProfileExtend } from "./lockProfileExtend";
|
import { lockProfileExtend } from "./lockProfileExtend";
|
||||||
|
import { BLOCK_SIZE } from "../../common";
|
||||||
|
import { ByteStream } from "../../../util/stream";
|
||||||
|
|
||||||
const debug = logger("app:idz:userdb:decoder");
|
const debug = logger("app:idz:userdb:decoder");
|
||||||
|
|
||||||
@@ -119,88 +120,53 @@ for (const fn of funcList) {
|
|||||||
msgLengths.set(fn.msgCode, fn.msgLen);
|
msgLengths.set(fn.msgCode, fn.msgLen);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readHeader(buf: Buffer) {
|
async function readRequest(stm: ByteStream): Promise<Request | undefined> {
|
||||||
return {
|
const head = await stm.read(BLOCK_SIZE);
|
||||||
blah: "blah",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Decoder extends Transform {
|
if (head.length === 0) {
|
||||||
state: Buffer;
|
// Connection closed
|
||||||
|
return undefined;
|
||||||
constructor() {
|
|
||||||
super({
|
|
||||||
readableObjectMode: true,
|
|
||||||
writableObjectMode: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.state = Buffer.alloc(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_transform(chunk: Buffer, encoding, callback) {
|
const msgCode = head.readUInt16LE(0x0000);
|
||||||
this.state = Buffer.concat([this.state, chunk]);
|
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) {
|
const tail = await stm.read(msgLen - BLOCK_SIZE);
|
||||||
return callback(null);
|
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);
|
yield req;
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,27 +141,18 @@ function encode(res: Response): Buffer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Encoder extends Transform {
|
export default function writeResponse(res: Response) {
|
||||||
constructor() {
|
debug("Object: %j", res);
|
||||||
super({
|
|
||||||
readableObjectMode: true,
|
const buf = encode(res);
|
||||||
writableObjectMode: true,
|
|
||||||
});
|
if (debug.enabled) {
|
||||||
|
debug("Encoded: %s", buf.toString("hex"));
|
||||||
}
|
}
|
||||||
|
|
||||||
_transform(res: Response, encoding, callback) {
|
if (buf.readInt16LE(0) === 0) {
|
||||||
debug("Object: %j", res);
|
throw new Error("Programming error: missing message type code");
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return buf;
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-9
@@ -1,8 +1,10 @@
|
|||||||
import logger from "debug";
|
import logger from "debug";
|
||||||
import { Socket } from "net";
|
import { Socket } from "net";
|
||||||
|
|
||||||
|
import readRequestStream from "./decoder";
|
||||||
|
import writeResponse from "./encoder";
|
||||||
import { dispatch } from "./handler";
|
import { dispatch } from "./handler";
|
||||||
import { setup } from "./setup";
|
import setup from "../common";
|
||||||
import { DataSource } from "../../sql";
|
import { DataSource } from "../../sql";
|
||||||
import { SqlRepositories } from "./sql";
|
import { SqlRepositories } from "./sql";
|
||||||
|
|
||||||
@@ -10,26 +12,28 @@ const debug = logger("app:idz:userdb");
|
|||||||
|
|
||||||
export default function idz(db: DataSource) {
|
export default function idz(db: DataSource) {
|
||||||
return async function(socket: Socket) {
|
return async function(socket: Socket) {
|
||||||
const { input, output } = setup(socket);
|
debug("Connection established");
|
||||||
|
|
||||||
debug("Connection opened");
|
|
||||||
|
|
||||||
try {
|
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 =>
|
const res = await db.transaction(txn =>
|
||||||
dispatch(new SqlRepositories(txn), req)
|
dispatch(new SqlRepositories(txn), req)
|
||||||
);
|
);
|
||||||
|
|
||||||
output.write(res);
|
await aesStream.write(writeResponse(res));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
if (debug.enabled) {
|
if (debug.enabled) {
|
||||||
debug("Error: %s", e.stack);
|
debug("Error: %s", error.stack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debug("Connection closed");
|
debug("Connection closed");
|
||||||
|
|
||||||
input.end();
|
socket.end();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<Request> & {
|
|
||||||
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 };
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user