@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/script/ff1.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* FF1 Format-Preserving Encryption (NIST SP 800-38G).
|
|
4
|
+
*
|
|
5
|
+
* This implementation uses AES-128 as the underlying cipher.
|
|
6
|
+
* FF1 is a Feistel cipher that encrypts strings of numerals
|
|
7
|
+
* while preserving their format (length and radix).
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.FF1 = void 0;
|
|
11
|
+
const aes_js_1 = require("./aes.js");
|
|
12
|
+
/**
|
|
13
|
+
* Ceiling division: ceil(a / b)
|
|
14
|
+
*/
|
|
15
|
+
function ceilDiv(a, b) {
|
|
16
|
+
return Math.ceil(a / b);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Convert a number array (base radix) to a bigint.
|
|
20
|
+
* Most significant digit first.
|
|
21
|
+
*/
|
|
22
|
+
function numArrayToBigInt(arr, radix) {
|
|
23
|
+
let result = 0n;
|
|
24
|
+
const radixBig = BigInt(radix);
|
|
25
|
+
for (const digit of arr) {
|
|
26
|
+
result = result * radixBig + BigInt(digit);
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Convert a bigint to a number array (base radix) with specified length.
|
|
32
|
+
* Most significant digit first. Pads with zeros if needed.
|
|
33
|
+
*/
|
|
34
|
+
function bigIntToNumArray(value, radix, length) {
|
|
35
|
+
const result = new Array(length).fill(0);
|
|
36
|
+
const radixBig = BigInt(radix);
|
|
37
|
+
let v = value;
|
|
38
|
+
for (let i = length - 1; i >= 0 && v > 0n; i--) {
|
|
39
|
+
result[i] = Number(v % radixBig);
|
|
40
|
+
v = v / radixBig;
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Convert bigint to big-endian byte array with specified length.
|
|
46
|
+
*/
|
|
47
|
+
function bigIntToBytes(value, length) {
|
|
48
|
+
const bytes = new Uint8Array(length);
|
|
49
|
+
let v = value;
|
|
50
|
+
for (let i = length - 1; i >= 0 && v > 0n; i--) {
|
|
51
|
+
bytes[i] = Number(v & 0xffn);
|
|
52
|
+
v >>= 8n;
|
|
53
|
+
}
|
|
54
|
+
return bytes;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Convert byte array to bigint (big-endian).
|
|
58
|
+
*/
|
|
59
|
+
function bytesToBigInt(bytes) {
|
|
60
|
+
let result = 0n;
|
|
61
|
+
for (const byte of bytes) {
|
|
62
|
+
result = (result << 8n) | BigInt(byte);
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Concatenate multiple Uint8Arrays.
|
|
68
|
+
*/
|
|
69
|
+
function concat(...arrays) {
|
|
70
|
+
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
|
|
71
|
+
const result = new Uint8Array(totalLength);
|
|
72
|
+
let offset = 0;
|
|
73
|
+
for (const arr of arrays) {
|
|
74
|
+
result.set(arr, offset);
|
|
75
|
+
offset += arr.length;
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* FF1 Format-Preserving Encryption cipher.
|
|
81
|
+
*
|
|
82
|
+
* Encrypts/decrypts a string of numerals (digits in base `radix`)
|
|
83
|
+
* while preserving the format.
|
|
84
|
+
*/
|
|
85
|
+
class FF1 {
|
|
86
|
+
/**
|
|
87
|
+
* Create an FF1 cipher with the given key and radix.
|
|
88
|
+
*
|
|
89
|
+
* @param key 16-byte AES key
|
|
90
|
+
* @param radix Base of the numeral system (2-65536)
|
|
91
|
+
*/
|
|
92
|
+
constructor(key, radix) {
|
|
93
|
+
Object.defineProperty(this, "aes", {
|
|
94
|
+
enumerable: true,
|
|
95
|
+
configurable: true,
|
|
96
|
+
writable: true,
|
|
97
|
+
value: void 0
|
|
98
|
+
});
|
|
99
|
+
Object.defineProperty(this, "radix", {
|
|
100
|
+
enumerable: true,
|
|
101
|
+
configurable: true,
|
|
102
|
+
writable: true,
|
|
103
|
+
value: void 0
|
|
104
|
+
});
|
|
105
|
+
if (radix < 2 || radix > 65536) {
|
|
106
|
+
throw new Error(`Radix must be in range [2, 65536], got ${radix}`);
|
|
107
|
+
}
|
|
108
|
+
this.aes = new aes_js_1.Aes128(key);
|
|
109
|
+
this.radix = radix;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* FF1 encryption.
|
|
113
|
+
*
|
|
114
|
+
* @param tweak Additional data (can be empty)
|
|
115
|
+
* @param plaintext Array of numerals (each in range [0, radix))
|
|
116
|
+
* @returns Encrypted numeral array of same length
|
|
117
|
+
*/
|
|
118
|
+
encrypt(tweak, plaintext) {
|
|
119
|
+
return this.cipher(tweak, plaintext, true);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* FF1 decryption.
|
|
123
|
+
*
|
|
124
|
+
* @param tweak Additional data (must match encryption)
|
|
125
|
+
* @param ciphertext Array of numerals
|
|
126
|
+
* @returns Decrypted numeral array
|
|
127
|
+
*/
|
|
128
|
+
decrypt(tweak, ciphertext) {
|
|
129
|
+
return this.cipher(tweak, ciphertext, false);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Core FF1 Feistel cipher (10 rounds).
|
|
133
|
+
*/
|
|
134
|
+
async cipher(tweak, input, encrypting) {
|
|
135
|
+
const n = input.length;
|
|
136
|
+
const t = tweak.length;
|
|
137
|
+
const radix = this.radix;
|
|
138
|
+
// Split into two halves
|
|
139
|
+
const u = Math.floor(n / 2);
|
|
140
|
+
const v = n - u;
|
|
141
|
+
// A gets first u elements, B gets last v elements
|
|
142
|
+
let A = input.slice(0, u);
|
|
143
|
+
let B = input.slice(u);
|
|
144
|
+
// Precompute constants per spec
|
|
145
|
+
// b = ceil(ceil(v * log2(radix)) / 8) - number of bytes to represent NUM_radix(B)
|
|
146
|
+
const b = Math.ceil(Math.ceil(v * Math.log2(radix)) / 8);
|
|
147
|
+
// d = 4 * ceil(b/4) + 4 - length of S (multiple of 4, at least 4 more than b)
|
|
148
|
+
const d = 4 * ceilDiv(b, 4) + 4;
|
|
149
|
+
// P = [1, 2, 1, radix (3 bytes), 10, u mod 256, n (4 bytes), t (4 bytes)]
|
|
150
|
+
const P = new Uint8Array(16);
|
|
151
|
+
P[0] = 1;
|
|
152
|
+
P[1] = 2;
|
|
153
|
+
P[2] = 1;
|
|
154
|
+
// radix as 3 bytes (big-endian, upper 8 bits then lower 16 bits)
|
|
155
|
+
P[3] = (radix >> 16) & 0xff;
|
|
156
|
+
P[4] = (radix >> 8) & 0xff;
|
|
157
|
+
P[5] = radix & 0xff;
|
|
158
|
+
P[6] = 10; // Number of rounds
|
|
159
|
+
P[7] = u & 0xff;
|
|
160
|
+
// n as 4 bytes big-endian
|
|
161
|
+
P[8] = (n >> 24) & 0xff;
|
|
162
|
+
P[9] = (n >> 16) & 0xff;
|
|
163
|
+
P[10] = (n >> 8) & 0xff;
|
|
164
|
+
P[11] = n & 0xff;
|
|
165
|
+
// t as 4 bytes big-endian
|
|
166
|
+
P[12] = (t >> 24) & 0xff;
|
|
167
|
+
P[13] = (t >> 16) & 0xff;
|
|
168
|
+
P[14] = (t >> 8) & 0xff;
|
|
169
|
+
P[15] = t & 0xff;
|
|
170
|
+
// 10 Feistel rounds
|
|
171
|
+
const rounds = encrypting ? [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] : [9, 8, 7, 6, 5, 4, 3, 2, 1, 0];
|
|
172
|
+
for (const i of rounds) {
|
|
173
|
+
// Determine m based on round parity
|
|
174
|
+
const m = (i % 2 === 0) ? u : v;
|
|
175
|
+
// Build Q: tweak || zeros || round || numB
|
|
176
|
+
// Q length must be multiple of 16
|
|
177
|
+
const numB = numArrayToBigInt(encrypting ? B : A, radix);
|
|
178
|
+
const numBBytes = bigIntToBytes(numB, b);
|
|
179
|
+
// Q = tweak || 0^(-t-b-1 mod 16) || i || [NUM_radix(B)]
|
|
180
|
+
const padLen = (16 - ((t + b + 1) % 16)) % 16;
|
|
181
|
+
const Q = concat(tweak, new Uint8Array(padLen), new Uint8Array([i]), numBBytes);
|
|
182
|
+
// R = PRF(P || Q)
|
|
183
|
+
// PRF uses AES-CBC-MAC over (P || Q) with blocks of 16 bytes
|
|
184
|
+
const pq = concat(P, Q);
|
|
185
|
+
const numBlocks = pq.length / 16;
|
|
186
|
+
const blocks = [];
|
|
187
|
+
for (let j = 0; j < numBlocks; j++) {
|
|
188
|
+
blocks.push(pq.slice(j * 16, (j + 1) * 16));
|
|
189
|
+
}
|
|
190
|
+
const R = await this.aes.cbcMac(blocks);
|
|
191
|
+
// S = first d bytes of R || CIPH(R ⊕ [1]) || CIPH(R ⊕ [2]) || ...
|
|
192
|
+
const S = new Uint8Array(d);
|
|
193
|
+
const rCopyLen = Math.min(16, d);
|
|
194
|
+
S.set(R.slice(0, rCopyLen), 0);
|
|
195
|
+
let sOffset = rCopyLen;
|
|
196
|
+
let counter = 1;
|
|
197
|
+
while (sOffset < d) {
|
|
198
|
+
// R XOR counter (counter as 16-byte big-endian)
|
|
199
|
+
const counterBlock = new Uint8Array(16);
|
|
200
|
+
counterBlock[15] = counter & 0xff;
|
|
201
|
+
counterBlock[14] = (counter >> 8) & 0xff;
|
|
202
|
+
counterBlock[13] = (counter >> 16) & 0xff;
|
|
203
|
+
counterBlock[12] = (counter >> 24) & 0xff;
|
|
204
|
+
const xored = new Uint8Array(16);
|
|
205
|
+
for (let k = 0; k < 16; k++) {
|
|
206
|
+
xored[k] = R[k] ^ counterBlock[k];
|
|
207
|
+
}
|
|
208
|
+
const block = await this.aes.encryptBlock(xored);
|
|
209
|
+
const copyLen = Math.min(16, d - sOffset);
|
|
210
|
+
S.set(block.slice(0, copyLen), sOffset);
|
|
211
|
+
sOffset += copyLen;
|
|
212
|
+
counter++;
|
|
213
|
+
}
|
|
214
|
+
// y = NUM(S) - interpret S as big-endian number
|
|
215
|
+
const y = bytesToBigInt(S);
|
|
216
|
+
// c = (NUM_radix(A/B) + y) mod radix^m (encrypt)
|
|
217
|
+
// c = (NUM_radix(A/B) - y) mod radix^m (decrypt)
|
|
218
|
+
const radixPowM = BigInt(radix) ** BigInt(m);
|
|
219
|
+
const numSrc = numArrayToBigInt(encrypting ? A : B, radix);
|
|
220
|
+
let c;
|
|
221
|
+
if (encrypting) {
|
|
222
|
+
c = (numSrc + y) % radixPowM;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
// For modular subtraction, add radixPowM to handle negative
|
|
226
|
+
c = ((numSrc - y) % radixPowM + radixPowM) % radixPowM;
|
|
227
|
+
}
|
|
228
|
+
// C = STR_m_radix(c)
|
|
229
|
+
const C = bigIntToNumArray(c, radix, m);
|
|
230
|
+
// Swap: A = B, B = C (encrypt) or B = A, A = C (decrypt)
|
|
231
|
+
if (encrypting) {
|
|
232
|
+
A = B;
|
|
233
|
+
B = C;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
B = A;
|
|
237
|
+
A = C;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Return A || B
|
|
241
|
+
return [...A, ...B];
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
exports.FF1 = FF1;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @tnid/encryption - Format-preserving encryption for TNIDs
|
|
3
|
+
*
|
|
4
|
+
* Provides FF1 (NIST SP 800-38G) encryption to convert time-ordered V0 TNIDs
|
|
5
|
+
* to random-looking V1 TNIDs, hiding timestamp information while remaining
|
|
6
|
+
* reversible with the secret key.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { EncryptionKey, encryptV0ToV1, decryptV1ToV0 } from "@tnid/encryption";
|
|
11
|
+
*
|
|
12
|
+
* // Create a key from hex string
|
|
13
|
+
* const key = EncryptionKey.fromHex("0102030405060708090a0b0c0d0e0f10");
|
|
14
|
+
*
|
|
15
|
+
* // Encrypt a V0 TNID to V1
|
|
16
|
+
* const encrypted = await encryptV0ToV1("user.Br2flcNDfF6LYICnT", key);
|
|
17
|
+
* // Returns a V1 TNID that looks random
|
|
18
|
+
*
|
|
19
|
+
* // Decrypt back to V0
|
|
20
|
+
* const decrypted = await decryptV1ToV0(encrypted, key);
|
|
21
|
+
* // decrypted === "user.Br2flcNDfF6LYICnT"
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @module
|
|
25
|
+
*/
|
|
26
|
+
export { EncryptionKey, EncryptionKeyError, EncryptionError, encryptV0ToV1, decryptV1ToV0, } from "./encryption.js";
|
|
27
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,eAAe,EACf,aAAa,EACb,aAAa,GACd,MAAM,iBAAiB,CAAC"}
|
package/script/index.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @tnid/encryption - Format-preserving encryption for TNIDs
|
|
4
|
+
*
|
|
5
|
+
* Provides FF1 (NIST SP 800-38G) encryption to convert time-ordered V0 TNIDs
|
|
6
|
+
* to random-looking V1 TNIDs, hiding timestamp information while remaining
|
|
7
|
+
* reversible with the secret key.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { EncryptionKey, encryptV0ToV1, decryptV1ToV0 } from "@tnid/encryption";
|
|
12
|
+
*
|
|
13
|
+
* // Create a key from hex string
|
|
14
|
+
* const key = EncryptionKey.fromHex("0102030405060708090a0b0c0d0e0f10");
|
|
15
|
+
*
|
|
16
|
+
* // Encrypt a V0 TNID to V1
|
|
17
|
+
* const encrypted = await encryptV0ToV1("user.Br2flcNDfF6LYICnT", key);
|
|
18
|
+
* // Returns a V1 TNID that looks random
|
|
19
|
+
*
|
|
20
|
+
* // Decrypt back to V0
|
|
21
|
+
* const decrypted = await decryptV1ToV0(encrypted, key);
|
|
22
|
+
* // decrypted === "user.Br2flcNDfF6LYICnT"
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @module
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.decryptV1ToV0 = exports.encryptV0ToV1 = exports.EncryptionError = exports.EncryptionKeyError = exports.EncryptionKey = void 0;
|
|
29
|
+
var encryption_js_1 = require("./encryption.js");
|
|
30
|
+
Object.defineProperty(exports, "EncryptionKey", { enumerable: true, get: function () { return encryption_js_1.EncryptionKey; } });
|
|
31
|
+
Object.defineProperty(exports, "EncryptionKeyError", { enumerable: true, get: function () { return encryption_js_1.EncryptionKeyError; } });
|
|
32
|
+
Object.defineProperty(exports, "EncryptionError", { enumerable: true, get: function () { return encryption_js_1.EncryptionError; } });
|
|
33
|
+
Object.defineProperty(exports, "encryptV0ToV1", { enumerable: true, get: function () { return encryption_js_1.encryptV0ToV1; } });
|
|
34
|
+
Object.defineProperty(exports, "decryptV1ToV0", { enumerable: true, get: function () { return encryption_js_1.decryptV1ToV0; } });
|