@oino-ts/hashid 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/OINOHashid.js +13 -13
- package/dist/esm/OINOHashid.js +12 -12
- package/dist/types/OINOHashid.d.ts +42 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +4 -4
- package/src/OINOHashid.test.ts +43 -12
- package/src/OINOHashid.ts +12 -12
package/dist/cjs/OINOHashid.js
CHANGED
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.OINOHashid = void 0;
|
|
8
|
+
exports.OINOHashid = exports.OINOHASHID_MAX_LENGTH = exports.OINOHASHID_MIN_LENGTH = void 0;
|
|
9
9
|
const node_crypto_1 = require("node:crypto");
|
|
10
10
|
const base_x_1 = require("base-x");
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const hashidEncoder = (0, base_x_1.default)(
|
|
11
|
+
exports.OINOHASHID_MIN_LENGTH = 12;
|
|
12
|
+
exports.OINOHASHID_MAX_LENGTH = 42;
|
|
13
|
+
const OINOHASHID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
14
|
+
const hashidEncoder = (0, base_x_1.default)(OINOHASHID_ALPHABET);
|
|
15
15
|
/**
|
|
16
16
|
* Hashid implementation for OINO API:s for the purpose of making it infeasible to scan
|
|
17
17
|
* through numeric autoinc keys. It's not a solution to keeping the id secret in insecure
|
|
@@ -35,10 +35,10 @@ class OINOHashid {
|
|
|
35
35
|
* @param staticIds whether hash values should remain static per row or random values
|
|
36
36
|
*
|
|
37
37
|
*/
|
|
38
|
-
constructor(key, domainId, minLength =
|
|
38
|
+
constructor(key, domainId, minLength = exports.OINOHASHID_MIN_LENGTH, staticIds = false) {
|
|
39
39
|
this._domainId = domainId;
|
|
40
|
-
if ((minLength <
|
|
41
|
-
throw Error("OINOHashid minLength needs to be between " +
|
|
40
|
+
if ((minLength < exports.OINOHASHID_MIN_LENGTH) || (minLength > exports.OINOHASHID_MAX_LENGTH)) {
|
|
41
|
+
throw Error("OINOHashid minLength (" + minLength + ")needs to be between " + exports.OINOHASHID_MIN_LENGTH + " and " + exports.OINOHASHID_MAX_LENGTH + "!");
|
|
42
42
|
}
|
|
43
43
|
this._minLength = Math.ceil(minLength / 2);
|
|
44
44
|
if (key.length != 32) {
|
|
@@ -59,15 +59,15 @@ class OINOHashid {
|
|
|
59
59
|
// if seed was given use it for pseudorandom chars, otherwise generate them
|
|
60
60
|
let random_chars = "";
|
|
61
61
|
if (this._staticIds) {
|
|
62
|
-
const hmac_seed = (0, node_crypto_1.createHmac)('
|
|
62
|
+
const hmac_seed = (0, node_crypto_1.createHmac)('sha256', this._key);
|
|
63
63
|
hmac_seed.update(this._domainId + " " + cellSeed);
|
|
64
|
-
random_chars = hashidEncoder.encode(hmac_seed.digest());
|
|
64
|
+
random_chars = hashidEncoder.encode(hmac_seed.digest());
|
|
65
65
|
}
|
|
66
66
|
else {
|
|
67
67
|
(0, node_crypto_1.randomFillSync)(this._iv, 0, 16);
|
|
68
|
-
random_chars = hashidEncoder.encode(this._iv);
|
|
68
|
+
random_chars = hashidEncoder.encode(this._iv);
|
|
69
69
|
}
|
|
70
|
-
const hmac = (0, node_crypto_1.createHmac)('
|
|
70
|
+
const hmac = (0, node_crypto_1.createHmac)('sha256', this._key);
|
|
71
71
|
let iv_seed = random_chars.substring(0, this._minLength);
|
|
72
72
|
hmac.update(this._domainId + " " + iv_seed);
|
|
73
73
|
const iv_data = hmac.digest();
|
|
@@ -88,7 +88,7 @@ class OINOHashid {
|
|
|
88
88
|
*/
|
|
89
89
|
decode(hashid) {
|
|
90
90
|
// reproduce nonce from seed
|
|
91
|
-
const hmac = (0, node_crypto_1.createHmac)('
|
|
91
|
+
const hmac = (0, node_crypto_1.createHmac)('sha256', this._key);
|
|
92
92
|
const iv_seed = hashid.substring(0, this._minLength);
|
|
93
93
|
hmac.update(this._domainId + " " + iv_seed);
|
|
94
94
|
const hash = hmac.digest();
|
package/dist/esm/OINOHashid.js
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { createCipheriv, createDecipheriv, createHmac, randomFillSync } from 'node:crypto';
|
|
7
7
|
import basex from 'base-x';
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const hashidEncoder = basex(
|
|
8
|
+
export const OINOHASHID_MIN_LENGTH = 12;
|
|
9
|
+
export const OINOHASHID_MAX_LENGTH = 42;
|
|
10
|
+
const OINOHASHID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
11
|
+
const hashidEncoder = basex(OINOHASHID_ALPHABET);
|
|
12
12
|
/**
|
|
13
13
|
* Hashid implementation for OINO API:s for the purpose of making it infeasible to scan
|
|
14
14
|
* through numeric autoinc keys. It's not a solution to keeping the id secret in insecure
|
|
@@ -32,10 +32,10 @@ export class OINOHashid {
|
|
|
32
32
|
* @param staticIds whether hash values should remain static per row or random values
|
|
33
33
|
*
|
|
34
34
|
*/
|
|
35
|
-
constructor(key, domainId, minLength =
|
|
35
|
+
constructor(key, domainId, minLength = OINOHASHID_MIN_LENGTH, staticIds = false) {
|
|
36
36
|
this._domainId = domainId;
|
|
37
|
-
if ((minLength <
|
|
38
|
-
throw Error("OINOHashid minLength needs to be between " +
|
|
37
|
+
if ((minLength < OINOHASHID_MIN_LENGTH) || (minLength > OINOHASHID_MAX_LENGTH)) {
|
|
38
|
+
throw Error("OINOHashid minLength (" + minLength + ")needs to be between " + OINOHASHID_MIN_LENGTH + " and " + OINOHASHID_MAX_LENGTH + "!");
|
|
39
39
|
}
|
|
40
40
|
this._minLength = Math.ceil(minLength / 2);
|
|
41
41
|
if (key.length != 32) {
|
|
@@ -56,15 +56,15 @@ export class OINOHashid {
|
|
|
56
56
|
// if seed was given use it for pseudorandom chars, otherwise generate them
|
|
57
57
|
let random_chars = "";
|
|
58
58
|
if (this._staticIds) {
|
|
59
|
-
const hmac_seed = createHmac('
|
|
59
|
+
const hmac_seed = createHmac('sha256', this._key);
|
|
60
60
|
hmac_seed.update(this._domainId + " " + cellSeed);
|
|
61
|
-
random_chars = hashidEncoder.encode(hmac_seed.digest());
|
|
61
|
+
random_chars = hashidEncoder.encode(hmac_seed.digest());
|
|
62
62
|
}
|
|
63
63
|
else {
|
|
64
64
|
randomFillSync(this._iv, 0, 16);
|
|
65
|
-
random_chars = hashidEncoder.encode(this._iv);
|
|
65
|
+
random_chars = hashidEncoder.encode(this._iv);
|
|
66
66
|
}
|
|
67
|
-
const hmac = createHmac('
|
|
67
|
+
const hmac = createHmac('sha256', this._key);
|
|
68
68
|
let iv_seed = random_chars.substring(0, this._minLength);
|
|
69
69
|
hmac.update(this._domainId + " " + iv_seed);
|
|
70
70
|
const iv_data = hmac.digest();
|
|
@@ -85,7 +85,7 @@ export class OINOHashid {
|
|
|
85
85
|
*/
|
|
86
86
|
decode(hashid) {
|
|
87
87
|
// reproduce nonce from seed
|
|
88
|
-
const hmac = createHmac('
|
|
88
|
+
const hmac = createHmac('sha256', this._key);
|
|
89
89
|
const iv_seed = hashid.substring(0, this._minLength);
|
|
90
90
|
hmac.update(this._domainId + " " + iv_seed);
|
|
91
91
|
const hash = hmac.digest();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export declare const OINOHASHID_MIN_LENGTH: number;
|
|
2
|
+
export declare const OINOHASHID_MAX_LENGTH: number;
|
|
3
|
+
/**
|
|
4
|
+
* Hashid implementation for OINO API:s for the purpose of making it infeasible to scan
|
|
5
|
+
* through numeric autoinc keys. It's not a solution to keeping the id secret in insecure
|
|
6
|
+
* channels, just making it hard enough to not iterate through the entire key space. Half
|
|
7
|
+
* of the the hashid length is nonce and half cryptotext, i.e. 16 char hashid 8 chars of
|
|
8
|
+
* base64 encoded nonce ~ 6 bytes or 48 bits of entropy.
|
|
9
|
+
*
|
|
10
|
+
*/
|
|
11
|
+
export declare class OINOHashid {
|
|
12
|
+
private _key;
|
|
13
|
+
private _iv;
|
|
14
|
+
private _domainId;
|
|
15
|
+
private _minLength;
|
|
16
|
+
private _staticIds;
|
|
17
|
+
/**
|
|
18
|
+
* Hashid constructor
|
|
19
|
+
*
|
|
20
|
+
* @param key AES128 key (32 char hex-string)
|
|
21
|
+
* @param domainId a sufficiently unique domain ID in which row-Id's are unique
|
|
22
|
+
* @param minLength minimum length of nonce and crypto
|
|
23
|
+
* @param staticIds whether hash values should remain static per row or random values
|
|
24
|
+
*
|
|
25
|
+
*/
|
|
26
|
+
constructor(key: string, domainId: string, minLength?: number, staticIds?: boolean);
|
|
27
|
+
/**
|
|
28
|
+
* Encode given id value as a hashid either using random data or given seed value for nonce.
|
|
29
|
+
*
|
|
30
|
+
* @param id numeric value
|
|
31
|
+
* @param cellSeed a sufficiently unique seed for the current cell to keep hashids unique but persistent (e.g. fieldname + primarykey values)
|
|
32
|
+
*
|
|
33
|
+
*/
|
|
34
|
+
encode(id: string, cellSeed?: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Decode given hashid.
|
|
37
|
+
*
|
|
38
|
+
* @param hashid value
|
|
39
|
+
*
|
|
40
|
+
*/
|
|
41
|
+
decode(hashid: string): string;
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { OINOHashid } from "./OINOHashid.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oino-ts/hashid",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "OINO TS package for hashid's.",
|
|
5
5
|
"author": "Matias Kiviniemi (pragmatta)",
|
|
6
6
|
"license": "MPL-2.0",
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
"types": "./dist/types/index.d.ts",
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@types/node": "^20.12.7",
|
|
21
|
-
"@oino-ts/common": "0.1.
|
|
22
|
-
"base-x": "5.0.0"
|
|
21
|
+
"@oino-ts/common": "0.1.3",
|
|
22
|
+
"base-x": "^5.0.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@oino-ts/types": "0.1.
|
|
25
|
+
"@oino-ts/types": "0.1.3"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
28
|
"src/*.ts",
|
package/src/OINOHashid.test.ts
CHANGED
|
@@ -6,43 +6,74 @@
|
|
|
6
6
|
|
|
7
7
|
import { expect, test } from "bun:test";
|
|
8
8
|
|
|
9
|
-
import { OINOHashid } from "./OINOHashid";
|
|
10
|
-
import { OINOLog, OINOConsoleLog, OINOLogLevel } from "@oino-ts/
|
|
9
|
+
import { OINOHASHID_MAX_LENGTH, OINOHashid } from "./OINOHashid";
|
|
10
|
+
import { OINOLog, OINOConsoleLog, OINOLogLevel } from "@oino-ts/common"
|
|
11
11
|
|
|
12
12
|
Math.random()
|
|
13
13
|
|
|
14
14
|
OINOLog.setLogger(new OINOConsoleLog(OINOLogLevel.error))
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
function benchmarkOINOHashId(hashid: OINOHashid, id: string, iterations: number = 1000): number {
|
|
17
|
+
const start = performance.now();
|
|
18
|
+
|
|
19
|
+
for (let i = 0; i < iterations; i++) {
|
|
20
|
+
const h = hashid.encode(id, '');
|
|
21
|
+
hashid.decode(h)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const end = performance.now();
|
|
25
|
+
const duration = end - start;
|
|
26
|
+
return Math.round(iterations / duration)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await test("OINOHashId persistent", async () => {
|
|
30
|
+
|
|
31
|
+
let hps_min = Number.MAX_VALUE
|
|
32
|
+
let hps_max = 0
|
|
33
|
+
|
|
34
|
+
for (let j=12; j<=OINOHASHID_MAX_LENGTH; j++) {
|
|
35
|
+
const hashid:OINOHashid = new OINOHashid('c7a87c6a5df870842eb6ef6d7937f0b4', 'OINOHashIdTestApp-persistent', j, true)
|
|
19
36
|
let i:number = 1
|
|
20
37
|
let id:string = ''
|
|
21
38
|
while (i <= j) {
|
|
22
39
|
id += i % 10
|
|
23
40
|
const hashed_id = hashid.encode(id, '')
|
|
24
41
|
const id2 = hashid.decode(hashed_id)
|
|
25
|
-
// console.log("j: " + j + ", i: " + i + ", id: " + id + ", hashed_id: " + hashed_id
|
|
42
|
+
// console.log("j: " + j + ", i: " + i + ", id: " + id + ", hashed_id: " + hashed_id)
|
|
26
43
|
expect(id).toMatch(id2)
|
|
27
44
|
i++
|
|
28
45
|
}
|
|
46
|
+
const hps = benchmarkOINOHashId(hashid, id, 4000)
|
|
47
|
+
hps_min = Math.min(hps, hps_min)
|
|
48
|
+
hps_max = Math.max(hps, hps_max)
|
|
49
|
+
expect(hps_min).toBeGreaterThanOrEqual(15)
|
|
50
|
+
expect(hps_max).toBeLessThanOrEqual(25)
|
|
29
51
|
}
|
|
30
|
-
|
|
52
|
+
console.log("OINOHashId persistent performance: " + hps_min + "k - " + hps_max + "k hashes per second")
|
|
31
53
|
})
|
|
32
54
|
|
|
33
|
-
test("OINOHashId random", async () => {
|
|
34
|
-
|
|
35
|
-
|
|
55
|
+
await test("OINOHashId random", async () => {
|
|
56
|
+
|
|
57
|
+
let hps_min = Number.MAX_VALUE
|
|
58
|
+
let hps_max = 0
|
|
59
|
+
|
|
60
|
+
for (let j=12; j<=OINOHASHID_MAX_LENGTH; j++) {
|
|
61
|
+
const hashid:OINOHashid = new OINOHashid('c7a87c6a5df870842eb6ef6d7937f0b4', 'OINOHashIdTestApp-random', j, false)
|
|
36
62
|
let i:number = 1
|
|
37
63
|
let id:string = ''
|
|
38
64
|
while (i <= j) {
|
|
39
65
|
id += i % 10
|
|
40
66
|
const hashed_id = hashid.encode(id, '')
|
|
41
67
|
const id2 = hashid.decode(hashed_id)
|
|
42
|
-
// console.log("j: " + j + ", i: " + i + ", id: " + id + ", hashed_id: " + hashed_id
|
|
68
|
+
// console.log("j: " + j + ", i: " + i + ", id: " + id + ", hashed_id: " + hashed_id)
|
|
43
69
|
expect(id).toMatch(id2)
|
|
44
70
|
i++
|
|
45
71
|
}
|
|
72
|
+
const hps = benchmarkOINOHashId(hashid, id, 4000)
|
|
73
|
+
hps_min = Math.min(hps, hps_min)
|
|
74
|
+
hps_max = Math.max(hps, hps_max)
|
|
46
75
|
}
|
|
47
|
-
|
|
76
|
+
console.log("OINOHashId random performance: " + hps_min + "k - " + hps_max + "k hashes per second")
|
|
48
77
|
})
|
|
78
|
+
|
|
79
|
+
|
package/src/OINOHashid.ts
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
import { BinaryLike, createCipheriv, createDecipheriv, createHmac, randomFillSync } from 'node:crypto';
|
|
8
8
|
import basex from 'base-x'
|
|
9
9
|
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const hashidEncoder = basex(
|
|
10
|
+
export const OINOHASHID_MIN_LENGTH:number = 12
|
|
11
|
+
export const OINOHASHID_MAX_LENGTH:number = 42
|
|
12
|
+
const OINOHASHID_ALPHABET:string = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
13
|
+
const hashidEncoder = basex(OINOHASHID_ALPHABET)
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Hashid implementation for OINO API:s for the purpose of making it infeasible to scan
|
|
@@ -37,10 +37,10 @@ export class OINOHashid {
|
|
|
37
37
|
* @param staticIds whether hash values should remain static per row or random values
|
|
38
38
|
*
|
|
39
39
|
*/
|
|
40
|
-
constructor (key: string, domainId:string, minLength:number =
|
|
40
|
+
constructor (key: string, domainId:string, minLength:number = OINOHASHID_MIN_LENGTH, staticIds:boolean = false) {
|
|
41
41
|
this._domainId = domainId
|
|
42
|
-
if ((minLength <
|
|
43
|
-
throw Error("OINOHashid minLength needs to be between " +
|
|
42
|
+
if ((minLength < OINOHASHID_MIN_LENGTH) || (minLength > OINOHASHID_MAX_LENGTH)) {
|
|
43
|
+
throw Error("OINOHashid minLength (" + minLength + ")needs to be between " + OINOHASHID_MIN_LENGTH + " and " + OINOHASHID_MAX_LENGTH + "!")
|
|
44
44
|
}
|
|
45
45
|
this._minLength = Math.ceil(minLength/2)
|
|
46
46
|
if (key.length != 32) {
|
|
@@ -63,15 +63,15 @@ export class OINOHashid {
|
|
|
63
63
|
// if seed was given use it for pseudorandom chars, otherwise generate them
|
|
64
64
|
let random_chars:string = ""
|
|
65
65
|
if (this._staticIds) {
|
|
66
|
-
const hmac_seed = createHmac('
|
|
66
|
+
const hmac_seed = createHmac('sha256', this._key as BinaryLike)
|
|
67
67
|
hmac_seed.update(this._domainId + " " + cellSeed)
|
|
68
|
-
random_chars = hashidEncoder.encode(hmac_seed.digest() as Uint8Array)
|
|
68
|
+
random_chars = hashidEncoder.encode(hmac_seed.digest() as Uint8Array)
|
|
69
69
|
|
|
70
70
|
} else {
|
|
71
71
|
randomFillSync(this._iv as Uint8Array, 0, 16)
|
|
72
|
-
random_chars = hashidEncoder.encode(this._iv as Uint8Array)
|
|
72
|
+
random_chars = hashidEncoder.encode(this._iv as Uint8Array)
|
|
73
73
|
}
|
|
74
|
-
const hmac = createHmac('
|
|
74
|
+
const hmac = createHmac('sha256', this._key as Uint8Array)
|
|
75
75
|
let iv_seed:string = random_chars.substring(0, this._minLength)
|
|
76
76
|
hmac.update(this._domainId + " " + iv_seed)
|
|
77
77
|
const iv_data:Buffer = hmac.digest()
|
|
@@ -95,7 +95,7 @@ export class OINOHashid {
|
|
|
95
95
|
*/
|
|
96
96
|
decode(hashid:string):string {
|
|
97
97
|
// reproduce nonce from seed
|
|
98
|
-
const hmac = createHmac('
|
|
98
|
+
const hmac = createHmac('sha256', this._key as Uint8Array)
|
|
99
99
|
const iv_seed = hashid.substring(0, this._minLength)
|
|
100
100
|
hmac.update(this._domainId + " " + iv_seed)
|
|
101
101
|
const hash:Buffer = hmac.digest()
|