231 lines
4.8 KiB
TypeScript
231 lines
4.8 KiB
TypeScript
/* eslint-disable no-bitwise */
|
|
import { createCipheriv, createDecipheriv } from "crypto";
|
|
import type { integer } from "types/misc";
|
|
|
|
const ASCII_0 = 48;
|
|
|
|
export function encryptPacket(packet: Buffer, key: Buffer) {
|
|
const cipher = createCipheriv("aes-128-ecb", key, null).setAutoPadding(false);
|
|
|
|
return Buffer.concat([cipher.update(packet), cipher.final()]);
|
|
}
|
|
|
|
export function decryptPacket(packet: Buffer, key: Buffer) {
|
|
const cipher = createDecipheriv("aes-128-ecb", key, null).setAutoPadding(false);
|
|
|
|
return Buffer.concat([cipher.update(packet), cipher.final()]);
|
|
}
|
|
|
|
function char2num(char: string) {
|
|
return char.charCodeAt(0) - ASCII_0 + 1;
|
|
}
|
|
|
|
function num2char(num: integer) {
|
|
let inner = num;
|
|
|
|
while (inner < 1) {
|
|
inner = inner + 10;
|
|
}
|
|
|
|
// 48 is ASCII 0
|
|
return String.fromCharCode(((inner - 1) % 10) + ASCII_0);
|
|
}
|
|
|
|
/**
|
|
* A modified version of the [Solitaire cipher](https://en.wikipedia.org/wiki/Solitaire_(cipher)),
|
|
* used for encrypting and decrypting access codes.
|
|
*/
|
|
export class Solitaire {
|
|
private deck: Array<integer>;
|
|
private readonly deckSize: integer;
|
|
readonly jokerA: integer;
|
|
readonly jokerB: integer;
|
|
|
|
constructor(deckSize: integer, jokerA?: integer, jokerB?: integer) {
|
|
if (jokerA && jokerA > deckSize) {
|
|
throw new Error(
|
|
"Invalid jokerA. jokerA must not be larger than the largest card on deck."
|
|
);
|
|
}
|
|
|
|
if (jokerB && jokerB > deckSize) {
|
|
throw new Error(
|
|
"Invalid jokerB. jokerB must not be larger than the largest card on deck."
|
|
);
|
|
}
|
|
|
|
this.jokerA = jokerA ?? deckSize - 1;
|
|
this.jokerB = jokerB ?? deckSize;
|
|
this.deck = [...Array(deckSize).keys()].map((v) => v + 1);
|
|
this.deckSize = deckSize;
|
|
}
|
|
|
|
static encrypt(src: string, key: string) {
|
|
const deck = new Solitaire(22, 21, 22);
|
|
|
|
for (const char of key) {
|
|
deck.deckHash();
|
|
deck.cutDeck(char2num(char));
|
|
}
|
|
|
|
let output = "";
|
|
|
|
for (const char of src) {
|
|
deck.deckHash();
|
|
|
|
const p = deck.getTopCardNumber() ?? 0;
|
|
|
|
output = output + num2char(char2num(char) + p);
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
static decrypt(src: string, key: string) {
|
|
const deck = new Solitaire(22, 21, 22);
|
|
|
|
for (const char of key) {
|
|
deck.deckHash();
|
|
deck.cutDeck(char2num(char));
|
|
}
|
|
|
|
let output = "";
|
|
|
|
for (const char of src) {
|
|
deck.deckHash();
|
|
|
|
const p = deck.getTopCardNumber() ?? 0;
|
|
|
|
output = output + num2char(char2num(char) - p);
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
private moveCardDown(card: integer) {
|
|
const cardLocation = this.deck.indexOf(card);
|
|
|
|
if (cardLocation === -1) {
|
|
throw new Error("Card was not in the deck.");
|
|
}
|
|
|
|
if (cardLocation < this.deckSize - 1) {
|
|
const nextCard = this.deck[cardLocation + 1];
|
|
|
|
if (!nextCard) {
|
|
throw new Error("Next card is undefined. This should not be possible.");
|
|
}
|
|
|
|
this.deck[cardLocation] = nextCard;
|
|
this.deck[cardLocation + 1] = card;
|
|
} else {
|
|
for (let i = this.deckSize - 1; i > 1; i--) {
|
|
const prevCard = this.deck[i - 1];
|
|
|
|
if (!prevCard) {
|
|
throw new Error("Previous card is undefined. This should not be possible.");
|
|
}
|
|
|
|
this.deck[i] = prevCard;
|
|
}
|
|
|
|
this.deck[1] = card;
|
|
}
|
|
}
|
|
|
|
private cutDeck(point: integer) {
|
|
const tmpDeck = this.deck.slice(0);
|
|
|
|
let idx = 0;
|
|
|
|
for (let i = point; i < this.deckSize - 1; i++) {
|
|
const item = tmpDeck[i];
|
|
|
|
if (!item) {
|
|
throw new Error("not happening.");
|
|
}
|
|
|
|
this.deck[idx] = item;
|
|
idx++;
|
|
}
|
|
|
|
for (let i = 0; i < point; i++) {
|
|
const item = tmpDeck[i];
|
|
|
|
if (!item) {
|
|
throw new Error("not happening.");
|
|
}
|
|
|
|
this.deck[idx] = item;
|
|
idx++;
|
|
}
|
|
}
|
|
|
|
private swapOutsideJoker() {
|
|
let joker1 = -1;
|
|
let joker1Value = -1;
|
|
let joker2 = -1;
|
|
let joker2Value = -1;
|
|
|
|
for (let i = 0; i < this.deckSize; i++) {
|
|
const card = this.deck[i];
|
|
|
|
if (!card) {
|
|
continue;
|
|
}
|
|
|
|
if (card === this.jokerA || card === this.jokerB) {
|
|
if (joker1 === -1) {
|
|
joker1 = i;
|
|
joker1Value = card;
|
|
} else {
|
|
joker2 = i;
|
|
joker2Value = card;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.deck = [
|
|
...this.deck.slice(joker2 + 1),
|
|
joker1Value,
|
|
...this.deck.slice(joker1 + 1, joker2),
|
|
joker2Value,
|
|
...this.deck.slice(0, joker1),
|
|
];
|
|
}
|
|
|
|
private cutByBottomCard() {
|
|
const bottom = this.deck[this.deckSize - 1];
|
|
|
|
if (!bottom) {
|
|
throw new Error("bottom card is undefined (not possbile)");
|
|
}
|
|
|
|
this.cutDeck(bottom === this.jokerB ? this.jokerA : bottom);
|
|
}
|
|
|
|
private getTopCardNumber() {
|
|
const top = this.deck[0];
|
|
|
|
if (!top) {
|
|
throw new Error("top card is undefined (not possbile)");
|
|
}
|
|
|
|
return this.deck[top === this.jokerB ? this.jokerA : top];
|
|
}
|
|
|
|
private deckHash() {
|
|
let p = -1;
|
|
|
|
do {
|
|
this.moveCardDown(this.jokerA);
|
|
this.moveCardDown(this.jokerB);
|
|
this.moveCardDown(this.jokerB);
|
|
this.swapOutsideJoker();
|
|
this.cutByBottomCard();
|
|
|
|
p = this.getTopCardNumber() ?? -1;
|
|
} while (p === this.jokerA || p === this.jokerB);
|
|
}
|
|
}
|