@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/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"}
@@ -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; } });
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }