diff --git a/README.md b/README.md index 128c2b5..2baad97 100644 --- a/README.md +++ b/README.md @@ -44,10 +44,11 @@ using your preferred package manager (`npm i keymask`, `yarn add keymask`, ## Usage -The module exports three classes, `Keymask`, `KeymaskGenerator` (the LCG) and -`KeymaskEncoder` (the character encoder). These can be used independently of -each other, but for simple use cases, the main `Keymask` class is typically all -you need. +The module exports three main classes, `Keymask`, `KeymaskGenerator` (the LCG) +and `KeymaskEncoder` (the character encoder). These can be used independently +of each other, but for simple use cases, the main `Keymask` class is typically +all you need. There is also an additional class, `StrictKeymask` that can be +used in special cases (described below under the `safe` option). **Example (Default settings)** @@ -144,7 +145,11 @@ const unmask = keymask.unmask("xMMJdmtCcf"); // 123456789 ### `safe` -Safe mode is triggered using a boolean flag on the options object. +Safe mode is triggered using a boolean flag on the options object. This +prevents encoded keymasks from containing any uppercase characters, making it +suitable for use in case-insensitive settings (such as a subdomain). It also +increases the block size from 12 to 14, something to bear in mind when +configuring the output size. **Example (Safe mode)** @@ -159,6 +164,18 @@ const masked = keymask.mask(123456789); // "mfwbdg" const unmask = keymask.unmask("mfwbdg"); // 123456789 ``` +#### `StrictKeymask` + +Some systems, in addition to being case-insensitive, do not allow the first +character of a string to be a numeral. In these cases, the `StrictKeymask` +class can be used as a replacement for the main `Keymask` class. This class +forces `safe` mode, and overrides the `mask` and `unmask` functions so that +initial numeric characters are replaced with a vowel. + +Although this introduces vowels into the encoding, thus the possibility of +recognizable words, offensive words beginning with `e`, `i`, `o` or `u` (and +containing no other vowels) are relatively uncommon. + ### `type` By default, `Keymask` unmasks values as a `number` when possible, while larger diff --git a/package.json b/package.json index cbbe34b..2e8694f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "keymask", - "version": "0.10.0", + "version": "0.10.1", "description": "Map sequential IDs or serial numbers to random-looking strings", "type": "module", "exports": { diff --git a/src/Keymask.ts b/src/Keymask.ts index 98f2fcb..2570b7d 100644 --- a/src/Keymask.ts +++ b/src/Keymask.ts @@ -120,8 +120,8 @@ export class Keymask { } else if (options.seed) { this.encoder = new KeymaskEncoder(options.seed, options.safe); this.generator = new KeymaskGenerator( - options.seed.byteLength < (options.safe ? 20 : 32) - ? void 0 + options.seed.byteLength < (options.safe ? 20 : 32) + ? void 0 : options.seed.slice(options.safe ? 12 : 24), options.safe ); @@ -225,4 +225,45 @@ export class Keymask { : n ) as KeymaskValue; } +} + +/** + * `StrictKeymask` extends the base `Keymask` class, forcing the `safe: true` + * option, while also preventing the first character of a keymask from being a + * number (replacing it with a vowel if present). + */ +export class StrictKeymask extends Keymask { + + constructor(options?: KeymaskOptions) { + super({ ...options, safe: true }); + } + + mask(value: KeymaskData): string { + const result = super.mask(value); + const first = result.charAt(0); + return first === "5" + ? "e" + result.substring(1) + : first === "9" + ? "i" + result.substring(1) + : first === "7" + ? "o" + result.substring(1) + : first === "2" + ? "u" + result.substring(1) + : result; + } + + unmask(value: string): KeymaskValue { + const first = value.charAt(0); + return super.unmask( + first === "e" + ? "5" + value.substring(1) + : first === "i" + ? "9" + value.substring(1) + : first === "o" + ? "7" + value.substring(1) + : first === "u" + ? "2" + value.substring(1) + : value + ); + } } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 1ccfd2e..177f95b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,10 @@ -export { Keymask, type KeymaskOptions } from "./Keymask"; +export { + Keymask, + StrictKeymask, + type KeymaskData, + type KeymaskOptions, + type KeymaskType, + type KeymaskValue +} from "./Keymask"; export { KeymaskEncoder } from "./KeymaskEncoder"; export { KeymaskGenerator } from "./KeymaskGenerator"; \ No newline at end of file diff --git a/test/Keymask.test.ts b/test/Keymask.test.ts index 57d0ef5..facb25c 100644 --- a/test/Keymask.test.ts +++ b/test/Keymask.test.ts @@ -1,5 +1,5 @@ import { equal, deepEqual } from "node:assert/strict"; -import { KeymaskEncoder, Keymask } from "../src/"; +import { KeymaskEncoder, Keymask, StrictKeymask } from "../src/"; describe("Keymask", () => { describe("Default options", () => { @@ -1387,4 +1387,117 @@ describe("Keymask", () => { deepEqual(keymask.unmask("g7qckgtvcghhh2nc"), buffer2); }); }); +}); + +describe("StrictKeymask", () => { + describe("Default options", () => { + const keymask = new StrictKeymask(); + + it("should mask and unmask in range 1", () => { + equal(keymask.mask(1), "k"); + equal(keymask.mask(22), "w"); + equal(keymask.unmask("k"), 1); + equal(keymask.unmask("w"), 22); + }); + + it("should mask and unmask in range 2", () => { + equal(keymask.mask(23), "t2"); + equal(keymask.mask(508), "vw"); + equal(keymask.unmask("t2"), 23); + equal(keymask.unmask("vw"), 508); + }); + + it("should mask and unmask in range 3", () => { + equal(keymask.mask(509), "tbn"); + equal(keymask.mask(8190), "zhq"); + equal(keymask.unmask("tbn"), 509); + equal(keymask.unmask("zhq"), 8190); + }); + + it("should mask and unmask in range 4", () => { + equal(keymask.mask(8191), "wccd"); + equal(keymask.mask(262138), "jfjr"); + equal(keymask.unmask("wccd"), 8191); + equal(keymask.unmask("jfjr"), 262138); + }); + + it("should mask and unmask in range 5", () => { + equal(keymask.mask(262139), "tm2wh"); + equal(keymask.mask(4194300), "tcgpk"); + equal(keymask.unmask("tm2wh"), 262139); + equal(keymask.unmask("tcgpk"), 4194300); + }); + + it("should mask and unmask in range 6", () => { + equal(keymask.mask(4194301), "izn2vj"); + equal(keymask.mask(134217688), "n7dgfq"); + equal(keymask.unmask("izn2vj"), 4194301); + equal(keymask.unmask("n7dgfq"), 134217688); + }); + + it("should mask and unmask in range 7", () => { + equal(keymask.mask(134217689), "jjc2rjd"); + equal(keymask.mask(4294967290), "rmrd5ft"); + equal(keymask.unmask("jjc2rjd"), 134217689); + equal(keymask.unmask("rmrd5ft"), 4294967290); + }); + + it("should mask and unmask in range 8", () => { + equal(keymask.mask(4294967291), "ohwj72gt"); + equal(keymask.mask(68719476730), "wghjnphj"); + equal(keymask.unmask("ohwj72gt"), 4294967291); + equal(keymask.unmask("wghjnphj"), 68719476730); + }); + + it("should mask and unmask in range 9", () => { + equal(keymask.mask(68719476731), "xhkztn29v"); + equal(keymask.mask(2199023255530), "utfrbg7ps"); + equal(keymask.unmask("xhkztn29v"), 68719476731); + equal(keymask.unmask("utfrbg7ps"), 2199023255530); + }); + + it("should mask and unmask in range 10", () => { + equal(keymask.mask(2199023255531), "wyjnjyf5pn"); + equal(keymask.mask(35184372088776), "k5tvhvs9zm"); + equal(keymask.unmask("wyjnjyf5pn"), 2199023255531); + equal(keymask.unmask("k5tvhvs9zm"), 35184372088776); + }); + + it("should mask and unmask in range 11", () => { + equal(keymask.mask(35184372088777), "eyqxcwjgcfv"); + equal(keymask.mask(1125899906842596), "s5xtjhkjzgm"); + equal(keymask.unmask("eyqxcwjgcfv"), 35184372088777); + equal(keymask.unmask("s5xtjhkjzgm"), 1125899906842596); + }); + + it("should mask and unmask in range 12", () => { + equal(keymask.mask(1125899906842597n), "kzd2kmfzbksd"); + equal(keymask.mask(36028797018963912n), "zxdhgvqmsnxp"); + equal(keymask.unmask("kzd2kmfzbksd"), 1125899906842597n); + equal(keymask.unmask("zxdhgvqmsnxp"), 36028797018963912n); + }); + + it("should mask and unmask in range 13", () => { + equal(keymask.mask(36028797018963913n), "fm7mspv5nt2mq"); + equal(keymask.mask(576460752303423432n), "dgwh9ks2mj55k"); + equal(keymask.unmask("fm7mspv5nt2mq"), 36028797018963913n); + equal(keymask.unmask("dgwh9ks2mj55k"), 576460752303423432n); + }); + + it("should mask and unmask in range 14", () => { + equal(keymask.mask(576460752303423433n), "cfkgghxwykkjsj"); + equal(keymask.mask(18446744073709551556n), "xfhpdkrn9fvpxp"); + equal(keymask.unmask("cfkgghxwykkjsj"), 576460752303423433n); + equal(keymask.unmask("xfhpdkrn9fvpxp"), 18446744073709551556n); + }); + + it("should process binary data", () => { + const buffer1 = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]).buffer; + const buffer2 = new Uint8Array([11, 22, 33, 44, 55, 66, 77, 88, 99]).buffer; + equal(keymask.mask(buffer1), "gjph95tz2792txgrfmn9dvvhn9vm"); + equal(keymask.mask(buffer2), "ixcxhygbpfrvngnm"); + deepEqual(keymask.unmask("gjph95tz2792txgrfmn9dvvhn9vm"), buffer1); + deepEqual(keymask.unmask("ixcxhygbpfrvngnm"), buffer2); + }); + }); }); \ No newline at end of file