Skip to content

Commit

Permalink
Adding compression and encryption modules for gzip, bzip2, and xtea
Browse files Browse the repository at this point in the history
  • Loading branch information
Tynarus committed Sep 5, 2021
1 parent 8cb7953 commit 52b3f5c
Show file tree
Hide file tree
Showing 9 changed files with 264 additions and 26 deletions.
46 changes: 26 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@

# @runejs/core

Core logging, networking, and buffer functionality for RuneJS applications.
Core logging, networking, compression, encryption, and additional buffer functionality for RuneJS applications.

### Logger
* `RuneLogger` singleton Pino `logger` wrapper:
### @runejs/core
* `logger` is exported as a singleton Pino logging wrapper, offering the following functions:
* `logger.info(...messages)`
* `logger.debug(...messages)`
* `logger.warn(...messages)`
Expand All @@ -19,20 +19,26 @@ Core logging, networking, and buffer functionality for RuneJS applications.
* Ability to set the Pino logging pretty print config value via `setLoggerPrettyPrint(boolean)`
* Setting of _all_ Pino logging options via `setLoggerOptions(Pino.LoggerOptions)`

### Byte Buffer
* Node `Uint8Array` wrapper with additional utility functions.
* Unified configurable `get` and `put` methods to easily move bytes within the buffer.
* Int24, Smart, Long and String type support.
* Big endian, little endian, and mixed endian support.
* Bit access through `openBitBuffer()`, `putBits()`, and `closeBitBuffer()`

### Networking Components

#### openServer(name, host, port, connectionHandlerFactory)
Spins up a new Node Socket server with the specified host and port.

#### SocketConnectionHandler
Handles connections made to a Socket server opened via `openServer()`

#### ServerConfigOptions
Options for a configured Socket server, imported using the `parseServerConfig()` function.
### @runejs/core/buffer
* `ByteBuffer` is the main export.
* Node `Uint8Array` wrapper with additional utility functions.
* Unified configurable `get` and `put` methods to easily move bytes within the buffer.
* Int24, Smart, Long and String type support.
* Big endian, little endian, and mixed endian support.
* Bit access through `openBitBuffer()`, `putBits()`, and `closeBitBuffer()`

### @runejs/core/net
* `SocketServer`
* Handles connections made to a RuneJS socket server.
* `SocketServer.launch(serverName, hostName, port, connectionHandlerFactory)`
* Spins up a new Node Socket server with the specified host and port.
* `ServerConfigOptions`
* Options for a configured Socket server, imported using the `parseServerConfig()` function.

### @runejs/core/compression
* Exported class `Gzip` handles Gzip compression and decompression.
* Exported class `Bzip2` handles Bzip2 compression and decompression.

### @runejs/core/encryption
Provides XTEA encryption and decryption functionality, as well as a key file loader.
* Exported as class `Xtea`
32 changes: 29 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@runejs/core",
"version": "1.5.4",
"version": "1.6.0",
"description": "Core logging, networking, and buffer functionality for RuneJS applications.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
Expand Down Expand Up @@ -30,9 +30,11 @@
},
"homepage": "https://github.com/runejs/core#readme",
"dependencies": {
"compressjs": "^1.0.3",
"js-yaml": "^3.14.1",
"pino": "^6.13.0",
"pino-pretty": "^4.8.0"
"pino-pretty": "^4.8.0",
"tslib": ">=2.1.0"
},
"devDependencies": {
"@runejs/eslint-config": "^1.0.0",
Expand All @@ -43,7 +45,6 @@
"eslint": "^7.32.0",
"rimraf": "^3.0.2",
"ts-node": "^9.1.1",
"tslib": ">=2.1.0",
"typescript": ">=4.2.0"
},
"peerDependencies": {
Expand Down
28 changes: 28 additions & 0 deletions src/compression/bzip2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ByteBuffer } from '../buffer';
import * as compressjs from 'compressjs';
const bzip = compressjs.Bzip2;


const charCode = (letter: string) => letter.charCodeAt(0);


export class Bzip2 {

public static compress(rawFileData: ByteBuffer): ByteBuffer {
const compressedFile = new ByteBuffer(bzip.compressFile(rawFileData, undefined, 1));
// Do not include the BZip compression level header because the client expects a headerless BZip format
return new ByteBuffer(compressedFile.slice(4, compressedFile.length));
}

public static decompress(compressedFileData: ByteBuffer): ByteBuffer {
const buffer = Buffer.alloc(compressedFileData.length + 4);
compressedFileData.copy(buffer, 4);
buffer[0] = charCode('B');
buffer[1] = charCode('Z');
buffer[2] = charCode('h');
buffer[3] = charCode('1');

return new ByteBuffer(bzip.decompressFile(buffer));
}

}
5 changes: 5 additions & 0 deletions src/compression/file-compression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum Compression {
uncompressed = 0,
bzip = 1,
gzip = 2
}
15 changes: 15 additions & 0 deletions src/compression/gzip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { gunzipSync, gzipSync } from 'zlib';
import { ByteBuffer } from '../buffer';


