├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .vscode
├── extensions.json
└── settings.json
├── README.md
├── cli.ts
├── deno.json
├── diagram.png
├── hasher.test.ts
├── hasher.ts
├── mod.ts
├── prime.bench.ts
├── prime.ts
└── scripts
└── build_npm.ts
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | deno-version: [v1.x]
15 | steps:
16 | - name: Git Checkout Deno Module
17 | uses: actions/checkout@v2
18 | - name: Use Deno Version ${{ matrix.deno-version }}
19 | uses: denoland/setup-deno@v1
20 | with:
21 | deno-version: ${{ matrix.deno-version }}
22 | - name: Format
23 | run: deno fmt --check
24 | - name: Lint
25 | run: deno lint
26 | - name: Unit
27 | run: deno test --coverage=coverage
28 | - name: Create coverage report
29 | run: deno coverage ./coverage --lcov > coverage.lcov
30 | - name: Collect coverage
31 | uses: codecov/codecov-action@v1.0.10
32 | with:
33 | file: ./coverage.lcov
34 | - name: Build Module
35 | run: deno task build:npm
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .npm
2 | deno.lock
3 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "github.vscode-github-actions",
4 | "denoland.vscode-deno",
5 | "streetsidesoftware.code-spell-checker"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "deno.enable": true,
3 | "deno.lint": true,
4 | "editor.formatOnSave": true,
5 | "editor.defaultFormatter": "denoland.vscode-deno",
6 | "cSpell.words": [
7 | "bunx",
8 | "deno",
9 | "denostack",
10 | "hasher",
11 | "inthash"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # inthash
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | inthash is a versatile library for generating integer hash values in Javascript
16 | and Typescript using Knuth's multiplicative method. With a user-friendly
17 | interface, this library allows you to obfuscate predictable numbers, making it
18 | ideal for scenarios like 'Auto Increment' values in databases. inthash supports
19 | `number`, `string`, `bigint`.
20 |
21 | ## Installation
22 |
23 | **Node.js**
24 |
25 | ```bash
26 | npm install inthash
27 | ```
28 |
29 | **Deno**
30 |
31 | ```ts
32 | import { Hasher } from "https://deno.land/x/inthash/mod.ts";
33 | ```
34 |
35 | ## Usage
36 |
37 | ### Generating Random Settings
38 |
39 | Run the following command to generate random settings for your hasher:
40 |
41 | ```bash
42 | # Node.js:
43 | npx inthash
44 |
45 | # Deno:
46 | deno run jsr:@denostack/inthash/cli
47 |
48 | # Bun
49 | bunx inthash
50 |
51 | # Output:
52 | # {
53 | # "bits": 53,
54 | # "prime": "6456111708547433",
55 | # "inverse": "3688000043513561",
56 | # "xor": "969402349590075"
57 | # }
58 | ```
59 |
60 | ### Creating and Using a Hasher
61 |
62 | Create a hasher with the generated settings:
63 |
64 | ```ts
65 | const hasher = new Hasher({
66 | bits: 53, // Javascript, Number.MAX_SAFE_INTEGER
67 | prime: "6456111708547433", // Random Prime
68 | inverse: "3688000043513561", // Modular Inverse
69 | xor: "969402349590075", // Random n-bit xor mask
70 | });
71 |
72 | const encoded = hasher.encode(100); // result: 6432533451586367
73 | const decoded = hasher.decode(encoded); // result: 100
74 | ```
75 |
76 | 
77 |
78 | ```ts
79 | // You can obfuscate predictable numbers like 'Auto Increment'!
80 | hasher.encode(0); // 969402349590075
81 | hasher.encode(1); // 6085136369434450
82 | hasher.encode(2); // 4132187376469225
83 | hasher.encode(3); // 2180123214014976
84 |
85 | hasher.encode(Number.MAX_SAFE_INTEGER - 3); // 2024647471942759
86 | hasher.encode(Number.MAX_SAFE_INTEGER - 2); // 6827076040726014
87 | hasher.encode(Number.MAX_SAFE_INTEGER - 1); // 4875011878271765
88 | hasher.encode(Number.MAX_SAFE_INTEGER); // 2922062885306540
89 | ```
90 |
91 | inthash also supports `string` and `bigint` values:
92 |
93 | ```ts
94 | // String input and output
95 | const encoded = hasher.encode("100"); // "6432533451586367"
96 | const decoded = hasher.decode(encoded); // "100"
97 | ```
98 |
99 | ```ts
100 | // BigInt input and output
101 | const encoded = hasher.encode(100n); // 6432533451586367n
102 | const decoded = hasher.decode(encoded); // 100n
103 | ```
104 |
105 | ### Handling MySQL `bigint(20)`
106 |
107 | To work with `bigint(20)` in MySQL, you need to handle 64-bit values. The old
108 | version of IntHash supported up to 53-bit values
109 | (`Number.MAX_SAFE_INTEGER === 2**53 - 1`). From v3 onwards, n-bit values are
110 | supported:
111 |
112 | ```bash
113 | # Node.js:
114 | npx inthash -b64
115 |
116 | # Deno:
117 | deno run https://deno.land/x/inthash/cli.ts -b64
118 |
119 | # Output:
120 | # {
121 | # "bits": 64,
122 | # "prime": "16131139598801670337",
123 | # "inverse": "14287487925114175297",
124 | # "xor": "8502035541264656686"
125 | # }
126 | ```
127 |
128 | ## See also
129 |
130 | - [optimus](https://github.com/jenssegers/optimus) A PHP implementation of
131 | Knuth's multiplicative hashing method. inthash is inspired by and ported from
132 | this library.
133 |
--------------------------------------------------------------------------------
/cli.ts:
--------------------------------------------------------------------------------
1 | // deno-lint-ignore-file no-explicit-any
2 |
3 | import { Hasher } from "./hasher.ts";
4 |
5 | const isDeno = typeof (globalThis as any).Deno !== "undefined";
6 |
7 | const cmd = isDeno ? "deno run jsr:@denostack/inthash/cli" : "npx inthash";
8 |
9 | const rawArgs = isDeno
10 | ? (globalThis as any).Deno.args
11 | : (globalThis as any).process.argv.slice(2);
12 | const cmdSuffix = rawArgs.join(" ");
13 | const args = parse(rawArgs);
14 |
15 | const bit = args.b ?? args.bit ?? args.bits ?? 53;
16 | const options = Hasher.generate(bit);
17 | const hasher = new Hasher(options);
18 |
19 | console.log(JSON.stringify(options, null, " "));
20 | console.error(`
21 | $ ${cmd}${cmdSuffix ? " " + cmdSuffix : ""} | pbcopy
22 |
23 | Now go ahead and paste it into your code! Good luck. :-)
24 |
25 | Note: The supported range of integers is from min: 0 to max: ${hasher._max}.
26 | Please make sure your inputs fall within this range.`);
27 |
28 | type Args = {
29 | b?: number;
30 | bit?: number;
31 | bits?: number;
32 | };
33 |
34 | function parse(args: string[]): Args {
35 | const argv: Args = {};
36 |
37 | for (let i = 0; i < args.length; i++) {
38 | const arg = args[i];
39 | let match;
40 | if ((match = arg.match(/^--(b|bit|bits)=(\d+)/))) {
41 | const [, key, value] = match;
42 | argv[key as "b" | "bit" | "bits"] = +value;
43 | } else if ((match = arg.match(/^--(b|bit|bits)$/))) {
44 | const [, key] = match;
45 | const next = args[i + 1];
46 | if (
47 | next &&
48 | /\d+/.test(next)
49 | ) {
50 | argv[key as "b" | "bit" | "bits"] = +next;
51 | i++;
52 | }
53 | } else if ((match = arg.match(/^-b(\d+)/))) {
54 | argv.b = +match[1];
55 | }
56 | }
57 | return argv;
58 | }
59 |
--------------------------------------------------------------------------------
/deno.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@denostack/inthash",
3 | "version": "3.0.4",
4 | "tasks": {
5 | "test": "deno task test:unit && deno task test:lint && deno task test:format && deno task test:types",
6 | "test:format": "deno fmt --check",
7 | "test:lint": "deno lint",
8 | "test:unit": "deno test -A",
9 | "test:types": "deno check mod.ts",
10 | "build:npm": "deno run --allow-sys --allow-env --allow-read --allow-write --allow-net --allow-run scripts/build_npm.ts"
11 | },
12 | "imports": {
13 | "@deno/dnt": "jsr:@deno/dnt@^0.41.1",
14 | "@std/assert": "jsr:@std/assert@^0.222.0",
15 | "@std/fmt": "jsr:@std/fmt@^0.222.0",
16 | "@std/testing": "jsr:@std/testing@^0.222.1"
17 | },
18 | "exports": {
19 | ".": "./mod.ts",
20 | "./cli": "./cli.ts",
21 | "./hasher": "./hasher.ts"
22 | },
23 | "lint": {
24 | "exclude": [".npm"]
25 | },
26 | "fmt": {
27 | "exclude": [".npm"]
28 | },
29 | "lock": false
30 | }
31 |
--------------------------------------------------------------------------------
/diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denostack/inthash/0e1b7076641ed5f4c0bd66dbcd47757ac8b9e7d1/diagram.png
--------------------------------------------------------------------------------
/hasher.test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals, assertNotEquals } from "@std/assert";
2 | import { assertSpyCall, assertSpyCalls, spy } from "@std/testing/mock";
3 | import { Hasher } from "./hasher.ts";
4 |
5 | Deno.test("hasher, encode and decode", () => {
6 | for (const bits of [16, 32, 53, 64, 128]) {
7 | let runs = 0;
8 | for (let r = 0; r < 5; r++) {
9 | const options = Hasher.generate(bits);
10 |
11 | assertEquals(options.bits, bits);
12 | assertEquals(BigInt(options.prime).toString(2).length, bits);
13 |
14 | const hasher = new Hasher(options);
15 |
16 | let n = 0n;
17 | const limitN = 2n ** BigInt(options.bits);
18 | while (n < limitN) {
19 | {
20 | const encoded = hasher.encode(n);
21 | assertEquals(typeof encoded, "bigint");
22 |
23 | const decoded = hasher.decode(encoded);
24 |
25 | assertEquals(decoded, n);
26 | assertEquals(typeof decoded, "bigint");
27 | }
28 | {
29 | const nAsString = n.toString();
30 | const encoded = hasher.encode(nAsString);
31 | assertEquals(typeof encoded, "string");
32 |
33 | const decoded = hasher.decode(encoded);
34 |
35 | assertEquals(decoded, nAsString);
36 | assertEquals(typeof decoded, "string");
37 | }
38 |
39 | const rand = Math.pow(2, Math.floor(Math.random() * (bits - 6)));
40 | n += BigInt(rand);
41 | runs++;
42 | }
43 |
44 | // last
45 | n = limitN - 1n;
46 | const encoded = hasher.encode(n);
47 |
48 | assertNotEquals(encoded, n);
49 | assertEquals(hasher.decode(encoded), n);
50 | }
51 |
52 | assertEquals(runs > 0, true);
53 | }
54 | });
55 |
56 | Deno.test("README.md sample", () => {
57 | const hasher = new Hasher({
58 | bits: 53, // Javascript, Number.MAX_SAFE_INTEGER
59 | prime: "6456111708547433",
60 | inverse: "3688000043513561",
61 | xor: "969402349590075",
62 | });
63 |
64 | const encoded = hasher.encode("100");
65 | const decoded = hasher.decode(encoded);
66 |
67 | assertEquals(encoded, "6432533451586367");
68 | assertEquals(decoded, "100");
69 |
70 | assertEquals(hasher.encode(1), 6085136369434450);
71 | assertEquals(hasher.encode(2), 4132187376469225);
72 | assertEquals(hasher.encode(3), 2180123214014976);
73 | assertEquals(hasher.encode(4), 6982551782798239);
74 | assertEquals(hasher.encode(5), 5030633649101110);
75 | assertEquals(hasher.encode(6), 3077950944243277);
76 | assertEquals(hasher.encode(7), 1125015438342116);
77 | });
78 |
79 | Deno.test("full coverage of 8bit", () => {
80 | for (let run = 0; run < 100; run++) {
81 | const hasher = new Hasher(Hasher.generate(8));
82 |
83 | for (let i = 0; i < 256; i++) {
84 | assertEquals(hasher.decode(hasher.encode(i)), i);
85 | }
86 | }
87 | });
88 |
89 | Deno.test("overflow", () => {
90 | const hasher = new Hasher(Hasher.generate(8));
91 |
92 | const logSpy = spy(console, "warn");
93 |
94 | hasher.encode(256);
95 |
96 | assertSpyCall(logSpy, 0, {
97 | args: ["input 256 is greater than max 255"],
98 | });
99 | assertSpyCalls(logSpy, 1);
100 | });
101 |
--------------------------------------------------------------------------------
/hasher.ts:
--------------------------------------------------------------------------------
1 | import { randomPrime } from "./prime.ts";
2 |
3 | function modInv(a: bigint, b: bigint): bigint {
4 | let t = 0n;
5 | let r = b;
6 | let nextT = 1n;
7 | let nextR = a;
8 | while (nextR > 0) {
9 | const q = ~~(r / nextR);
10 | const [lastT, lastR] = [t, r];
11 | [t, r] = [nextT, nextR];
12 | [nextT, nextR] = [lastT - q * nextT, lastR - q * nextR];
13 | }
14 | return t < 0 ? t + b : t;
15 | }
16 |
17 | function generateXor(bits: number): bigint {
18 | let result = 0n;
19 | for (let i = 0; i < bits; i++) {
20 | result = (result << 1n) | (Math.random() < 0.5 ? 1n : 0n);
21 | }
22 | return result;
23 | }
24 |
25 | export interface HasherOptions {
26 | bits: number;
27 | prime: string;
28 | inverse: string;
29 | xor: string;
30 | }
31 |
32 | export class Hasher {
33 | static generate(
34 | bits?: number,
35 | ): HasherOptions {
36 | bits = bits ?? Number.MAX_SAFE_INTEGER.toString(2).length; // default to Number.MAX_SAFE_INTEGER
37 | if (bits < 2) {
38 | throw new Error("bits must be greater than 2");
39 | }
40 | const modBase = 2n ** BigInt(bits);
41 | const prime = randomPrime(bits);
42 | return {
43 | bits,
44 | prime: prime.toString(),
45 | inverse: modInv(prime, modBase).toString(),
46 | xor: generateXor(bits).toString(),
47 | };
48 | }
49 |
50 | _prime: bigint;
51 | _inverse: bigint;
52 | _xor: bigint;
53 | _mask: bigint;
54 | _max: bigint;
55 |
56 | constructor({ bits, prime, inverse, xor }: HasherOptions) {
57 | this._prime = BigInt(prime);
58 | this._inverse = BigInt(inverse);
59 | this._xor = BigInt(xor);
60 | this._mask = 2n ** BigInt(bits) - 1n;
61 | this._max = (1n << BigInt(bits)) - 1n;
62 | }
63 |
64 | encode(n: number): number;
65 | encode(n: bigint): bigint;
66 | encode(n: string): string;
67 | encode(n: number | bigint | string): number | bigint | string {
68 | if (typeof n === "string") {
69 | return this.encode(BigInt(n)).toString();
70 | }
71 | if (typeof n === "number") {
72 | return Number(this.encode(BigInt(n)));
73 | }
74 | if (n > this._max) {
75 | console.warn(`input ${n} is greater than max ${this._max}`);
76 | }
77 | return n * this._prime & this._mask ^ this._xor;
78 | }
79 |
80 | decode(n: number): number;
81 | decode(n: bigint): bigint;
82 | decode(n: string): string;
83 | decode(n: number | bigint | string): number | bigint | string {
84 | if (typeof n === "string") {
85 | return this.decode(BigInt(n)).toString();
86 | }
87 | if (typeof n === "number") {
88 | return Number(this.decode(BigInt(n)));
89 | }
90 | return (n ^ this._xor) * this._inverse & this._mask;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/mod.ts:
--------------------------------------------------------------------------------
1 | export type { HasherOptions } from "./hasher.ts";
2 | export { Hasher } from "./hasher.ts";
3 |
--------------------------------------------------------------------------------
/prime.bench.ts:
--------------------------------------------------------------------------------
1 | import { isPrimeMillerRabin } from "./prime.ts";
2 |
3 | function isPrime(n: bigint) {
4 | if (n === 2n) {
5 | return true;
6 | }
7 | for (let i = 3n; i * i <= n; i++) {
8 | if (n % i === 0n) {
9 | return false;
10 | }
11 | }
12 | return true;
13 | }
14 |
15 | const primes = [
16 | 53912869n,
17 | 6067841561n,
18 | ];
19 |
20 | Deno.bench({ name: "isPrime", group: "prime" }, () => {
21 | for (const p of primes) {
22 | isPrime(p);
23 | }
24 | });
25 |
26 | Deno.bench(
27 | { name: "isPrimeMillerRabin", group: "prime", baseline: true },
28 | () => {
29 | for (const p of primes) {
30 | isPrimeMillerRabin(p);
31 | }
32 | },
33 | );
34 |
--------------------------------------------------------------------------------
/prime.ts:
--------------------------------------------------------------------------------
1 | function modPow(x: bigint, y: bigint, p: bigint) {
2 | x = x % p;
3 |
4 | let result = 1n;
5 | while (y > 0n) {
6 | if (y & 1n) {
7 | result = (result * x) % p;
8 | }
9 |
10 | y = y / 2n;
11 | x = (x * x) % p;
12 | }
13 | return result;
14 | }
15 |
16 | function randomBigInt(range: bigint): bigint {
17 | const n = range.toString(2).length;
18 | if (n === 1) {
19 | return 0n;
20 | }
21 | if (n < 52) {
22 | return BigInt(Math.floor(Math.random() * Number(range)));
23 | }
24 |
25 | return BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)); // TODO
26 | }
27 |
28 | // https://github.com/openssl/openssl/blob/4cedf30e995f9789cf6bb103e248d33285a84067/crypto/bn/bn_prime.c#L337
29 | export function isPrimeMillerRabin(w: bigint, iterations?: number): boolean {
30 | if (w === 2n) {
31 | return true;
32 | }
33 | if (w < 2n || w % 2n === 0n) {
34 | return false;
35 | }
36 |
37 | const w1 = w - 1n;
38 | const w3 = w - 3n;
39 | let a = 1;
40 | let m = w1;
41 | // (Step 1) Calculate largest integer 'a' such that 2^a divides w-1
42 | // (Step 2) m = (w-1) / 2^a
43 | while (m % 2n === 0n) {
44 | a++;
45 | m /= 2n;
46 | }
47 |
48 | // TODO montgomery
49 |
50 | iterations = iterations ?? w.toString(2).length > 2048 ? 128 : 64;
51 |
52 | // (Step 4)
53 | outer_loop:
54 | for (let i = 0; i < iterations; i++) {
55 | // (Step 4.1) obtain a Random string of bits b where 1 < b < w-1 */
56 | const b = randomBigInt(w3) + 2n;
57 |
58 | // (Step 4.5) z = b^m mod w
59 | // in openssl, Montgomery modular multiplication (TODO)
60 | let z = modPow(b, m, w);
61 |
62 | /* (Step 4.6) if (z = 1 or z = w-1) */
63 | if (z === 1n || z === w1) {
64 | continue outer_loop;
65 | }
66 | /* (Step 4.7) for j = 1 to a-1 */
67 | for (let j = 1; j < a; j++) {
68 | // (Step 4.7.1 - 4.7.2) x = z, z = x^2 mod w
69 | z = modPow(z, 2n, w);
70 |
71 | // (Step 4.7.3)
72 | if (z === w1) {
73 | continue outer_loop;
74 | }
75 | // (Step 4.7.4)
76 | if (z === 1n) {
77 | return false;
78 | }
79 | }
80 | return false;
81 | }
82 | return true;
83 | }
84 |
85 | function randomOdd(bits: number): bigint {
86 | let result = 1n;
87 | for (let i = 2; i < bits; i++) {
88 | result = (result << 1n) | (Math.random() < 0.5 ? 1n : 0n);
89 | }
90 | return (result << 1n) | 1n;
91 | }
92 |
93 | export function randomPrime(bits: number): bigint {
94 | if (bits < 2) {
95 | return 1n;
96 | }
97 | let result = randomOdd(bits);
98 | while (!isPrimeMillerRabin(result)) {
99 | result += 2n;
100 | if (result.toString(2).length > bits) {
101 | result = randomOdd(bits);
102 | }
103 | }
104 | return result;
105 | }
106 |
--------------------------------------------------------------------------------
/scripts/build_npm.ts:
--------------------------------------------------------------------------------
1 | import { build, emptyDir } from "@deno/dnt";
2 | import { bgGreen } from "@std/fmt/colors";
3 |
4 | const denoInfo = JSON.parse(
5 | Deno.readTextFileSync(new URL("../deno.json", import.meta.url)),
6 | );
7 | const version = denoInfo.version;
8 |
9 | console.log(bgGreen(`version: ${version}`));
10 |
11 | await emptyDir("./.npm");
12 |
13 | await build({
14 | entryPoints: [
15 | "./mod.ts",
16 | {
17 | kind: "bin",
18 | name: "inthash",
19 | path: "./cli.ts",
20 | },
21 | ],
22 | outDir: "./.npm",
23 | shims: {
24 | deno: false,
25 | },
26 | test: false,
27 | compilerOptions: {
28 | lib: ["ES2020", "DOM"],
29 | },
30 | package: {
31 | name: "inthash",
32 | version,
33 | description:
34 | "Efficient integer hashing library using Knuth's multiplicative method for Javascript and Typescript, perfect for obfuscating sequential numbers.",
35 | keywords: [
36 | "id obfuscation",
37 | "obfuscate",
38 | "obfuscation",
39 | "knuth",
40 | "uuid",
41 | "hash",
42 | "auto-increment",
43 | "optimus",
44 | "bigint",
45 | "typescript",
46 | ],
47 | author: "Changwan Jun ",
48 | license: "MIT",
49 | repository: {
50 | type: "git",
51 | url: "git://github.com/denostack/inthash.git",
52 | },
53 | bugs: {
54 | url: "https://github.com/denostack/inthash/issues",
55 | },
56 | },
57 | });
58 |
59 | // post build steps
60 | Deno.copyFileSync("README.md", ".npm/README.md");
61 |
--------------------------------------------------------------------------------