diff --git a/packages/sessions/src/index.ts b/packages/sessions/src/index.ts index a21dfcce5..736f95608 100644 --- a/packages/sessions/src/index.ts +++ b/packages/sessions/src/index.ts @@ -1,4 +1,4 @@ -import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto"; +import { createCipheriv, createDecipheriv } from "node:crypto"; /** * Represents a pair of encryption and decryption functions. @@ -14,31 +14,75 @@ type CipherPair = { * Generates a pair of cipher and decipher functions for game encryption. * @returns The cipher and decipher functions. */ -function createGameEncryptionPair(): CipherPair { - const key = randomBytes(32); - const iv = randomBytes(16); - const cipher = createCipheriv("aes-256-cbc", key, iv); - const decipher = createDecipheriv("aes-256-cbc", key, iv); - return { - encrypt: cipher.update.bind(cipher), - decrypt: decipher.update.bind(decipher), - }; +function createGameEncryptionPair(key: string): CipherPair { + try { + assertStringIsHex(key); + if (key.length !== 16) { + throw Error( + `Invalid game key length: ${key.length}. The key must be 16 bytes long.`, + ); + } + + // The key used by the game 8 bytes long. + // Since the key is in hex format, we need to slice it to 16 characters. + key = key.slice(0, 16); + + // The IV is intentionally required to be all zeros. + const iv = Buffer.alloc(8); + const keyBuffer = Buffer.from(key, "hex"); + + // The algorithm is intentionally set to "des-cbc". + // This is because the game uses this insecure algorithm. + // We are intentionally using an insecure algorithm here to match the game. + const cipher = createCipheriv("des-cbc", keyBuffer, iv); + const decipher = createDecipheriv("des-cbc", keyBuffer, iv); + + return { + encrypt: cipher.update.bind(cipher), + decrypt: decipher.update.bind(decipher), + }; + } catch (error: unknown) { + const err = new Error(`Failed to create game encryption pair`); + err.cause = error; + throw err; + } } /** * Generates a pair of encryption and decryption functions for the server. - * + * + * @param key - The key to use for encryption and decryption. Must be 16 hex characters. * @returns {CipherPair} The encryption and decryption functions. */ -function createServerEncryptionPair(): CipherPair { - const key = randomBytes(32); - const iv = randomBytes(16); - const cipher = createCipheriv("aes-256-cbc", key, iv); - const decipher = createDecipheriv("aes-256-cbc", key, iv); - return { - encrypt: cipher.update.bind(cipher), - decrypt: decipher.update.bind(decipher), - }; +function createServerEncryptionPair(key: string): CipherPair { + try { + assertStringExists(key); + assertStringIsHex(key); + if (key.length !== 16) { + throw Error( + `Invalid server key length: ${key.length}. The key must be 16 bytes long.`, + ); + } + + // The IV is intentionally required to be empty. + const iv = Buffer.alloc(0); + const keyBuffer = Buffer.from(key, "hex"); + + // The algorithm is intentionally set to "rc4". + // This is because the game uses this insecure algorithm. + // We are intentionally using an insecure algorithm here to match the game. + const cipher = createCipheriv("rc4", keyBuffer, iv); + const decipher = createDecipheriv("rc4", keyBuffer, iv); + + return { + encrypt: cipher.update.bind(cipher), + decrypt: decipher.update.bind(decipher), + }; + } catch (error: unknown) { + const err = new Error(`Failed to create server encryption pair`); + err.cause = error; + throw err; + } } type ConnectedClient = { @@ -58,21 +102,28 @@ type ConnectedClient = { serverEncryptionHandshakeComplete: boolean; }; - - /** * Sets the client encryption for a connected client. - * + * * @param client - The connected client to set the encryption for. * @param sessionKey - The session key to associate with the client. * @returns The updated connected client with the encryption set. */ -export function setClientEncryption(client: ConnectedClient, sessionKey: string): ConnectedClient { - const gameEncryptionPair = createGameEncryptionPair(); - const serverEncryptionPair = createServerEncryptionPair(); - client.sessionKey = sessionKey; - client.gameEncryptionPair = gameEncryptionPair; - client.serverEncryptionPair = serverEncryptionPair; +export function setClientEncryption( + client: ConnectedClient, + sessionKey: string, +): ConnectedClient { + try { + const gameEncryptionPair = createGameEncryptionPair(sessionKey); + const serverEncryptionPair = createServerEncryptionPair(sessionKey); + client.sessionKey = sessionKey; + client.gameEncryptionPair = gameEncryptionPair; + client.serverEncryptionPair = serverEncryptionPair; + } catch (error: unknown) { + const err = new Error(`Failed to set client encryption`); + err.cause = error; + throw err; + } return client; } @@ -90,9 +141,7 @@ const connectedClients: Record = {}; * @returns The connected client with the specified customer ID. * @throws Error if no client is found with the given customer ID. */ -export function findClientByCustomerId( - customerId: number, -): ConnectedClient { +export function findClientByCustomerId(customerId: number): ConnectedClient { const client = Object.values(connectedClients).find( (client) => client.customerId === customerId, ); @@ -145,7 +194,7 @@ export function newClientConnection( /** * Saves the client connection with the specified connection ID. - * + * * @param connectionId - The ID of the connection. * @param client - The connected client to be saved. */ @@ -164,3 +213,21 @@ export function clearConnectedClients(): void { delete connectedClients[connectionId]; } } + +function assertStringExists(str: string): void { + if (str === "" || typeof str === "undefined") { + throw new Error("String not provided"); + } +} + +/** + * Asserts that a given string is a valid hexadecimal string. + * + * @param str - The string to be validated. + * @throws {Error} If the string is not a valid hexadecimal string. + */ +function assertStringIsHex(str: string): void { + if (!/^[0-9a-fA-F]+$/.test(str)) { + throw new Error(`Invalid hex string: ${str}`); + } +} diff --git a/packages/sessions/test/index.test.ts b/packages/sessions/test/index.test.ts index e353cf14d..fb95ff18c 100644 --- a/packages/sessions/test/index.test.ts +++ b/packages/sessions/test/index.test.ts @@ -1,7 +1,18 @@ import { beforeEach, describe, expect, it } from "vitest"; -import { saveClientConnection, clearConnectedClients, findClientByCustomerId, hasClientEncryptionPair, newClientConnection, setClientEncryption } from "../index.js" +import { + saveClientConnection, + clearConnectedClients, + findClientByCustomerId, + hasClientEncryptionPair, + newClientConnection, + setClientEncryption, +} from "../index.js"; describe("Client connections", () => { + beforeEach(() => { + clearConnectedClients(); + }); + describe("newClientConnection", () => { it("should create a new client connection", () => { const connectionId = "123"; @@ -29,11 +40,6 @@ describe("Client connections", () => { }); describe("findClientByCustomerId", () => { - - beforeEach(() => { - clearConnectedClients(); - }); - it("should find a client by customer ID", () => { const connectionId = "123"; const customerId = 456; @@ -52,17 +58,53 @@ describe("Client connections", () => { }); }); + describe("setClientEncryption", () => { + it("should set the client encryption pair", () => { + const connectionId = "123"; + const customerId = 456; + const sessionKey = "ea25e21a2a022d71"; + + const client = newClientConnection(connectionId, customerId); + saveClientConnection(connectionId, client); + + setClientEncryption(client, sessionKey); + + expect(client.sessionKey).toBe(sessionKey); + }); + + it("should throw an error if the session key is not provided", () => { + const connectionId = "123"; + const customerId = 456; + + const client = newClientConnection(connectionId, customerId); + saveClientConnection(connectionId, client); + + expect(() => setClientEncryption(client, "")).toThrow(); + }); + + it("should throw an error if the session key is invalid", () => { + const connectionId = "123"; + const customerId = 456; + + const client = newClientConnection(connectionId, customerId); + saveClientConnection(connectionId, client); + + expect(() => setClientEncryption(client, "invalid")).toThrow(); + }); + }); + describe("hasClientEncryptionPair", () => { it("should return true if the client has an encryption pair", () => { const connectionId = "123"; const customerId = 456; + const sessionKey = "ea25e21a2a022d71"; const client = newClientConnection(connectionId, customerId); saveClientConnection(connectionId, client); expect(hasClientEncryptionPair(client, "game")).toBe(false); expect(hasClientEncryptionPair(client, "server")).toBe(false); - setClientEncryption(client, "sessionKey"); + setClientEncryption(client, sessionKey); expect(hasClientEncryptionPair(client, "game")).toBe(true); expect(hasClientEncryptionPair(client, "server")).toBe(true); @@ -84,4 +126,3 @@ describe("Client connections", () => { }); }); }); - \ No newline at end of file diff --git a/packages/sessions/tsconfig.json b/packages/sessions/tsconfig.json index 751b5710c..e6cbe5160 100644 --- a/packages/sessions/tsconfig.json +++ b/packages/sessions/tsconfig.json @@ -6,6 +6,7 @@ }, "include": [ "index.ts", - "src" + "src", + "test" ] } \ No newline at end of file