Files
Miasma_Archive/tests/scramble/stringtool.test.ts
T
2026-06-13 09:36:13 -04:00

182 lines
6.7 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { StringScrambler } from "../../src/utils/stringtool";
describe("StringScrambler", () => {
const PASSPHRASE = "ctf-test-passphrase";
const SALT = "test-salt-value";
describe("round-trip with same instance", () => {
const scrambler = new StringScrambler(PASSPHRASE, SALT);
test.each([
["empty string", ""],
["single char", "x"],
["two chars", "AB"],
["short ascii", "hello"],
["ascii with punctuation", "Hello, World!"],
["mixed symbols", "longer string with numbers 123 and symbols !@#"],
["whitespace only", " \t "],
["control chars", "line1\nline2\tend"],
["unicode latin", "café résumé"],
["unicode emoji", "rocket 🚀 and 👍🏽"],
["cjk", "中文字符串"],
["long string", "The quick brown fox jumps over the lazy dog. ".repeat(20)],
["binary-like bytes", String.fromCharCode(...Array.from({ length: 256 }, (_, i) => i))],
])("should round-trip %s", (_label, value) => {
const encoded = scrambler.encode(value);
const decoded = scrambler.decode(encoded);
expect(decoded).toBe(value);
});
});
describe("round-trip across separate instances with same passphrase+ salt", () => {
test("should decode with a different instance using the same parameters", () => {
const a = new StringScrambler(PASSPHRASE, SALT);
const b = new StringScrambler(PASSPHRASE, SALT);
const value = "cross-instance secret payload";
expect(b.decode(a.encode(value))).toBe(value);
});
test("should round-trip multiple values across instances", () => {
const a = new StringScrambler(PASSPHRASE, SALT);
const b = new StringScrambler(PASSPHRASE, SALT);
for (const value of ["one", "two", "three 🎉", "中文"]) {
expect(b.decode(a.encode(value))).toBe(value);
}
});
});
describe("encoding properties", () => {
const scrambler = new StringScrambler(PASSPHRASE, SALT);
test("should produce a valid base64 string", () => {
expect(scrambler.encode("hello world")).toMatch(
/^[A-Za-z0-9+/]+=*$/,
);
});
test("should produce different ciphertext for the same plaintext", () => {
const value = "same plaintext";
const e1 = scrambler.encode(value);
const e2 = scrambler.encode(value);
expect(e1).not.toBe(e2);
expect(scrambler.decode(e1)).toBe(value);
expect(scrambler.decode(e2)).toBe(value);
});
test("should have a 16-byte encrypted nonce prefix", () => {
const value = "abcdef";
const encoded = scrambler.encode(value);
const buf = Buffer.from(encoded, "base64");
expect(buf.length).toBe(16 + Buffer.byteLength(value, "utf8"));
});
test("nonce should differ between encodings", () => {
const b1 = Buffer.from(scrambler.encode("a"), "base64").subarray(0, 16);
const b2 = Buffer.from(scrambler.encode("a"), "base64").subarray(0, 16);
expect(b1.equals(b2)).toBe(false);
});
test("nonce bytes should be non-zero and not the raw nonce", () => {
const encoded = scrambler.encode("test");
const nonceBytes = Buffer.from(encoded, "base64").subarray(0, 16);
// Encrypted nonce should not be trivially identifiable
const allZero = nonceBytes.every((b) => b === 0);
expect(allZero).toBe(false);
});
});
describe("CBC diffusion", () => {
const scrambler = new StringScrambler(PASSPHRASE, SALT);
test("changing one byte of plaintext changes subsequent ciphertext bytes", () => {
const a = Buffer.from(scrambler.encode("A".repeat(16)), "base64").subarray(16);
const b = Buffer.from(scrambler.encode("B" + "A".repeat(15)), "base64").subarray(16);
// With CBC chaining, the first byte differs AND subsequent bytes propagate
let diffCount = 0;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) diffCount++;
}
expect(diffCount).toBeGreaterThan(1);
});
test("changing one byte mid-string changes all subsequent bytes", () => {
const prefix = "AAAA";
const a = Buffer.from(
scrambler.encode(prefix + "AAAA"),
"base64",
).subarray(16);
const b = Buffer.from(
scrambler.encode(prefix + "BBBB"),
"base64",
).subarray(16);
let diffCount = 0;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) diffCount++;
}
expect(diffCount).toBeGreaterThan(1);
});
});
describe("multi-round", () => {
const scrambler = new StringScrambler(PASSPHRASE, SALT);
test("three rounds still round-trip correctly", () => {
const value = "multi-round verification string";
expect(scrambler.decode(scrambler.encode(value))).toBe(value);
});
test("ciphertext size equals plaintext size plus nonce", () => {
for (const value of ["", "a", "hello world", "x".repeat(1000)]) {
const encoded = scrambler.encode(value);
const buf = Buffer.from(encoded, "base64");
expect(buf.length).toBe(16 + Buffer.byteLength(value, "utf8"));
}
});
});
describe("isolation between parameters", () => {
test("should NOT decode correctly with a different passphrase", () => {
const a = new StringScrambler("pass-one", SALT);
const b = new StringScrambler("pass-two", SALT);
const encoded = a.encode("top secret");
expect(b.decode(encoded)).not.toBe("top secret");
});
test("should NOT decode correctly with a different salt", () => {
const a = new StringScrambler(PASSPHRASE, "salt-one");
const b = new StringScrambler(PASSPHRASE, "salt-two");
const encoded = a.encode("top secret");
expect(b.decode(encoded)).not.toBe("top secret");
});
test("different salts produce completely different ciphertexts", () => {
const a = new StringScrambler(PASSPHRASE, "salt-A");
const b = new StringScrambler(PASSPHRASE, "salt-B");
const e1 = a.encode("same input");
const e2 = b.encode("same input");
expect(e1).not.toBe(e2);
});
});
describe("default (random) parameters", () => {
test("should round-trip within a single instance", () => {
const scrambler = new StringScrambler();
const value = "self-contained secret";
expect(scrambler.decode(scrambler.encode(value))).toBe(value);
});
test("should not be decodable by a fresh default instance", () => {
const a = new StringScrambler();
const b = new StringScrambler();
expect(b.decode(a.encode("hello"))).not.toBe("hello");
});
test("default salt and explicit salt produce different encodings", () => {
const a = new StringScrambler(PASSPHRASE);
const b = new StringScrambler(PASSPHRASE, SALT);
expect(a.encode("test")).not.toBe(b.encode("test"));
});
});
});