@tnid/encryption 0.0.4
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/LICENSE +21 -0
- package/README.md +148 -0
- package/esm/_dnt.shims.d.ts +2 -0
- package/esm/_dnt.shims.d.ts.map +1 -0
- package/esm/_dnt.shims.js +57 -0
- package/esm/aes.d.ts +25 -0
- package/esm/aes.d.ts.map +1 -0
- package/esm/aes.js +62 -0
- package/esm/bits.d.ts +45 -0
- package/esm/bits.d.ts.map +1 -0
- package/esm/bits.js +109 -0
- package/esm/encryption.d.ts +56 -0
- package/esm/encryption.d.ts.map +1 -0
- package/esm/encryption.js +194 -0
- package/esm/ff1.d.ts +45 -0
- package/esm/ff1.d.ts.map +1 -0
- package/esm/ff1.js +240 -0
- package/esm/index.d.ts +27 -0
- package/esm/index.d.ts.map +1 -0
- package/esm/index.js +26 -0
- package/esm/package.json +3 -0
- package/package.json +40 -0
- package/script/_dnt.shims.d.ts +2 -0
- package/script/_dnt.shims.d.ts.map +1 -0
- package/script/_dnt.shims.js +60 -0
- package/script/aes.d.ts +25 -0
- package/script/aes.d.ts.map +1 -0
- package/script/aes.js +99 -0
- package/script/bits.d.ts +45 -0
- package/script/bits.d.ts.map +1 -0
- package/script/bits.js +118 -0
- package/script/encryption.d.ts +56 -0
- package/script/encryption.d.ts.map +1 -0
- package/script/encryption.js +202 -0
- package/script/ff1.d.ts +45 -0
- package/script/ff1.d.ts.map +1 -0
- package/script/ff1.js +244 -0
- package/script/index.d.ts +27 -0
- package/script/index.d.ts.map +1 -0
- package/script/index.js +34 -0
- package/script/package.json +3 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael Edlinger
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# @tnid/encryption
|
|
2
|
+
|
|
3
|
+
Format-preserving encryption for TNIDs - hide timestamp information by encrypting V0 TNIDs to V1.
|
|
4
|
+
|
|
5
|
+
## Why Encrypt TNIDs?
|
|
6
|
+
|
|
7
|
+
V0 TNIDs contain a timestamp (like UUIDv7), which reveals when the ID was created. This can leak information you may not want to expose publicly, such as:
|
|
8
|
+
|
|
9
|
+
- When a user account was created
|
|
10
|
+
- The order in which records were created
|
|
11
|
+
- Approximate creation rates
|
|
12
|
+
|
|
13
|
+
By encrypting V0 to V1, you get a valid high-entropy V1 TNID that hides this information while remaining decryptable on the backend.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# npm
|
|
19
|
+
npm install @tnid/encryption @tnid/core
|
|
20
|
+
|
|
21
|
+
# pnpm
|
|
22
|
+
pnpm add @tnid/encryption @tnid/core
|
|
23
|
+
|
|
24
|
+
# bun
|
|
25
|
+
bun add @tnid/encryption @tnid/core
|
|
26
|
+
|
|
27
|
+
# deno
|
|
28
|
+
deno add npm:@tnid/encryption npm:@tnid/core
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Platform Support
|
|
32
|
+
|
|
33
|
+
Requires `globalThis.crypto` (Web Crypto API):
|
|
34
|
+
|
|
35
|
+
- Node.js 20+
|
|
36
|
+
- Deno 1.0+
|
|
37
|
+
- Bun 1.0+
|
|
38
|
+
- Modern browsers (ES2020+)
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { Tnid, TnidType } from "@tnid/core";
|
|
44
|
+
import { EncryptionKey, encryptV0ToV1, decryptV1ToV0 } from "@tnid/encryption";
|
|
45
|
+
|
|
46
|
+
const UserId = Tnid("user");
|
|
47
|
+
type UserId = TnidType<typeof UserId>;
|
|
48
|
+
|
|
49
|
+
// Create an encryption key (16 bytes / 128 bits)
|
|
50
|
+
const key = EncryptionKey.fromHex("0102030405060708090a0b0c0d0e0f10");
|
|
51
|
+
|
|
52
|
+
// Create a time-ordered V0 ID
|
|
53
|
+
const v0 = UserId.new_v0();
|
|
54
|
+
|
|
55
|
+
// Encrypt to V1 before sending to client
|
|
56
|
+
const v1 = await encryptV0ToV1(v0, key);
|
|
57
|
+
|
|
58
|
+
// Decrypt on the backend to recover the original
|
|
59
|
+
const decrypted = await decryptV1ToV0(v1, key);
|
|
60
|
+
// decrypted === v0
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## How It Works
|
|
64
|
+
|
|
65
|
+
The encryption uses Format-Preserving Encryption (FPE) with AES-128 in FF1 mode (NIST SP 800-38G). This encrypts the 100 Payload bits while preserving:
|
|
66
|
+
|
|
67
|
+
- The TNID name (unchanged)
|
|
68
|
+
- The UUID version/variant bits (valid UUIDv8)
|
|
69
|
+
- The overall 128-bit structure
|
|
70
|
+
|
|
71
|
+
The TNID variant changes from V0 to V1, making the encrypted ID indistinguishable from a randomly generated V1 TNID.
|
|
72
|
+
|
|
73
|
+
## API Reference
|
|
74
|
+
|
|
75
|
+
### `EncryptionKey`
|
|
76
|
+
|
|
77
|
+
A 128-bit (16 byte) encryption key.
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// From 32-character hex string
|
|
81
|
+
const key = EncryptionKey.fromHex("0102030405060708090a0b0c0d0e0f10");
|
|
82
|
+
|
|
83
|
+
// From raw bytes
|
|
84
|
+
const key = EncryptionKey.fromBytes(
|
|
85
|
+
new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16])
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Get key bytes (returns a copy)
|
|
89
|
+
const bytes: Uint8Array = key.asBytes();
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### `encryptV0ToV1(tnid, key)`
|
|
93
|
+
|
|
94
|
+
Encrypts a V0 TNID to V1, hiding timestamp information.
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
const v1 = await encryptV0ToV1("user.Br2flcNDfF6LYICnT", key);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
- **Input**: V0 TNID string
|
|
101
|
+
- **Output**: V1 TNID string (same name, encrypted payload)
|
|
102
|
+
- **Idempotent**: If input is already V1, returns it unchanged
|
|
103
|
+
- **Throws**: `EncryptionError` if input is invalid or unsupported variant
|
|
104
|
+
|
|
105
|
+
### `decryptV1ToV0(tnid, key)`
|
|
106
|
+
|
|
107
|
+
Decrypts a V1 TNID back to V0, recovering timestamp information.
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
const v0 = await decryptV1ToV0("user.X3Wxwp0wOy4OZp_rP", key);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- **Input**: V1 TNID string (encrypted)
|
|
114
|
+
- **Output**: V0 TNID string (original with timestamp)
|
|
115
|
+
- **Idempotent**: If input is already V0, returns it unchanged
|
|
116
|
+
- **Throws**: `EncryptionError` if input is invalid or unsupported variant
|
|
117
|
+
|
|
118
|
+
### Error Classes
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { EncryptionKeyError, EncryptionError } from "@tnid/encryption";
|
|
122
|
+
|
|
123
|
+
// EncryptionKeyError - invalid key format
|
|
124
|
+
try {
|
|
125
|
+
EncryptionKey.fromHex("invalid");
|
|
126
|
+
} catch (e) {
|
|
127
|
+
if (e instanceof EncryptionKeyError) {
|
|
128
|
+
console.log("Invalid key:", e.message);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// EncryptionError - encryption/decryption failed
|
|
133
|
+
try {
|
|
134
|
+
await encryptV0ToV1("invalid-tnid", key);
|
|
135
|
+
} catch (e) {
|
|
136
|
+
if (e instanceof EncryptionError) {
|
|
137
|
+
console.log("Encryption failed:", e.message);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Note
|
|
143
|
+
|
|
144
|
+
The encryption functionality is not part of the TNID specification. Encrypted TNIDs are standard V1 TNIDs and remain fully compatible with any TNID implementation.
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_dnt.shims.d.ts","sourceRoot":"","sources":["../src/_dnt.shims.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,aAAa,gCAA2C,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const dntGlobals = {};
|
|
2
|
+
export const dntGlobalThis = createMergeProxy(globalThis, dntGlobals);
|
|
3
|
+
function createMergeProxy(baseObj, extObj) {
|
|
4
|
+
return new Proxy(baseObj, {
|
|
5
|
+
get(_target, prop, _receiver) {
|
|
6
|
+
if (prop in extObj) {
|
|
7
|
+
return extObj[prop];
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
return baseObj[prop];
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
set(_target, prop, value) {
|
|
14
|
+
if (prop in extObj) {
|
|
15
|
+
delete extObj[prop];
|
|
16
|
+
}
|
|
17
|
+
baseObj[prop] = value;
|
|
18
|
+
return true;
|
|
19
|
+
},
|
|
20
|
+
deleteProperty(_target, prop) {
|
|
21
|
+
let success = false;
|
|
22
|
+
if (prop in extObj) {
|
|
23
|
+
delete extObj[prop];
|
|
24
|
+
success = true;
|
|
25
|
+
}
|
|
26
|
+
if (prop in baseObj) {
|
|
27
|
+
delete baseObj[prop];
|
|
28
|
+
success = true;
|
|
29
|
+
}
|
|
30
|
+
return success;
|
|
31
|
+
},
|
|
32
|
+
ownKeys(_target) {
|
|
33
|
+
const baseKeys = Reflect.ownKeys(baseObj);
|
|
34
|
+
const extKeys = Reflect.ownKeys(extObj);
|
|
35
|
+
const extKeysSet = new Set(extKeys);
|
|
36
|
+
return [...baseKeys.filter((k) => !extKeysSet.has(k)), ...extKeys];
|
|
37
|
+
},
|
|
38
|
+
defineProperty(_target, prop, desc) {
|
|
39
|
+
if (prop in extObj) {
|
|
40
|
+
delete extObj[prop];
|
|
41
|
+
}
|
|
42
|
+
Reflect.defineProperty(baseObj, prop, desc);
|
|
43
|
+
return true;
|
|
44
|
+
},
|
|
45
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
46
|
+
if (prop in extObj) {
|
|
47
|
+
return Reflect.getOwnPropertyDescriptor(extObj, prop);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
return Reflect.getOwnPropertyDescriptor(baseObj, prop);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
has(_target, prop) {
|
|
54
|
+
return prop in extObj || prop in baseObj;
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
package/esm/aes.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES-128 block cipher wrapper using Web Crypto API.
|
|
3
|
+
*
|
|
4
|
+
* FF1 requires raw AES block cipher (AES-ECB), which Web Crypto doesn't expose.
|
|
5
|
+
* We simulate AES-ECB using AES-CBC with a zero IV for single-block operations.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* AES-128 block cipher for FF1.
|
|
9
|
+
* Caches the imported CryptoKey for efficiency.
|
|
10
|
+
*/
|
|
11
|
+
export declare class Aes128 {
|
|
12
|
+
private keyPromise;
|
|
13
|
+
constructor(keyBytes: Uint8Array);
|
|
14
|
+
/**
|
|
15
|
+
* Encrypts a single 16-byte block using AES-128.
|
|
16
|
+
* Uses AES-CBC with zero IV, which is equivalent to AES-ECB for single blocks.
|
|
17
|
+
*/
|
|
18
|
+
encryptBlock(block: Uint8Array): Promise<Uint8Array>;
|
|
19
|
+
/**
|
|
20
|
+
* AES-CBC-MAC: Chain multiple blocks using CBC mode.
|
|
21
|
+
* Returns the final encrypted block (the MAC).
|
|
22
|
+
*/
|
|
23
|
+
cbcMac(blocks: Uint8Array[]): Promise<Uint8Array>;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=aes.d.ts.map
|
package/esm/aes.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aes.d.ts","sourceRoot":"","sources":["../src/aes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAeH;;;GAGG;AACH,qBAAa,MAAM;IACjB,OAAO,CAAC,UAAU,CAAqB;gBAE3B,QAAQ,EAAE,UAAU;IAchC;;;OAGG;IACG,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IAmB1D;;;OAGG;IACG,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,UAAU,CAAC;CAgBxD"}
|
package/esm/aes.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES-128 block cipher wrapper using Web Crypto API.
|
|
3
|
+
*
|
|
4
|
+
* FF1 requires raw AES block cipher (AES-ECB), which Web Crypto doesn't expose.
|
|
5
|
+
* We simulate AES-ECB using AES-CBC with a zero IV for single-block operations.
|
|
6
|
+
*/
|
|
7
|
+
// Web Crypto API accessor (same pattern as @tnid/core)
|
|
8
|
+
// deno-lint-ignore no-explicit-any
|
|
9
|
+
import * as dntShim from "./_dnt.shims.js";
|
|
10
|
+
const crypto = dntShim.dntGlobalThis.crypto;
|
|
11
|
+
/**
|
|
12
|
+
* AES-128 block cipher for FF1.
|
|
13
|
+
* Caches the imported CryptoKey for efficiency.
|
|
14
|
+
*/
|
|
15
|
+
export class Aes128 {
|
|
16
|
+
constructor(keyBytes) {
|
|
17
|
+
Object.defineProperty(this, "keyPromise", {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
configurable: true,
|
|
20
|
+
writable: true,
|
|
21
|
+
value: void 0
|
|
22
|
+
});
|
|
23
|
+
if (keyBytes.length !== 16) {
|
|
24
|
+
throw new Error(`AES-128 key must be 16 bytes, got ${keyBytes.length}`);
|
|
25
|
+
}
|
|
26
|
+
// Import key once and cache the promise
|
|
27
|
+
this.keyPromise = crypto.subtle.importKey("raw", keyBytes, { name: "AES-CBC" }, false, ["encrypt"]);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Encrypts a single 16-byte block using AES-128.
|
|
31
|
+
* Uses AES-CBC with zero IV, which is equivalent to AES-ECB for single blocks.
|
|
32
|
+
*/
|
|
33
|
+
async encryptBlock(block) {
|
|
34
|
+
if (block.length !== 16) {
|
|
35
|
+
throw new Error(`AES block must be 16 bytes, got ${block.length}`);
|
|
36
|
+
}
|
|
37
|
+
const key = await this.keyPromise;
|
|
38
|
+
const iv = new Uint8Array(16); // Zero IV
|
|
39
|
+
// AES-CBC with zero IV for single block = AES-ECB
|
|
40
|
+
const result = await crypto.subtle.encrypt({ name: "AES-CBC", iv }, key, block);
|
|
41
|
+
// Result includes padding; we only need the first 16 bytes
|
|
42
|
+
return new Uint8Array(result, 0, 16);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* AES-CBC-MAC: Chain multiple blocks using CBC mode.
|
|
46
|
+
* Returns the final encrypted block (the MAC).
|
|
47
|
+
*/
|
|
48
|
+
async cbcMac(blocks) {
|
|
49
|
+
let state = new Uint8Array(16); // Start with zero block
|
|
50
|
+
for (const block of blocks) {
|
|
51
|
+
// XOR state with block
|
|
52
|
+
const xored = new Uint8Array(16);
|
|
53
|
+
for (let i = 0; i < 16; i++) {
|
|
54
|
+
xored[i] = state[i] ^ block[i];
|
|
55
|
+
}
|
|
56
|
+
// Encrypt and copy to new array to satisfy TypeScript
|
|
57
|
+
const encrypted = await this.encryptBlock(xored);
|
|
58
|
+
state = new Uint8Array(encrypted);
|
|
59
|
+
}
|
|
60
|
+
return state;
|
|
61
|
+
}
|
|
62
|
+
}
|
package/esm/bits.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bit manipulation for TNID encryption.
|
|
3
|
+
*
|
|
4
|
+
* These functions extract and expand the 100 Payload bits that are
|
|
5
|
+
* encrypted/decrypted, matching the Rust implementation exactly.
|
|
6
|
+
*/
|
|
7
|
+
export declare const RIGHT_SECRET_DATA_SECTION_MASK = 1152921504606846975n;
|
|
8
|
+
export declare const MIDDLE_SECRET_DATA_SECTION_MASK = 75539416981840613867520n;
|
|
9
|
+
export declare const LEFT_SECRET_DATA_SECTION_MASK = 324518552449500907168526845870080n;
|
|
10
|
+
export declare const COMPLETE_SECRET_DATA_MASK: bigint;
|
|
11
|
+
export declare const SECRET_DATA_BIT_NUM = 100;
|
|
12
|
+
export declare const HEX_DIGIT_COUNT = 25;
|
|
13
|
+
/**
|
|
14
|
+
* Extracts Payload bits (excludes Name bits, UUID-specific bits, and TNID Variant bits).
|
|
15
|
+
*
|
|
16
|
+
* Compacts the Payload bits from the three sections into a single 100-bit value.
|
|
17
|
+
* The returned bigint will have its lowest 100 bits populated with data,
|
|
18
|
+
* and the highest bits set to zero.
|
|
19
|
+
*/
|
|
20
|
+
export declare function extractSecretDataBits(id: bigint): bigint;
|
|
21
|
+
/**
|
|
22
|
+
* Expands compacted Payload bits back into their positions.
|
|
23
|
+
*
|
|
24
|
+
* This is the inverse of extractSecretDataBits.
|
|
25
|
+
* `bits` should have its lowest 100 bits populated with Payload data.
|
|
26
|
+
*/
|
|
27
|
+
export declare function expandSecretDataBits(bits: bigint): bigint;
|
|
28
|
+
/**
|
|
29
|
+
* Convert 100-bit value to 25 hex digits (each 0-15).
|
|
30
|
+
* Most significant digit first.
|
|
31
|
+
*/
|
|
32
|
+
export declare function toHexDigits(data: bigint): number[];
|
|
33
|
+
/**
|
|
34
|
+
* Convert 25 hex digits back to 100-bit value.
|
|
35
|
+
*/
|
|
36
|
+
export declare function fromHexDigits(digits: number[]): bigint;
|
|
37
|
+
/**
|
|
38
|
+
* Get the TNID variant from a 128-bit ID value.
|
|
39
|
+
*/
|
|
40
|
+
export declare function getVariant(id: bigint): "v0" | "v1" | "v2" | "v3";
|
|
41
|
+
/**
|
|
42
|
+
* Change the variant bits in a 128-bit ID.
|
|
43
|
+
*/
|
|
44
|
+
export declare function setVariant(id: bigint, variant: "v0" | "v1"): bigint;
|
|
45
|
+
//# sourceMappingURL=bits.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bits.d.ts","sourceRoot":"","sources":["../src/bits.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,eAAO,MAAM,8BAA8B,uBAA0C,CAAC;AAGtF,eAAO,MAAM,+BAA+B,2BAA0C,CAAC;AAGvF,eAAO,MAAM,6BAA6B,qCAA0C,CAAC;AAGrF,eAAO,MAAM,yBAAyB,QAGP,CAAC;AAGhC,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAGvC,eAAO,MAAM,eAAe,KAAK,CAAC;AAElC;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAaxD;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAezD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAOlD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAMtD;AAOD;;GAEG;AACH,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAchE;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,CAInE"}
|
package/esm/bits.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bit manipulation for TNID encryption.
|
|
3
|
+
*
|
|
4
|
+
* These functions extract and expand the 100 Payload bits that are
|
|
5
|
+
* encrypted/decrypted, matching the Rust implementation exactly.
|
|
6
|
+
*/
|
|
7
|
+
// Mask for the right-most Payload bits section (bits 0-51, 52 bits)
|
|
8
|
+
export const RIGHT_SECRET_DATA_SECTION_MASK = 0x00000000000000000fffffffffffffffn;
|
|
9
|
+
// Mask for the middle Payload bits section (bits 64-75, 12 bits)
|
|
10
|
+
export const MIDDLE_SECRET_DATA_SECTION_MASK = 0x0000000000000fff0000000000000000n;
|
|
11
|
+
// Mask for the left-most Payload bits section (bits 80-107, 28 bits)
|
|
12
|
+
export const LEFT_SECRET_DATA_SECTION_MASK = 0x00000fffffff00000000000000000000n;
|
|
13
|
+
// Complete mask for all Payload bits (100 bits)
|
|
14
|
+
export const COMPLETE_SECRET_DATA_MASK = RIGHT_SECRET_DATA_SECTION_MASK |
|
|
15
|
+
MIDDLE_SECRET_DATA_SECTION_MASK |
|
|
16
|
+
LEFT_SECRET_DATA_SECTION_MASK;
|
|
17
|
+
// Number of Payload bits
|
|
18
|
+
export const SECRET_DATA_BIT_NUM = 100;
|
|
19
|
+
// Number of hex digits for FF1 (100 bits / 4 bits per hex digit)
|
|
20
|
+
export const HEX_DIGIT_COUNT = 25;
|
|
21
|
+
/**
|
|
22
|
+
* Extracts Payload bits (excludes Name bits, UUID-specific bits, and TNID Variant bits).
|
|
23
|
+
*
|
|
24
|
+
* Compacts the Payload bits from the three sections into a single 100-bit value.
|
|
25
|
+
* The returned bigint will have its lowest 100 bits populated with data,
|
|
26
|
+
* and the highest bits set to zero.
|
|
27
|
+
*/
|
|
28
|
+
export function extractSecretDataBits(id) {
|
|
29
|
+
// Right section stays in place
|
|
30
|
+
let extracted = id & RIGHT_SECRET_DATA_SECTION_MASK;
|
|
31
|
+
// Middle section: shift right by 4 to compact
|
|
32
|
+
const BETWEEN_MIDDLE_RIGHT = 4n;
|
|
33
|
+
extracted = extracted | ((id & MIDDLE_SECRET_DATA_SECTION_MASK) >> BETWEEN_MIDDLE_RIGHT);
|
|
34
|
+
// Left section: shift right by 8 to compact
|
|
35
|
+
const BETWEEN_LEFT_MIDDLE = BETWEEN_MIDDLE_RIGHT + 4n;
|
|
36
|
+
extracted = extracted | ((id & LEFT_SECRET_DATA_SECTION_MASK) >> BETWEEN_LEFT_MIDDLE);
|
|
37
|
+
return extracted;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Expands compacted Payload bits back into their positions.
|
|
41
|
+
*
|
|
42
|
+
* This is the inverse of extractSecretDataBits.
|
|
43
|
+
* `bits` should have its lowest 100 bits populated with Payload data.
|
|
44
|
+
*/
|
|
45
|
+
export function expandSecretDataBits(bits) {
|
|
46
|
+
// Right section stays in place
|
|
47
|
+
let expanded = bits & RIGHT_SECRET_DATA_SECTION_MASK;
|
|
48
|
+
// Middle section shifts left
|
|
49
|
+
const BETWEEN_MIDDLE_RIGHT = 4n;
|
|
50
|
+
const middleMask = MIDDLE_SECRET_DATA_SECTION_MASK >> BETWEEN_MIDDLE_RIGHT;
|
|
51
|
+
expanded = expanded | ((bits & middleMask) << BETWEEN_MIDDLE_RIGHT);
|
|
52
|
+
// Left section shifts left
|
|
53
|
+
const BETWEEN_LEFT_MIDDLE = BETWEEN_MIDDLE_RIGHT + 4n;
|
|
54
|
+
const leftMask = LEFT_SECRET_DATA_SECTION_MASK >> BETWEEN_LEFT_MIDDLE;
|
|
55
|
+
expanded = expanded | ((bits & leftMask) << BETWEEN_LEFT_MIDDLE);
|
|
56
|
+
return expanded;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Convert 100-bit value to 25 hex digits (each 0-15).
|
|
60
|
+
* Most significant digit first.
|
|
61
|
+
*/
|
|
62
|
+
export function toHexDigits(data) {
|
|
63
|
+
const hexDigits = new Array(HEX_DIGIT_COUNT);
|
|
64
|
+
for (let i = 0; i < HEX_DIGIT_COUNT; i++) {
|
|
65
|
+
const shift = BigInt((HEX_DIGIT_COUNT - 1 - i) * 4);
|
|
66
|
+
hexDigits[i] = Number((data >> shift) & 0xfn);
|
|
67
|
+
}
|
|
68
|
+
return hexDigits;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Convert 25 hex digits back to 100-bit value.
|
|
72
|
+
*/
|
|
73
|
+
export function fromHexDigits(digits) {
|
|
74
|
+
let result = 0n;
|
|
75
|
+
for (const digit of digits) {
|
|
76
|
+
result = (result << 4n) | BigInt(digit);
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
// Variant bit positions (bits 60-61 in the 128-bit TNID)
|
|
81
|
+
const VARIANT_MASK = 3n << 60n;
|
|
82
|
+
const V0_VARIANT = 0n << 60n;
|
|
83
|
+
const V1_VARIANT = 1n << 60n;
|
|
84
|
+
/**
|
|
85
|
+
* Get the TNID variant from a 128-bit ID value.
|
|
86
|
+
*/
|
|
87
|
+
export function getVariant(id) {
|
|
88
|
+
const variantBits = (id >> 60n) & 3n;
|
|
89
|
+
switch (variantBits) {
|
|
90
|
+
case 0n:
|
|
91
|
+
return "v0";
|
|
92
|
+
case 1n:
|
|
93
|
+
return "v1";
|
|
94
|
+
case 2n:
|
|
95
|
+
return "v2";
|
|
96
|
+
case 3n:
|
|
97
|
+
return "v3";
|
|
98
|
+
default:
|
|
99
|
+
throw new Error("Unreachable");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Change the variant bits in a 128-bit ID.
|
|
104
|
+
*/
|
|
105
|
+
export function setVariant(id, variant) {
|
|
106
|
+
const cleared = id & ~VARIANT_MASK;
|
|
107
|
+
const variantBits = variant === "v0" ? V0_VARIANT : V1_VARIANT;
|
|
108
|
+
return cleared | variantBits;
|
|
109
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TNID encryption using FF1 Format-Preserving Encryption.
|
|
3
|
+
*
|
|
4
|
+
* Provides functions to convert V0 (time-ordered) TNIDs to V1 (random-looking)
|
|
5
|
+
* TNIDs and vice versa, hiding timestamp information while remaining reversible.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Error when creating an EncryptionKey.
|
|
9
|
+
*/
|
|
10
|
+
export declare class EncryptionKeyError extends Error {
|
|
11
|
+
constructor(message: string);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Error when encrypting or decrypting a TNID.
|
|
15
|
+
*/
|
|
16
|
+
export declare class EncryptionError extends Error {
|
|
17
|
+
constructor(message: string);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A 128-bit (16 byte) encryption key for TNID encryption.
|
|
21
|
+
*/
|
|
22
|
+
export declare class EncryptionKey {
|
|
23
|
+
private readonly bytes;
|
|
24
|
+
private constructor();
|
|
25
|
+
/**
|
|
26
|
+
* Creates a new encryption key from raw bytes.
|
|
27
|
+
*/
|
|
28
|
+
static fromBytes(bytes: Uint8Array): EncryptionKey;
|
|
29
|
+
/**
|
|
30
|
+
* Creates an encryption key from a 32-character hex string.
|
|
31
|
+
*/
|
|
32
|
+
static fromHex(hex: string): EncryptionKey;
|
|
33
|
+
/**
|
|
34
|
+
* Returns the key as a byte array.
|
|
35
|
+
*/
|
|
36
|
+
asBytes(): Uint8Array;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Encrypts a V0 TNID to V1, hiding timestamp information.
|
|
40
|
+
*
|
|
41
|
+
* @param tnid The V0 TNID string to encrypt
|
|
42
|
+
* @param key The encryption key
|
|
43
|
+
* @returns The encrypted V1 TNID string
|
|
44
|
+
* @throws EncryptionError if the TNID is not V0 or is invalid
|
|
45
|
+
*/
|
|
46
|
+
export declare function encryptV0ToV1(tnid: string, key: EncryptionKey): Promise<string>;
|
|
47
|
+
/**
|
|
48
|
+
* Decrypts a V1 TNID back to V0, recovering timestamp information.
|
|
49
|
+
*
|
|
50
|
+
* @param tnid The V1 TNID string to decrypt
|
|
51
|
+
* @param key The encryption key (must match the one used for encryption)
|
|
52
|
+
* @returns The decrypted V0 TNID string
|
|
53
|
+
* @throws EncryptionError if the TNID is not V1 or is invalid
|
|
54
|
+
*/
|
|
55
|
+
export declare function decryptV1ToV0(tnid: string, key: EncryptionKey): Promise<string>;
|
|
56
|
+
//# sourceMappingURL=encryption.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encryption.d.ts","sourceRoot":"","sources":["../src/encryption.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAmBH;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAa;IAEnC,OAAO;IAIP;;OAEG;IACH,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,UAAU,GAAG,aAAa;IASlD;;OAEG;IACH,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa;IAsB1C;;OAEG;IACH,OAAO,IAAI,UAAU;CAGtB;AA6DD;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAmCrF;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAmCrF"}
|