Skip to content

Commit

Permalink
chore: Add legacy crypto support verification
Browse files Browse the repository at this point in the history
  • Loading branch information
drazisil committed Jun 2, 2024
1 parent 08f7263 commit 5bac332
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 10 deletions.
24 changes: 15 additions & 9 deletions packages/main/src/NPSUserLoginPayload.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { NPSMessagePayload } from "./NPSMessagePayload.js";
import type { INPSPayload } from "./types.js";

/**
* @typedef INPSPayload
* @type {INPSPayload}
*/
class PackedSessionKey {
key: string;
timestamp: number;

constructor(bytes: Buffer) {
this.key = bytes.toString("hex", 0, 16);
this.timestamp = bytes.readUInt32BE(16);
}
}


/**
* @implements {INPSPayload}
Expand All @@ -16,14 +22,14 @@ export class NPSUserLoginPayload
implements INPSPayload
{
ticket: string;
sessionKey: string;
sessionKey: Buffer;
gameId: string;

constructor() {
super();
this.data = Buffer.alloc(0);
this.ticket = "";
this.sessionKey = "";
this.sessionKey = Buffer.alloc(0);
this.gameId = "";
}

Expand All @@ -33,7 +39,7 @@ export class NPSUserLoginPayload
* @param {Buffer} data
* @returns {NPSUserLoginPayload}
*/
static parse(data: Buffer, len = data.length) {
static parse(data: Buffer, len: number = data.length): NPSUserLoginPayload {
if (data.length !== len) {
throw new Error(
`Invalid payload length: ${data.length}, expected: ${len}`,
Expand All @@ -48,7 +54,7 @@ export class NPSUserLoginPayload
offset = nextLen + 2;
offset += 2; // Skip one empty word
nextLen = data.readUInt16BE(offset);
self.sessionKey = data.toString("hex", offset + 2, offset + 2 + nextLen);
self.sessionKey = data.subarray(offset + 2, offset + 2 + nextLen);
offset += nextLen + 2;
nextLen = data.readUInt16BE(offset);
self.gameId = data
Expand All @@ -75,7 +81,7 @@ export class NPSUserLoginPayload
/**
* @returns {string}
*/
toString() {
toString(): string {
return `Ticket: ${this.ticket}, SessionKey: ${this.sessionKey}, GameId: ${this.gameId}`;
}
}
126 changes: 126 additions & 0 deletions packages/main/src/encryption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// mcos is a game server, written from scratch, for an old game
// Copyright (C) <2017> <Drazi Crendraven>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

import { Cipher, Decipher, createCipheriv, createDecipheriv, getCiphers } from "node:crypto";


/**
* @external crypto
* @see {@link https://nodejs.org/api/crypto.html}
*/

/**
* A pair of encryption ciphers.
*/
export class McosEncryptionPair {
_cipher: Cipher;
_decipher: Decipher;
/**
* Create a new encryption pair.
*
* This function creates a new encryption pair. It is used to encrypt and
* decrypt data sent to and from the client.
*
* @param {module:crypto.Cipher} cipher The cipher to use for encryption.
* @param {module:crypto.Decipher} decipher The decipher to use for decryption.
*/
constructor(cipher: Cipher, decipher: Decipher) {
this._cipher = cipher;
this._decipher = decipher;
}

/**
* @param {Buffer} data The data to encrypt.
* @returns {Buffer} The encrypted data.
*/
encrypt(data: Buffer): Buffer {
return this._cipher.update(data);
}

/**
* @param {Buffer} data The data to decrypt.
* @returns {Buffer} The decrypted data.
*/
decrypt(data: Buffer): Buffer {
return this._decipher.update(data);
}
}

/**
* This function creates a new encryption pair for use with the game server
*
* @param {string} key The key to use for encryption
* @returns {McosEncryptionPair} The encryption pair
*/
export function createCommandEncryptionPair(key: string): McosEncryptionPair {
if (key.length < 16) {
throw Error(`Key too short: ${key}`);
}

const sKey = key.slice(0, 16);

// Deepcode ignore HardcodedSecret: This uses an empty IV
const desIV = Buffer.alloc(8);

const gsCipher = createCipheriv("des-cbc", Buffer.from(sKey, "hex"), desIV);
gsCipher.setAutoPadding(false);

const gsDecipher = createDecipheriv(
"des-cbc",
Buffer.from(sKey, "hex"),
desIV,
);
gsDecipher.setAutoPadding(false);

return new McosEncryptionPair(gsCipher, gsDecipher);
}

/**
* This function creates a new encryption pair for use with the database server
*
* @param {string} key The key to use for encryption
* @returns {McosEncryptionPair} The encryption pair
* @throws Error if the key is too short
*/
export function createDataEncryptionPair(key: string): McosEncryptionPair {
if (key.length < 16) {
throw Error(`Key too short: ${key}`);
}

const stringKey = Buffer.from(key, "hex");

// File deepcode ignore InsecureCipher: RC4 is the encryption algorithum used here, file deepcode ignore HardcodedSecret: A blank IV is used here
const tsCipher = createCipheriv("rc4", stringKey.subarray(0, 16), "");
const tsDecipher = createDecipheriv("rc4", stringKey.subarray(0, 16), "");

return new McosEncryptionPair(tsCipher, tsDecipher);
}

/**
* This function checks if the server supports the legacy ciphers
*
* @returns void
* @throws Error if the server does not support the legacy ciphers
*/
export function verifyLegacyCipherSupport() {
const cipherList = getCiphers();
if (!cipherList.includes("des-cbc")) {
throw Error("DES-CBC cipher not available");
}
if (!cipherList.includes("rc4")) {
throw Error("RC4 cipher not available");
}
}
30 changes: 30 additions & 0 deletions packages/main/src/handleUserLogin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
import { NPSUserLoginPayload } from "./NPSUserLoginPayload.js";
import fs from "node:fs";
import crypto from "node:crypto";
import type { TClientCallback } from "./types.js";

export function loadPrivateKey(path: string): string {
const privateKey = fs.readFileSync(path);

return privateKey.toString("utf8");
}

export function decryptSessionKey(
encryptedSessionKey: string,
privateKey: string,
): string {
const sessionKeyStructure = crypto.privateDecrypt(
privateKey,
Buffer.from(encryptedSessionKey, "hex"),
);

return sessionKeyStructure.toString("hex");
}

/**
*
* @param {import("./NPSUserLoginPayload.js").NPSUserLoginPayload} payload
Expand All @@ -12,4 +32,14 @@ export function handleUserLogin(
) {
const userLoginPayload = payload;
console.log(`User login: ${userLoginPayload.toString()}`);

const privateKey = loadPrivateKey("data/private_key.pem");

const sessionKey = decryptSessionKey(userLoginPayload.sessionKey.toString(), privateKey);

console.log(`Session key: ${Buffer.from(sessionKey, "hex").toString("hex")}`);

const key = sessionKey.slice(4, 4 + 64);

console.log(`Key: ${Buffer.from(key, "hex").toString("hex")}`);
}
7 changes: 6 additions & 1 deletion packages/main/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import * as crypto from "node:crypto";
import * as Sentry from "@sentry/node";
import * as net from "node:net";
import * as http from "node:http";
import { verifyLegacyCipherSupport } from "./encryption.js";

type TOnDataHandler = (
port: number,
Expand Down Expand Up @@ -126,10 +127,14 @@ async function _atExit(exitCode = 0) {
// === MAIN ===

function main() {
process.on("exit", (/** @type {number} **/ code) => {
process.on("exit", (code: number) => {
console.log(`Server exited with code ${code}`);
});

console.log("Verifying legacy crypto support...");

verifyLegacyCipherSupport();

console.log("Starting obsidian...");
const authServer = new WebServer(
3000,
Expand Down

0 comments on commit 5bac332

Please sign in to comment.