├── .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 | Build 5 | Coverage 6 | License 7 | Language Typescript 8 |
9 | JSR version 10 | Deno version 11 | NPM Version 12 | Downloads 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 | ![diagram](./diagram.png) 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 | --------------------------------------------------------------------------------