@1auth/crypto 0.0.0-rc.6 → 0.0.0-rc.7
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/index.js +117 -44
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -24,6 +24,7 @@ const defaults = {
|
|
|
24
24
|
symmetricEncryptionKey: undefined, // symmetricRandomEncryptionKey()
|
|
25
25
|
symmetricEncryptionAlgorithm: "chacha20-poly1305", // 2025-03: AES-256 GCM (aes-256-gcm) or ChaCha20-Poly1305 (chacha20-poly1305)
|
|
26
26
|
symmetricEncryptionEncoding: undefined, // https://nodejs.org/api/buffer.html#buffers-and-character-encodings
|
|
27
|
+
encryptionKeyProvider: "1", // wraps new row keys, see Encryption key providers
|
|
27
28
|
symmetricSignatureHashAlgorithm: undefined, // fallback to defaultHashAlgorithm
|
|
28
29
|
symmetricSignatureSecret: undefined, // symmetricRandomSignatureSecret()
|
|
29
30
|
symmetricSignatureEncoding: undefined, // fallback to defaultEncoding
|
|
@@ -84,12 +85,22 @@ export default (opt = {}) => {
|
|
|
84
85
|
options.digestChecksumSalt = makeOptionsBuffer(options.digestChecksumSalt);
|
|
85
86
|
if (!options.digestChecksumPepper) {
|
|
86
87
|
throw new Error(
|
|
87
|
-
"@1auth/crypto digestChecksumPepper is empty, use a stored secret made from randomBytes(
|
|
88
|
+
"@1auth/crypto digestChecksumPepper is empty, use a stored secret made from randomBytes(32) Checksum peppering disabled.",
|
|
88
89
|
);
|
|
89
90
|
}
|
|
90
91
|
options.digestChecksumPepper = makeOptionsBuffer(
|
|
91
92
|
options.digestChecksumPepper,
|
|
92
93
|
);
|
|
94
|
+
// The pepper is the digest HMAC key; anything shorter than the 256 bit
|
|
95
|
+
// secret every other option uses weakens the blind index. A 12 byte pepper
|
|
96
|
+
// is the old encrypt-era size — it must rotate anyway, because the digest
|
|
97
|
+
// construction changed under it.
|
|
98
|
+
if (options.digestChecksumPepper.length < 32) {
|
|
99
|
+
throw new RangeError(
|
|
100
|
+
`@1auth/crypto digestChecksumPepper must be at least 32 bytes, received ${options.digestChecksumPepper.length}. Use randomChecksumPepper().`,
|
|
101
|
+
{ cause: { length: options.digestChecksumPepper.length } },
|
|
102
|
+
);
|
|
103
|
+
}
|
|
93
104
|
options.digestChecksumHashAlgorithm ??= options.defaultHashAlgorithm;
|
|
94
105
|
options.digestChecksumEncoding ??= options.defaultEncoding;
|
|
95
106
|
|
|
@@ -97,7 +108,7 @@ export default (opt = {}) => {
|
|
|
97
108
|
const encodedLength = (byteLength) =>
|
|
98
109
|
Buffer.alloc(byteLength).toString(options.symmetricEncryptionEncoding)
|
|
99
110
|
.length;
|
|
100
|
-
symmetricEncryptionEncodingLengths.iv = encodedLength(
|
|
111
|
+
symmetricEncryptionEncodingLengths.iv = encodedLength(ivLength);
|
|
101
112
|
symmetricEncryptionEncodingLengths.ivAndAuthTag =
|
|
102
113
|
symmetricEncryptionEncodingLengths.iv + encodedLength(authTagLength);
|
|
103
114
|
};
|
|
@@ -177,7 +188,7 @@ export const randomChecksumSalt = () => {
|
|
|
177
188
|
return randomBytes(32); // 256 bits
|
|
178
189
|
};
|
|
179
190
|
export const randomChecksumPepper = () => {
|
|
180
|
-
return
|
|
191
|
+
return randomBytes(32); // 256 bits
|
|
181
192
|
};
|
|
182
193
|
|
|
183
194
|
export const createSaltedValue = (value, { checksumSalt } = {}) => {
|
|
@@ -188,25 +199,23 @@ export const createSaltedValue = (value, { checksumSalt } = {}) => {
|
|
|
188
199
|
const newValue = value + checksumSalt;
|
|
189
200
|
return newValue;
|
|
190
201
|
};
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
// existing digests, supporting GDPR right-to-erasure
|
|
194
|
-
// The
|
|
202
|
+
// HMAC keyed by the pepper (a blind index): deterministic so equal values
|
|
203
|
+
// yield equal digests for lookups, one-way without the pepper. Rotating the
|
|
204
|
+
// pepper invalidates all existing digests, supporting GDPR right-to-erasure
|
|
205
|
+
// workflows. The pepper has to be passable, or a caller that supplies it
|
|
206
|
+
// still silently gets whatever the module globals happen to hold. That is
|
|
207
|
+
// what a key rotation needs: the new digest, computed before the new material
|
|
208
|
+
// is live.
|
|
195
209
|
export const createPepperedValue = (
|
|
196
210
|
value,
|
|
197
|
-
{
|
|
211
|
+
{ hashAlgorithm, checksumPepper } = {},
|
|
198
212
|
) => {
|
|
213
|
+
hashAlgorithm ??= options.digestChecksumHashAlgorithm;
|
|
199
214
|
checksumPepper ??= options.digestChecksumPepper;
|
|
200
|
-
|
|
201
|
-
if (!checksumPepper || !encryptionKey) {
|
|
215
|
+
if (!checksumPepper) {
|
|
202
216
|
return value;
|
|
203
217
|
}
|
|
204
|
-
|
|
205
|
-
encryptionKey,
|
|
206
|
-
sub: "",
|
|
207
|
-
iv: checksumPepper,
|
|
208
|
-
});
|
|
209
|
-
return newValue;
|
|
218
|
+
return createHmac(hashAlgorithm, checksumPepper).update(value).digest();
|
|
210
219
|
};
|
|
211
220
|
|
|
212
221
|
export const createChecksum = (value, { hashAlgorithm, encoding } = {}) => {
|
|
@@ -220,6 +229,7 @@ export const createSeasonedChecksum = (
|
|
|
220
229
|
) => {
|
|
221
230
|
return createChecksum(
|
|
222
231
|
createPepperedValue(createSaltedValue(value, { checksumSalt }), {
|
|
232
|
+
hashAlgorithm,
|
|
223
233
|
checksumPepper,
|
|
224
234
|
}),
|
|
225
235
|
{
|
|
@@ -247,11 +257,14 @@ export const createSaltedDigest = (
|
|
|
247
257
|
};
|
|
248
258
|
export const createPepperedDigest = (
|
|
249
259
|
value,
|
|
250
|
-
{ hashAlgorithm, encoding, checksumPepper
|
|
260
|
+
{ hashAlgorithm, encoding, checksumPepper } = {},
|
|
251
261
|
) => {
|
|
252
262
|
hashAlgorithm ??= options.digestChecksumHashAlgorithm;
|
|
253
263
|
const checksum = createChecksum(
|
|
254
|
-
createPepperedValue(value, {
|
|
264
|
+
createPepperedValue(value, {
|
|
265
|
+
hashAlgorithm,
|
|
266
|
+
checksumPepper,
|
|
267
|
+
}),
|
|
255
268
|
{
|
|
256
269
|
hashAlgorithm,
|
|
257
270
|
encoding,
|
|
@@ -261,7 +274,7 @@ export const createPepperedDigest = (
|
|
|
261
274
|
};
|
|
262
275
|
export const createSeasonedDigest = (
|
|
263
276
|
value,
|
|
264
|
-
{ hashAlgorithm, encoding, checksumSalt, checksumPepper
|
|
277
|
+
{ hashAlgorithm, encoding, checksumSalt, checksumPepper } = {},
|
|
265
278
|
) => {
|
|
266
279
|
hashAlgorithm ??= options.digestChecksumHashAlgorithm;
|
|
267
280
|
const checksum = createSeasonedChecksum(value, {
|
|
@@ -269,7 +282,6 @@ export const createSeasonedDigest = (
|
|
|
269
282
|
encoding,
|
|
270
283
|
checksumSalt,
|
|
271
284
|
checksumPepper,
|
|
272
|
-
encryptionKey,
|
|
273
285
|
});
|
|
274
286
|
return `${hashAlgorithm}:${checksum}`;
|
|
275
287
|
};
|
|
@@ -405,6 +417,8 @@ export const verifySecretHash = verifyArgon2;
|
|
|
405
417
|
|
|
406
418
|
// *** Symmetric Encryption *** //
|
|
407
419
|
const authTagLength = 16;
|
|
420
|
+
// 96 bits, the nonce size both supported AEAD ciphers take.
|
|
421
|
+
const ivLength = 12;
|
|
408
422
|
// 16 is already node's default for both supported AEAD ciphers, so this is
|
|
409
423
|
// belt-and-braces against a future cipher whose default differs.
|
|
410
424
|
// Stryker disable next-line ObjectLiteral: byte-identical output either way
|
|
@@ -418,23 +432,91 @@ export const symmetricRandomEncryptionKey = () => {
|
|
|
418
432
|
};
|
|
419
433
|
|
|
420
434
|
export const randomIV = () => {
|
|
421
|
-
return randomBytes(
|
|
435
|
+
return randomBytes(ivLength); // 96 bits
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
// *** Encryption key providers *** //
|
|
439
|
+
// Wrapped keys store as `<provider>:<payload>` and read by prefix, not config,
|
|
440
|
+
// so providers change with no migration. Name = package suffix + version:
|
|
441
|
+
// `@1auth/crypto` -> `1`, `@1auth/crypto-kms` -> `kms1`. No prefix predates the
|
|
442
|
+
// scheme, unambiguous because base64, base64url and hex all exclude `:`.
|
|
443
|
+
export const encryptionKeyProviderSeparator = ":";
|
|
444
|
+
const encryptionKeyProviderDefault = "1";
|
|
445
|
+
|
|
446
|
+
// Null prototype: the name comes off a stored wrapped key, so on a plain object
|
|
447
|
+
// `toString:…` or `constructor:…` resolves to an inherited function, passes the
|
|
448
|
+
// truthiness guards below, and dies on `provider.decrypt is not a function`
|
|
449
|
+
// instead of falling back as documented. It also makes `__proto__` a storable
|
|
450
|
+
// provider name rather than a silently discarded assignment.
|
|
451
|
+
const encryptionKeyProviders = Object.create(null);
|
|
452
|
+
|
|
453
|
+
export const registerEncryptionKeyProvider = (name, provider) => {
|
|
454
|
+
encryptionKeyProviders[name] = provider;
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
export const getEncryptionKeyProviders = () => encryptionKeyProviders;
|
|
458
|
+
|
|
459
|
+
const parseEncryptedKey = (encryptedKey) => {
|
|
460
|
+
const separatorIndex = encryptedKey.indexOf(encryptionKeyProviderSeparator);
|
|
461
|
+
const name = encryptedKey.substring(0, separatorIndex);
|
|
462
|
+
// unknown prefix falls to `1` and fails its signature check, never mis-decrypts
|
|
463
|
+
if (separatorIndex < 0 || !encryptionKeyProviders[name]) {
|
|
464
|
+
return {
|
|
465
|
+
provider: encryptionKeyProviders[encryptionKeyProviderDefault],
|
|
466
|
+
payload: encryptedKey,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
provider: encryptionKeyProviders[name],
|
|
471
|
+
payload: encryptedKey.substring(separatorIndex + 1),
|
|
472
|
+
};
|
|
422
473
|
};
|
|
423
474
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
{ encryptionKey, signatureSecret } = {}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
signatureSecret ??= options.symmetricSignatureSecret;
|
|
475
|
+
// The in-process provider, and the format every pre-prefix row was written in.
|
|
476
|
+
registerEncryptionKeyProvider(encryptionKeyProviderDefault, {
|
|
477
|
+
generate: (sub, { encryptionKey, signatureSecret } = {}) => {
|
|
478
|
+
encryptionKey ??= options.symmetricEncryptionKey;
|
|
479
|
+
signatureSecret ??= options.symmetricSignatureSecret;
|
|
430
480
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
481
|
+
const rowEncryptionKey = symmetricRandomEncryptionKey();
|
|
482
|
+
const rowEncryptedKey = symmetricEncrypt(rowEncryptionKey, {
|
|
483
|
+
encryptionKey,
|
|
484
|
+
signatureSecret,
|
|
485
|
+
sub,
|
|
486
|
+
});
|
|
487
|
+
return { encryptionKey: rowEncryptionKey, encryptedKey: rowEncryptedKey };
|
|
488
|
+
},
|
|
489
|
+
decrypt: (payload, sub, { encryptionKey, signatureSecret } = {}) => {
|
|
490
|
+
encryptionKey ??= options.symmetricEncryptionKey;
|
|
491
|
+
signatureSecret ??= options.symmetricSignatureSecret;
|
|
492
|
+
|
|
493
|
+
return Buffer.from(
|
|
494
|
+
symmetricDecrypt(payload, {
|
|
495
|
+
encryptionKey,
|
|
496
|
+
signatureSecret,
|
|
497
|
+
sub,
|
|
498
|
+
encoding: options.symmetricEncryptionEncoding,
|
|
499
|
+
}),
|
|
500
|
+
options.symmetricEncryptionEncoding,
|
|
501
|
+
);
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
export const symmetricGenerateEncryptionKey = (sub, providerOptions = {}) => {
|
|
506
|
+
const name =
|
|
507
|
+
providerOptions.encryptionKeyProvider ?? options.encryptionKeyProvider;
|
|
508
|
+
const provider = encryptionKeyProviders[name];
|
|
509
|
+
if (!provider) {
|
|
510
|
+
throw new Error("Unknown encryptionKeyProvider", { cause: { name } });
|
|
511
|
+
}
|
|
512
|
+
const { encryptionKey, encryptedKey } = provider.generate(
|
|
435
513
|
sub,
|
|
436
|
-
|
|
437
|
-
|
|
514
|
+
providerOptions,
|
|
515
|
+
);
|
|
516
|
+
return {
|
|
517
|
+
encryptionKey,
|
|
518
|
+
encryptedKey: `${name}${encryptionKeyProviderSeparator}${encryptedKey}`,
|
|
519
|
+
};
|
|
438
520
|
};
|
|
439
521
|
|
|
440
522
|
// sub add context to encryption
|
|
@@ -528,17 +610,8 @@ export const symmetricDecryptKey = (
|
|
|
528
610
|
encryptedKey,
|
|
529
611
|
{ sub, encryptionKey, signatureSecret } = {},
|
|
530
612
|
) => {
|
|
531
|
-
|
|
532
|
-
signatureSecret
|
|
533
|
-
return Buffer.from(
|
|
534
|
-
symmetricDecrypt(encryptedKey, {
|
|
535
|
-
encryptionKey,
|
|
536
|
-
signatureSecret,
|
|
537
|
-
sub,
|
|
538
|
-
encoding: options.symmetricEncryptionEncoding,
|
|
539
|
-
}),
|
|
540
|
-
options.symmetricEncryptionEncoding,
|
|
541
|
-
);
|
|
613
|
+
const { provider, payload } = parseEncryptedKey(encryptedKey);
|
|
614
|
+
return provider.decrypt(payload, sub, { encryptionKey, signatureSecret });
|
|
542
615
|
};
|
|
543
616
|
|
|
544
617
|
export const symmetricDecrypt = (
|