diff --git a/src/idz/bigint.ts b/src/idz/bigint.ts new file mode 100644 index 0000000..1d85a5f --- /dev/null +++ b/src/idz/bigint.ts @@ -0,0 +1,33 @@ +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/decoder.ts b/src/idz/decoder.ts new file mode 100644 index 0000000..9f40143 --- /dev/null +++ b/src/idz/decoder.ts @@ -0,0 +1,91 @@ +import { Transform } from "stream"; + +import { MSG, MSG_LEN } from "./defs"; + +const readers = new Map(); + +function readHeader(buf) { + return { + blah: "blah", + }; +} + +readers.set(MSG.GET_CONFIG_DATA_REQ, buf => { + return { type: "get_config_data_req" }; +}); + +readers.set(MSG.GET_CONFIG_DATA_2_REQ, buf => { + return { type: "get_config_data_2_req" }; +}); + +readers.set(MSG.GET_SERVER_LIST_REQ, buf => { + return { type: "get_server_list_req" }; +}); + +export class Decoder extends Transform { + state: Buffer; + + constructor() { + super({ + readableObjectMode: true, + writableObjectMode: true, + }); + + this.state = Buffer.alloc(0); + } + + _transform(chunk, encoding, callback) { + this.state = Buffer.concat([this.state, chunk]); + + // Read header + + if (this.state.length < 0x04) { + return callback(null); + } + + 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 = MSG_LEN.get(msgCode); + const reader = readers.get(msgCode); + + if (msgLen === undefined || reader === 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(0x30, 0x30 + msgLen); + const payload = reader(reqBuf); + + console.log("Idz: RAW:", reqBuf.toString("hex")); + console.log("Idz: Header:", header); + console.log("Idz: Payload:", payload); + + return callback(null, { header, payload }); + } +} diff --git a/src/idz/defs.ts b/src/idz/defs.ts new file mode 100644 index 0000000..8ffb65a --- /dev/null +++ b/src/idz/defs.ts @@ -0,0 +1,14 @@ +export const MSG = { + GET_CONFIG_DATA_REQ: 0x0004, + GET_CONFIG_DATA_RES: 0x0005, + GET_SERVER_LIST_REQ: 0x0006, + GET_SERVER_LIST_RES: 0x0007, + GET_CONFIG_DATA_2_REQ: 0x00ab, + GET_CONFIG_DATA_2_RES: 0x00ac, +}; + +export const MSG_LEN = new Map(); + +MSG_LEN.set(MSG.GET_CONFIG_DATA_REQ, 0x0050); +MSG_LEN.set(MSG.GET_CONFIG_DATA_2_REQ, 0x0010); +MSG_LEN.set(MSG.GET_SERVER_LIST_REQ, 0x0020); diff --git a/src/idz/encoder.ts b/src/idz/encoder.ts new file mode 100644 index 0000000..6233840 --- /dev/null +++ b/src/idz/encoder.ts @@ -0,0 +1,76 @@ +import { Transform } from "stream"; + +import { MSG } from "./defs"; + +const writers = new Map Buffer>(); + +writers.set("get_config_data_res", obj => { + const buf = Buffer.alloc(0x01a0); + + buf.writeUInt16LE(MSG.GET_CONFIG_DATA_RES, 0x0000); + buf.writeUInt16LE(obj.status, 0x0002); + + return buf; +}); + +writers.set("get_config_data_2_res", obj => { + const buf = Buffer.alloc(0x230); + + buf.writeUInt16LE(MSG.GET_CONFIG_DATA_2_RES, 0x0000); + buf.writeUInt16LE(obj.status, 0x0002); + + return buf; +}); + +writers.set("get_server_list_res", obj => { + const buf = Buffer.alloc(0x04b0); + + buf.writeUInt16LE(MSG.GET_SERVER_LIST_RES, 0x0000); + buf.writeUInt16LE(obj.status, 0x0002); + buf.write(obj.userDb.addr, 0x0004); + buf.writeUInt16LE(obj.userDb.tcp, 0x0084); + buf.writeUInt16LE(obj.userDb.http, 0x0086); + buf.write(obj.matchAddr, 0x0088); + buf.writeUInt16LE(obj.matchPort.tcp, 0x0108); + buf.writeUInt16LE(obj.matchPort.udpSend, 0x010a); + buf.writeUInt16LE(obj.matchPort.udpRecv, 0x010c); + buf.writeUInt16LE(obj.tagMatchPort.tcp, 0x010e); + buf.writeUInt16LE(obj.tagMatchPort.udpSend, 0x0110); + buf.writeUInt16LE(obj.tagMatchPort.udpRecv, 0x0112); + buf.write(obj.event.addr, 0x0114); + buf.writeUInt16LE(obj.event.tcp, 0x0194); + buf.write(obj.screenshot.addr, 0x0198); + buf.writeUInt16LE(obj.screenshot.tcp, 0x0218); + buf.write(obj.pingReturn, 0x021c); + buf.write(obj.echo1.addr, 0x029c); + buf.write(obj.echo2.addr, 0x031c); + buf.writeUInt16LE(obj.echo1.udp, 0x39c); + buf.writeUInt16LE(obj.echo2.udp, 0x39e); + buf.write(obj.newsUrl, 0x03a0); + buf.write(obj.reportErrorUrl, 0x0424); + + return buf; +}); + +export class Encoder extends Transform { + constructor() { + super({ + readableObjectMode: true, + writableObjectMode: true, + }); + } + + _transform(obj, encoding, callback) { + console.log("Idz: Response:", obj); + + const writer = writers.get(obj.type); + + if (writer === undefined) { + return callback(new Error(`No writer for type ${obj.type}`)); + } + + const buf = writer(obj); + + return callback(null, buf); + } +} diff --git a/src/idz/index.ts b/src/idz/index.ts new file mode 100644 index 0000000..ca3d2fa --- /dev/null +++ b/src/idz/index.ts @@ -0,0 +1,80 @@ +import { hostname } from "os"; + +import setup from "./setup"; + +export default async function idz(socket) { + const { input, output } = setup(socket); + + console.log("Idz: Connection opened"); + + try { + for await (const msg of input) { + switch (msg.payload.type) { + case "get_server_list_req": + const myHost = hostname(); + + output.write({ + type: "get_server_list_res", + status: 1, + userDb: { + addr: myHost, + tcp: 10000, + http: 10001, + }, + matchAddr: myHost, + matchPort: { + tcp: 10002, + udpSend: 10003, + udpRecv: 10004, + }, + tagMatchPort: { + tcp: 10005, + udpSend: 10006, + udpRecv: 10007, + }, + event: { + addr: myHost, + tcp: 10008, + }, + screenshot: { + addr: myHost, + tcp: 10009, + }, + pingReturn: myHost, + echo1: { + addr: myHost, + udp: 10010, + }, + echo2: { + addr: myHost, + udp: 10011, + }, + newsUrl: `http://${myHost}:10012/news`, + reportErrorUrl: `http://${myHost}:10013/error`, + }); + + break; + + case "get_config_data_req": + output.write({ + type: "get_config_data_res", + status: 1, + }); + + break; + + case "get_config_data_2_req": + output.write({ + type: "get_config_data_2_res", + status: 1, + }); + + break; + } + } + } catch (e) { + console.log("Idz: Error", e); + } + + console.log("Idz: Connection closed"); +} diff --git a/src/idz/ping.ts b/src/idz/ping.ts new file mode 100644 index 0000000..d220331 --- /dev/null +++ b/src/idz/ping.ts @@ -0,0 +1,11 @@ +import { createSocket } from "dgram"; + +export default function createPing(port: number) { + const socket = createSocket("udp4"); + + socket.bind(port); + socket.on("message", (msg, rinfo) => { + console.log(`Idz Ping: Ping from ${rinfo.address}:${rinfo.port}`); + socket.send(msg, rinfo.port, rinfo.address); + }); +} diff --git a/src/idz/setup.ts b/src/idz/setup.ts new file mode 100644 index 0000000..b21143a --- /dev/null +++ b/src/idz/setup.ts @@ -0,0 +1,62 @@ +import { createCipheriv, createDecipheriv } from "crypto"; +import { Socket } from "net"; +import { pipeline } from "stream"; + +import { byteString, modPow } from "./bigint"; +import { Decoder } from "./decoder"; +import { Encoder } from "./encoder"; + +// Proof-of-concept, so we only ever use one of the ten RSA keys +const key = { + N: 4922323266120814292574970172377860734034664704992758249880018618131907367614177800329506877981986877921220485681998287752778495334541127048495486311792061n, + d: 1163847742215766215216916151663017691387519688859977157498780867776436010396072628219119707788340687440419444081289736279466637153082223960965411473296473n, + e: 3961365081960959178294197133768419551060435043430437330799371731939550352626564261219865471710058480523874787120718634318364066605378505537556570049131337n, + hashN: 2662304617, +}; + +// Proof-of-concept, so we only use one fixed session key +const sessionKey = 0xffddeeccbbaa99887766554433221100n; + +// -- TEST -- +const test1 = modPow(sessionKey, key.e, key.N); +const test2 = modPow(test1, key.d, key.N); + +console.log("RSA ENC :", byteString(test1, 0x40).toString("hex")); +console.log("RSA ENCDEC :", byteString(test2, 0x40).toString("hex")); +// -- TEST -- + +export default function setup(socket: Socket) { + // + // 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 keybuf = byteString(sessionKey, 0x10); + const input = pipeline( + socket, + createDecipheriv("aes-128-ecb", keybuf, null).setAutoPadding(false), + new Decoder() + ); + + const output = new Encoder(); + + pipeline( + output, + createCipheriv("aes-128-ecb", keybuf, null).setAutoPadding(false), + socket + ); + + return { input, output }; +} diff --git a/src/index.ts b/src/index.ts index daf987b..11730ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,8 @@ import aimedb from "./aimedb"; import billing from "./billing"; import chunithm from "./chunithm"; import diva from "./diva"; +import idz from "./idz"; +import idzPing from "./idz/ping"; import startup from "./startup"; const tls = { @@ -21,4 +23,10 @@ https.createServer(tls, billing).listen(8443); http.createServer(chunithm).listen(9000); http.createServer(diva).listen(9001); +net.createServer(idz).listen(10000); +idzPing(10001); +idzPing(10003); +idzPing(10010); +idzPing(10011); + console.log("Startup OK"); diff --git a/src/startup.ts b/src/startup.ts index bcf2e08..3bef059 100644 --- a/src/startup.ts +++ b/src/startup.ts @@ -4,10 +4,15 @@ import read = require("raw-body"); import { unzipSync } from "zlib"; import { hostname } from "os"; -const services = new Map(); +const myHost = hostname(); +const uris = new Map(); -services.set("SDBT", 9000); // Chunithm -services.set("SBZV", 9001); // Project Diva Future Tone +uris.set("SDBT", `http://${myHost}:9000/`); // Chunithm +uris.set("SBZV", `http://${myHost}:9001/`); // Project Diva Future Tone + +const hosts = new Map(); + +hosts.set("SDDF", `${myHost}:10000`); // Initial D Zero const app = express(); @@ -60,9 +65,6 @@ app.use(async function(req, res, next) { app.post("/sys/servlet/PowerOn", function(req, resp) { console.log("\n--- Startup Request ---\n\n", req.body); - const portNo = services.get(req.body.game_id); - const uri = portNo !== undefined ? `http://${hostname()}:${portNo}/` : ""; - // Cut milliseconds out of ISO timestamp const now = new Date(); @@ -71,8 +73,8 @@ app.post("/sys/servlet/PowerOn", function(req, resp) { const resParams = { stat: 1, - uri, - host: "", + uri: uris.get(req.body.game_id) || "", + host: hosts.get(req.body.game_id) || "", place_id: "123", name: "Name", nickname: "Nick", diff --git a/tsconfig.json b/tsconfig.json index 4209ab1..209a7c6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,7 @@ "moduleResolution": "node", "outDir": "./bin/", "strictNullChecks": true, - "target": "es2017" + "target": "esnext" }, "include": ["./src/"], "exclude": ["./src/**/*.test.ts"]