Skip to content

Commit

Permalink
Add StrictKeymask class
Browse files Browse the repository at this point in the history
  • Loading branch information
turiyadev committed Jan 31, 2024
1 parent b474b21 commit dac3517
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 10 deletions.
27 changes: 22 additions & 5 deletions README.md
Expand Up @@ -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)**

Expand Down Expand Up @@ -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)**

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion 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": {
Expand Down
45 changes: 43 additions & 2 deletions src/Keymask.ts
Expand Up @@ -120,8 +120,8 @@ export class Keymask<T extends KeymaskType> {
} 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
);
Expand Down Expand Up @@ -225,4 +225,45 @@ export class Keymask<T extends KeymaskType> {
: n
) as KeymaskValue<T>;
}
}

/**
* `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<T extends KeymaskType> extends Keymask<T> {

constructor(options?: KeymaskOptions<T>) {
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<T> {
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
);
}
}
9 changes: 8 additions & 1 deletion 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";
115 changes: 114 additions & 1 deletion 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", () => {
Expand Down Expand Up @@ -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);
});
});
});

0 comments on commit dac3517

Please sign in to comment.