export class Gzip {

public static compress(buffer: ByteBuffer): ByteBuffer {
return new ByteBuffer(gzipSync(buffer));
}

public static decompress(buffer: ByteBuffer): ByteBuffer {
return new ByteBuffer(gunzipSync(buffer));
}

}
3 changes: 3 additions & 0 deletions src/compression/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './bzip2';
export * from './gzip';
export * from './file-compression';
1 change: 1 addition & 0 deletions src/encryption/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './xtea';
153 changes: 153 additions & 0 deletions src/encryption/xtea.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import path from 'path';
import fs from 'fs';
import { logger } from '../logger';
import { ByteBuffer } from '../buffer';


export type XteaKey = [ number, number, number, number ];


export interface XteaConfig {
archive: number;
group: number;
name_hash: number;
name: string;
mapsquare: number;
key: XteaKey;
}


export interface XteaKeys {
gameVersion: number;
key: XteaKey;
}


const toInt = value => value | 0;


export class Xtea {

public static loadKeys(xteaConfigPath: string): Map<string, XteaKeys[]> {
if(!fs.existsSync(xteaConfigPath)) {
logger.error(`Error loading XTEA keys: ${xteaConfigPath} was not found.`);
return null;
}

const stats = fs.statSync(xteaConfigPath);

if(!stats.isDirectory()) {
logger.error(`Error loading XTEA keys: ${xteaConfigPath} is not a directory.`);
return null;
}

const xteaKeys: Map<string, XteaKeys[]> = new Map<string, XteaKeys[]>();
const xteaFileNames = fs.readdirSync(xteaConfigPath);
for(const fileName of xteaFileNames) {
try {
const gameVersionString = fileName.substring(0, fileName.indexOf('.json'));
if(!gameVersionString) {
logger.error(`Error loading XTEA config file ${fileName}: No game version supplied.`);
continue;
}

const gameVersion: number = Number(gameVersionString);
if(!gameVersion || isNaN(gameVersion)) {
logger.error(`Error loading XTEA config file ${fileName}: Invalid game version supplied.`);
continue;
}

const fileContent = fs.readFileSync(path.join(xteaConfigPath, fileName), 'utf-8');
const xteaConfigList = JSON.parse(fileContent) as XteaConfig[];

if(!xteaConfigList?.length) {
logger.error(`Error loading XTEA config file ${fileName}: File is empty.`);
continue;
}

for(const xteaConfig of xteaConfigList) {
if(!xteaConfig?.name || !xteaConfig?.key?.length) {
continue;
}

const { name: fileName, key } = xteaConfig;
let fileKeys: XteaKeys[] = [];

if(xteaKeys.has(fileName)) {
fileKeys = xteaKeys.get(fileName);
}

fileKeys.push({ gameVersion, key });
xteaKeys.set(fileName, fileKeys);
}
} catch(error) {
logger.error(`Error loading XTEA config file ${fileName}:`, error);
}
}

return xteaKeys;
}

public static validKeys(keys?: number[] | undefined): boolean {
return keys?.length === 4 && (keys[0] !== 0 || keys[1] !== 0 || keys[2] !== 0 || keys[3] !== 0);
}

// @TODO unit testing
public static encrypt(input: ByteBuffer, keys: number[], length: number): ByteBuffer {
const encryptedBuffer = new ByteBuffer(length);
const chunks = length / 8;
input.readerIndex = 0;

for(let i = 0; i < chunks; i++) {
let v0 = input.get('int');
let v1 = input.get('int');
let sum = 0;
const delta = -0x61c88647;

let rounds = 32;
while(rounds-- > 0) {
v0 += ((sum + keys[sum & 3]) ^ (v1 + ((v1 >>> 5) ^ (v1 << 4))));
sum += delta
v1 += ((v0 + ((v0 >>> 5) ^ (v0 << 4))) ^ (keys[(sum >>> 11) & 3] + sum));
}

encryptedBuffer.put(v0, 'int');
encryptedBuffer.put(v1, 'int');
}

return encryptedBuffer.flipWriter();
}

// @TODO unit testing
public static decrypt(input: ByteBuffer, keys: number[], length: number): ByteBuffer {
if(!keys?.length) {
return input;
}

const output = new ByteBuffer(length);
const numBlocks = Math.floor(length / 8);

for(let block = 0; block < numBlocks; block++) {
let v0 = input.get('int');
let v1 = input.get('int');
let sum = 0x9E3779B9 * 32;

for(let i = 0; i < 32; i++) {
v1 -= ((toInt(v0 << 4) ^ toInt(v0 >>> 5)) + v0) ^ (sum + keys[(sum >>> 11) & 3]);
v1 = toInt(v1);

sum -= 0x9E3779B9;

v0 -= ((toInt(v1 << 4) ^ toInt(v1 >>> 5)) + v1) ^ (sum + keys[sum & 3]);
v0 = toInt(v0);
}

output.put(v0, 'int');
output.put(v1, 'int');
}

input.copy(output, output.writerIndex, input.readerIndex);
return output;
}

}

0 comments on commit 52b3f5c

Please sign in to comment.