From 230750cadf6bee12b0bd9bb67d2d70915a207d15 Mon Sep 17 00:00:00 2001 From: shai_hulud Date: Fri, 3 Jul 2026 02:48:06 +0000 Subject: [PATCH] Upload files to "tests/scramble" --- tests/scramble/stringtool.test.ts | 182 ++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 tests/scramble/stringtool.test.ts diff --git a/tests/scramble/stringtool.test.ts b/tests/scramble/stringtool.test.ts new file mode 100644 index 0000000..394be5f --- /dev/null +++ b/tests/scramble/stringtool.test.ts @@ -0,0 +1,182 @@ +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")); + }); + }); +}); \ No newline at end of file