@1auth/crypto 0.0.0-rc.5 → 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.
Files changed (2) hide show
  1. package/index.js +117 -44
  2. 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(12) Checksum peppering disabled.",
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(12);
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 randomIV(); // 96
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
- // Deterministic encryption using a fixed IV (checksumPepper) to enable
192
- // privacy-compliant digest lookups. Rotating the pepper invalidates all
193
- // existing digests, supporting GDPR right-to-erasure workflows.
194
- // The ciphertexts are never stored directly - only their hashes are persisted.
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
- { checksumPepper, encryptionKey } = {},
211
+ { hashAlgorithm, checksumPepper } = {},
198
212
  ) => {
213
+ hashAlgorithm ??= options.digestChecksumHashAlgorithm;
199
214
  checksumPepper ??= options.digestChecksumPepper;
200
- encryptionKey ??= options.symmetricEncryptionKey;
201
- if (!checksumPepper || !encryptionKey) {
215
+ if (!checksumPepper) {
202
216
  return value;
203
217
  }
204
- const newValue = symmetricEncrypt(value, {
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, encryptionKey } = {},
260
+ { hashAlgorithm, encoding, checksumPepper } = {},
251
261
  ) => {
252
262
  hashAlgorithm ??= options.digestChecksumHashAlgorithm;
253
263
  const checksum = createChecksum(
254
- createPepperedValue(value, { checksumPepper, encryptionKey }),
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, encryptionKey } = {},
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(12); // 96 bits
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
- export const symmetricGenerateEncryptionKey = (
425
- sub,
426
- { encryptionKey, signatureSecret } = {},
427
- ) => {
428
- encryptionKey ??= options.symmetricEncryptionKey;
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
- const rowEncryptionKey = symmetricRandomEncryptionKey();
432
- const rowEncryptedKey = symmetricEncrypt(rowEncryptionKey, {
433
- encryptionKey,
434
- signatureSecret,
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
- return { encryptionKey: rowEncryptionKey, encryptedKey: rowEncryptedKey };
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
- encryptionKey ??= options.symmetricEncryptionKey;
532
- signatureSecret ??= options.symmetricSignatureSecret;
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 = (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1auth/crypto",
3
- "version": "0.0.0-rc.5",
3
+ "version": "0.0.0-rc.7",
4
4
  "description": "Cryptographic utilities for encryption, hashing, and signing with modern algorithms",
5
5
  "type": "module",
6
6
  "engines": {