@blamejs/pki 0.4.13 → 0.4.15

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.
@@ -164,59 +164,69 @@ async function _buildKari(cek, cert, opts) {
164
164
  var keyAlg = cert.subjectPublicKeyInfo.algorithm;
165
165
  var wrapName = _wrapOidForKek(cek.length);
166
166
  var ukm = opts.ukm ? guard.bytes.view(opts.ukm, CmsError, "cms/bad-input", "ukm") : null;
167
- var ecdhPub, origKeyAlgId, kek;
168
- if (keyAlg.oid === O("ecPublicKey")) {
169
- var curveOid = asn1.read.oid(asn1.decode(keyAlg.parameters));
170
- var ka = EC_KA[curveOid];
171
- if (!ka) throw _err("cms/unsupported-algorithm", "unsupported recipient EC curve for kari");
172
- var recipPub = await subtle.importKey("spki", cert.subjectPublicKeyInfo.bytes, { name: "ECDH", namedCurve: ka.curve }, false, []);
173
- var eph = await subtle.generateKey({ name: "ECDH", namedCurve: ka.curve }, true, ["deriveBits"]);
174
- origKeyAlgId = { spki: Buffer.from(await subtle.exportKey("spki", eph.publicKey)), scheme: ka.scheme };
175
- var z = Buffer.from(await subtle.deriveBits({ name: "ECDH", public: recipPub }, eph.privateKey, null));
176
- var zKey = await subtle.importKey("raw", z, { name: "X963KDF" }, false, ["deriveBits"]);
177
- var sharedInfo = _eccSharedInfo(wrapName, ukm, cek.length);
178
- kek = Buffer.from(await subtle.deriveBits({ name: "X963KDF", hash: ka.hash, info: sharedInfo }, zKey, cek.length * 8));
179
- void ecdhPub;
180
- } else if (MONT_KA[keyAlg.oid]) {
181
- var mka = MONT_KA[keyAlg.oid];
182
- var rPub = await subtle.importKey("spki", cert.subjectPublicKeyInfo.bytes, { name: mka.name }, false, []);
183
- var meph = await subtle.generateKey({ name: mka.name }, true, ["deriveBits"]);
184
- origKeyAlgId = { spki: Buffer.from(await subtle.exportKey("spki", meph.publicKey)), scheme: mka.scheme };
185
- var mz = Buffer.from(await subtle.deriveBits({ name: mka.name, public: rPub }, meph.privateKey, null));
186
- if (mz.every(function (x) { return x === 0; })) throw _err("cms/bad-input", "the X25519/X448 shared secret is all-zero (low-order point)");
187
- var mzKey = await subtle.importKey("raw", mz, { name: "HKDF" }, false, ["deriveBits"]);
188
- // RFC 8418 sec. 2.2: when a ukm is present it is used BOTH as the HKDF salt AND as the
189
- // ECC-CMS-SharedInfo entityUInfo (the HKDF info) -- omitting it from the info diverges the KEK
190
- // from any conformant peer that reads the transmitted ukm.
191
- kek = Buffer.from(await subtle.deriveBits({ name: "HKDF", hash: mka.hkdf, salt: ukm || Buffer.alloc(0), info: _eccSharedInfo(wrapName, ukm, cek.length) }, mzKey, cek.length * 8));
192
- } else {
193
- // Coverage residual: unreachable -- _buildRecipient routes only ecPublicKey / X25519 / X448
194
- // keys into _buildKari; a defensive throw against a future dispatch change.
195
- throw _err("cms/unsupported-algorithm", "unsupported recipient key algorithm for kari");
196
- }
197
- var encryptedKey = await _aesKwWrap(kek, cek);
198
- // originatorKey [1] IMPLICIT OriginatorPublicKey { algorithm, publicKey BIT STRING }.
199
- var origSpki = asn1.decode(origKeyAlgId.spki);
200
- var origPubBits = origSpki.children[1]; // BIT STRING node
201
- var originatorKey = b.contextConstructed(1, Buffer.concat([origSpki.children[0].bytes, origPubBits.bytes]));
202
- // KeyAgreeRecipientIdentifier CHOICE { issuerAndSerialNumber, rKeyId [0] IMPLICIT
203
- // RecipientKeyIdentifier } -- the SKI form here wraps a SEQUENCE (rKeyId), unlike ktri's bare
204
- // subjectKeyIdentifier [0] IMPLICIT OCTET STRING.
205
- var ridNode;
206
- _assertKeyIdentifier(opts.keyIdentifier);
207
- if (opts.keyIdentifier === "subjectKeyIdentifier") {
208
- var ski = _skiOf(cert);
209
- if (!ski) throw _err("cms/bad-input", "keyIdentifier: \"subjectKeyIdentifier\" requires the recipient certificate to carry a subjectKeyIdentifier extension");
210
- ridNode = b.contextConstructed(0, b.octetString(ski));
211
- } else {
212
- ridNode = b.sequence([b.raw(cert.issuer.bytes), b.integer(cert.serialNumber)]);
167
+ // z / mz (the raw agreement secret) and the derived kek are all allocated here and all secret.
168
+ // Declared together so one `finally` clears whichever the chosen branch produced, including when
169
+ // a later encoding step throws (RFC 9629 sec. 7 asks the same of the KEM builder below).
170
+ var ecdhPub, origKeyAlgId, kek, z, mz;
171
+ try {
172
+ if (keyAlg.oid === O("ecPublicKey")) {
173
+ var curveOid = asn1.read.oid(asn1.decode(keyAlg.parameters));
174
+ var ka = EC_KA[curveOid];
175
+ if (!ka) throw _err("cms/unsupported-algorithm", "unsupported recipient EC curve for kari");
176
+ var recipPub = await subtle.importKey("spki", cert.subjectPublicKeyInfo.bytes, { name: "ECDH", namedCurve: ka.curve }, false, []);
177
+ var eph = await subtle.generateKey({ name: "ECDH", namedCurve: ka.curve }, true, ["deriveBits"]);
178
+ origKeyAlgId = { spki: Buffer.from(await subtle.exportKey("spki", eph.publicKey)), scheme: ka.scheme };
179
+ z = Buffer.from(await subtle.deriveBits({ name: "ECDH", public: recipPub }, eph.privateKey, null));
180
+ var zKey = await subtle.importKey("raw", z, { name: "X963KDF" }, false, ["deriveBits"]);
181
+ var sharedInfo = _eccSharedInfo(wrapName, ukm, cek.length);
182
+ kek = Buffer.from(await subtle.deriveBits({ name: "X963KDF", hash: ka.hash, info: sharedInfo }, zKey, cek.length * 8));
183
+ void ecdhPub;
184
+ } else if (MONT_KA[keyAlg.oid]) {
185
+ var mka = MONT_KA[keyAlg.oid];
186
+ var rPub = await subtle.importKey("spki", cert.subjectPublicKeyInfo.bytes, { name: mka.name }, false, []);
187
+ var meph = await subtle.generateKey({ name: mka.name }, true, ["deriveBits"]);
188
+ origKeyAlgId = { spki: Buffer.from(await subtle.exportKey("spki", meph.publicKey)), scheme: mka.scheme };
189
+ mz = Buffer.from(await subtle.deriveBits({ name: mka.name, public: rPub }, meph.privateKey, null));
190
+ if (mz.every(function (x) { return x === 0; })) throw _err("cms/bad-input", "the X25519/X448 shared secret is all-zero (low-order point)");
191
+ var mzKey = await subtle.importKey("raw", mz, { name: "HKDF" }, false, ["deriveBits"]);
192
+ // RFC 8418 sec. 2.2: when a ukm is present it is used BOTH as the HKDF salt AND as the
193
+ // ECC-CMS-SharedInfo entityUInfo (the HKDF info) -- omitting it from the info diverges the KEK
194
+ // from any conformant peer that reads the transmitted ukm.
195
+ kek = Buffer.from(await subtle.deriveBits({ name: "HKDF", hash: mka.hkdf, salt: ukm || Buffer.alloc(0), info: _eccSharedInfo(wrapName, ukm, cek.length) }, mzKey, cek.length * 8));
196
+ } else {
197
+ // Coverage residual: unreachable -- _buildRecipient routes only ecPublicKey / X25519 / X448
198
+ // keys into _buildKari; a defensive throw against a future dispatch change.
199
+ throw _err("cms/unsupported-algorithm", "unsupported recipient key algorithm for kari");
200
+ }
201
+ var encryptedKey = await _aesKwWrap(kek, cek);
202
+ // originatorKey [1] IMPLICIT OriginatorPublicKey { algorithm, publicKey BIT STRING }.
203
+ var origSpki = asn1.decode(origKeyAlgId.spki);
204
+ var origPubBits = origSpki.children[1]; // BIT STRING node
205
+ var originatorKey = b.contextConstructed(1, Buffer.concat([origSpki.children[0].bytes, origPubBits.bytes]));
206
+ // KeyAgreeRecipientIdentifier CHOICE { issuerAndSerialNumber, rKeyId [0] IMPLICIT
207
+ // RecipientKeyIdentifier } -- the SKI form here wraps a SEQUENCE (rKeyId), unlike ktri's bare
208
+ // subjectKeyIdentifier [0] IMPLICIT OCTET STRING.
209
+ var ridNode;
210
+ _assertKeyIdentifier(opts.keyIdentifier);
211
+ if (opts.keyIdentifier === "subjectKeyIdentifier") {
212
+ var ski = _skiOf(cert);
213
+ if (!ski) throw _err("cms/bad-input", "keyIdentifier: \"subjectKeyIdentifier\" requires the recipient certificate to carry a subjectKeyIdentifier extension");
214
+ ridNode = b.contextConstructed(0, b.octetString(ski));
215
+ } else {
216
+ ridNode = b.sequence([b.raw(cert.issuer.bytes), b.integer(cert.serialNumber)]);
217
+ }
218
+ var rek = b.sequence([b.sequence([ridNode, b.octetString(encryptedKey)])]); // RecipientEncryptedKeys SEQ OF { rid, encKey }
219
+ var kekAlg = b.sequence([b.oid(O(origKeyAlgId.scheme)), _algId(wrapName, "absent")]);
220
+ var kariKids = [b.integer(3n), b.explicit(0, originatorKey)];
221
+ if (ukm) kariKids.push(b.explicit(1, b.octetString(ukm)));
222
+ kariKids.push(kekAlg, rek);
223
+ return { tag: 1, node: b.sequence(kariKids) };
224
+ } finally {
225
+ // Buffer.from(arrayBuffer) is a VIEW over the engine's returned buffer, so clearing z / mz
226
+ // clears that allocation too. The CEK belongs to the caller (the message, not this recipient)
227
+ // and is wiped once at the end of encrypt; the ephemeral PUBLIC key is not secret.
228
+ guard.secret.zeroizeAll([z, mz, kek], CmsError, "cms/bad-input", "the key-agreement shared secret");
213
229
  }
214
- var rek = b.sequence([b.sequence([ridNode, b.octetString(encryptedKey)])]); // RecipientEncryptedKeys SEQ OF { rid, encKey }
215
- var kekAlg = b.sequence([b.oid(O(origKeyAlgId.scheme)), _algId(wrapName, "absent")]);
216
- var kariKids = [b.integer(3n), b.explicit(0, originatorKey)];
217
- if (ukm) kariKids.push(b.explicit(1, b.octetString(ukm)));
218
- kariKids.push(kekAlg, rek);
219
- return { tag: 1, node: b.sequence(kariKids) };
220
230
  }
221
231
 
222
232
  // ---- kekri (symmetric KEK) : AES-KW --------------------------------------
@@ -232,22 +242,37 @@ async function _buildKekri(cek, desc) {
232
242
  // A PBKDF2 iterationCount MUST be a positive integer within the same cap the decryptor enforces -- a
233
243
  // ---- pwri (password) : PBKDF2 + RFC 3211 double-CBC PWRI-KEK ---------------
234
244
  async function _buildPwri(cek, desc) {
235
- var password = pbes2.passwordBytes(desc.password, _err, "cms");
245
+ // A string / Uint8Array password is encoded into a buffer THIS toolkit allocated -- a credential
246
+ // copy -- so it is cleared once the derivation has consumed it. A caller-supplied Buffer is
247
+ // borrowed and left intact.
248
+ // The option validation runs BEFORE the password is encoded: a rejected iteration count or salt
249
+ // would otherwise abandon an owned credential copy on the way out.
236
250
  var iterations = pbes2.assertIterations(desc.iterations == null ? 600000 : desc.iterations, _err, "cms");
237
251
  var salt = desc.salt ? pbes2.assertSalt(guard.bytes.view(desc.salt, CmsError, "cms/bad-input", "salt"), _err, "cms") : nodeCrypto.randomBytes(16);
238
252
  var prf = desc.prf || "hmacWithSHA256";
253
+ _prfHash(prf); // reject an unsupported prf HERE, before a credential copy exists to abandon
239
254
  var innerKeyBytes = 32; // AES-256-CBC inner
255
+ var pwOwn = pbes2.passwordBytesOwned(desc.password, _err, "cms");
256
+ var password = pwOwn.bytes;
240
257
  var kekKey = await subtle.importKey("raw", password, { name: "PBKDF2" }, false, ["deriveBits"]);
258
+ // The derived KEK is this function's allocation and is cleared below. `password` is NOT: when the
259
+ // caller passes a Buffer, passwordBytes hands back that very buffer, so wiping it would destroy
260
+ // the caller's own memory -- the one failure this rule must never cause.
241
261
  var kek = Buffer.from(await subtle.deriveBits({ name: "PBKDF2", hash: _prfHash(prf), salt: salt, iterations: iterations }, kekKey, innerKeyBytes * 8));
242
- // The RFC 3211 double-CBC wrap under an inner AES-256-CBC whose IV is carried in the
243
- // keyEncryptionAlgorithm parameter (id-alg-PWRI-KEK parameter = the inner cipher AlgorithmIdentifier).
244
- var iv = nodeCrypto.randomBytes(16);
245
- var encryptedKey = _pwriWrapIv(kek, cek, iv);
246
- // PBKDF2-params as keyDerivationAlgorithm [0] IMPLICIT.
247
- var kdfParams = pbes2.pbkdf2ParamsSeq(salt, iterations, prf);
248
- var kdfAlg = b.contextConstructed(0, Buffer.concat([b.oid(O("pbkdf2")), kdfParams]));
249
- var keyEncAlg = b.sequence([b.oid(O("id-alg-PWRI-KEK")), b.sequence([b.oid(O("aes256-CBC")), b.octetString(iv)])]);
250
- return { tag: 3, node: b.sequence([b.integer(0n), kdfAlg, keyEncAlg, b.octetString(encryptedKey)]) };
262
+ if (pwOwn.owned) guard.secret.zeroize(password, CmsError, "cms/bad-input", "the password encoding");
263
+ try {
264
+ // The RFC 3211 double-CBC wrap under an inner AES-256-CBC whose IV is carried in the
265
+ // keyEncryptionAlgorithm parameter (id-alg-PWRI-KEK parameter = the inner cipher AlgorithmIdentifier).
266
+ var iv = nodeCrypto.randomBytes(16);
267
+ var encryptedKey = _pwriWrapIv(kek, cek, iv);
268
+ // PBKDF2-params as keyDerivationAlgorithm [0] IMPLICIT.
269
+ var kdfParams = pbes2.pbkdf2ParamsSeq(salt, iterations, prf);
270
+ var kdfAlg = b.contextConstructed(0, Buffer.concat([b.oid(O("pbkdf2")), kdfParams]));
271
+ var keyEncAlg = b.sequence([b.oid(O("id-alg-PWRI-KEK")), b.sequence([b.oid(O("aes256-CBC")), b.octetString(iv)])]);
272
+ return { tag: 3, node: b.sequence([b.integer(0n), kdfAlg, keyEncAlg, b.octetString(encryptedKey)]) };
273
+ } finally {
274
+ guard.secret.zeroize(kek, CmsError, "cms/bad-input", "the password-derived key-encryption key");
275
+ }
251
276
  }
252
277
 
253
278
  // ---- kemri (ML-KEM ori) : RFC 9629 + 9936 ---------------------------------
@@ -319,15 +344,25 @@ function _pwriFormat(cek) {
319
344
  var blk = 16;
320
345
  var padLen = body.length % blk === 0 ? 0 : blk - (body.length % blk);
321
346
  if (body.length + padLen < 2 * blk) padLen += (2 * blk - (body.length + padLen));
322
- return Buffer.concat([body, nodeCrypto.randomBytes(padLen)]);
347
+ // and each hold a plaintext copy of the CEK; only the padded result is returned, so
348
+ // the intermediates are cleared here rather than abandoned.
349
+ var wrapped = Buffer.concat([body, nodeCrypto.randomBytes(padLen)]);
350
+ guard.secret.zeroizeAll([check, body], CmsError, "cms/bad-input", "a PWRI formatting intermediate");
351
+ return wrapped;
323
352
  }
324
353
  function _pwriWrapIv(kek, cek, iv) {
354
+ // wk is the formatted plaintext -- a complete copy of the CEK -- and is cleared once the first
355
+ // encryption pass has consumed it. pass1 is already ciphertext.
325
356
  var wk = _pwriFormat(cek);
326
- var c1 = nodeCrypto.createCipheriv("aes-256-cbc", kek, iv); c1.setAutoPadding(false);
327
- var pass1 = Buffer.concat([c1.update(wk), c1.final()]);
328
- var iv2 = pass1.subarray(pass1.length - 16);
329
- var c2 = nodeCrypto.createCipheriv("aes-256-cbc", kek, iv2); c2.setAutoPadding(false);
330
- return Buffer.concat([c2.update(pass1), c2.final()]);
357
+ try {
358
+ var c1 = nodeCrypto.createCipheriv("aes-256-cbc", kek, iv); c1.setAutoPadding(false);
359
+ var pass1 = Buffer.concat([c1.update(wk), c1.final()]);
360
+ var iv2 = pass1.subarray(pass1.length - 16);
361
+ var c2 = nodeCrypto.createCipheriv("aes-256-cbc", kek, iv2); c2.setAutoPadding(false);
362
+ return Buffer.concat([c2.update(pass1), c2.final()]);
363
+ } finally {
364
+ guard.secret.zeroize(wk, CmsError, "cms/bad-input", "the PWRI plaintext block");
365
+ }
331
366
  }
332
367
 
333
368
  var PRF_HASH = { hmacWithSHA1: "SHA-1", hmacWithSHA256: "SHA-256", hmacWithSHA384: "SHA-384", hmacWithSHA512: "SHA-512" };
@@ -376,16 +411,23 @@ async function encrypt(content, recipients, opts) {
376
411
  var contentType = opts.contentType || "data";
377
412
  var cek = nodeCrypto.randomBytes(ca.keyBits / 8);
378
413
 
379
- // EncryptedData: a single non-array { cek } or { password } descriptor, no RecipientInfos.
380
- if (!Array.isArray(recipients)) return _encryptedData(contentBytes, recipients, ca, contentType, opts, cek);
414
+ // The CEK protects the content for EVERY recipient, so unlike a per-recipient shared secret it is
415
+ // cleared ONCE, after the last recipient entry is built and the content is encrypted -- wiping it
416
+ // inside the recipient loop would destroy the key the remaining recipients must be given.
417
+ try {
418
+ // EncryptedData: a single non-array { cek } or { password } descriptor, no RecipientInfos.
419
+ if (!Array.isArray(recipients)) return _encryptedData(contentBytes, recipients, ca, contentType, opts, cek);
381
420
 
382
- if (!recipients.length) throw _err("cms/bad-input", "at least one recipient is required (RFC 5652 sec. 6.1)");
383
- var recips = [];
384
- for (var i = 0; i < recipients.length; i++) recips.push(await _buildRecipient(cek, recipients[i], opts));
385
- var riNodes = recips.map(_taggedRecipient);
421
+ if (!recipients.length) throw _err("cms/bad-input", "at least one recipient is required (RFC 5652 sec. 6.1)");
422
+ var recips = [];
423
+ for (var i = 0; i < recipients.length; i++) recips.push(await _buildRecipient(cek, recipients[i], opts));
424
+ var riNodes = recips.map(_taggedRecipient);
386
425
 
387
- if (ca.aead) return _emit(_authEnvelopedData(contentBytes, cek, ca, contentType, opts, riNodes, recips), "authEnvelopedData", opts);
388
- return _emit(_envelopedData(contentBytes, cek, ca, contentType, riNodes, recips), "envelopedData", opts);
426
+ if (ca.aead) return _emit(_authEnvelopedData(contentBytes, cek, ca, contentType, opts, riNodes, recips), "authEnvelopedData", opts);
427
+ return _emit(_envelopedData(contentBytes, cek, ca, contentType, riNodes, recips), "envelopedData", opts);
428
+ } finally {
429
+ guard.secret.zeroize(cek, CmsError, "cms/bad-input", "the content-encryption key");
430
+ }
389
431
  }
390
432
 
391
433
  function _emit(inner, ctName, opts) {
@@ -445,16 +487,27 @@ function _encryptedData(contentBytes, desc, ca, contentType, opts, cek) {
445
487
  }
446
488
 
447
489
  function _encryptedDataPbes2(contentBytes, desc, ca, contentType, iv, opts) {
448
- var password = pbes2.passwordBytes(desc.password, _err, "cms");
490
+ // A string / Uint8Array password is encoded into a buffer THIS toolkit allocated -- a credential
491
+ // copy -- so it is cleared once the derivation has consumed it. A caller-supplied Buffer is
492
+ // borrowed and left intact.
493
+ var pwOwn2 = pbes2.passwordBytesOwned(desc.password, _err, "cms");
494
+ var password = pwOwn2.bytes;
449
495
  var iterations = pbes2.assertIterations(desc.iterations == null ? 600000 : desc.iterations, _err, "cms");
450
496
  var salt = desc.salt ? pbes2.assertSalt(guard.bytes.view(desc.salt, CmsError, "cms/bad-input", "salt"), _err, "cms") : nodeCrypto.randomBytes(16);
451
497
  var prf = desc.prf || "hmacWithSHA256";
498
+ // The password-derived content key is this function's allocation; `password` may be the caller's
499
+ // own buffer (passwordBytes passes a Buffer straight through) and is left alone.
452
500
  var key = nodeCrypto.pbkdf2Sync(password, salt, iterations, ca.keyBits / 8, pbes2.prfNodeByName(prf, _err, "cms"));
453
- var enc = pbes2.cbcEncrypt(key, iv, contentBytes, ca.keyBits);
454
- var contentAlg = pbes2.pbes2AlgId(salt, iterations, prf, ca.oid, iv);
455
- var eci = b.sequence([b.oid(O(contentType)), contentAlg, b.contextPrimitive(0, enc)]);
456
- var inner = b.sequence([b.integer(0n), eci]);
457
- return _emit(inner, "encryptedData", { pem: opts && opts.pem != null ? opts.pem : desc.pem });
501
+ if (pwOwn2.owned) guard.secret.zeroize(password, CmsError, "cms/bad-input", "the password encoding");
502
+ try {
503
+ var enc = pbes2.cbcEncrypt(key, iv, contentBytes, ca.keyBits);
504
+ var contentAlg = pbes2.pbes2AlgId(salt, iterations, prf, ca.oid, iv);
505
+ var eci = b.sequence([b.oid(O(contentType)), contentAlg, b.contextPrimitive(0, enc)]);
506
+ var inner = b.sequence([b.integer(0n), eci]);
507
+ return _emit(inner, "encryptedData", { pem: opts && opts.pem != null ? opts.pem : desc.pem });
508
+ } finally {
509
+ guard.secret.zeroize(key, CmsError, "cms/bad-input", "the password-derived content-encryption key");
510
+ }
458
511
  }
459
512
 
460
513
  // ---- content-encryption primitives ----------------------------------------
@@ -505,65 +558,71 @@ async function authenticate(content, recipients, opts) {
505
558
  // RFC 5652 sec. 9.1: authAttrs MUST be present when the eContentType is not id-data.
506
559
  if (contentType !== "data" && !withAttrs) throw _err("cms/bad-input", "AuthenticatedData with a non-data contentType requires authenticated attributes (RFC 5652 sec. 9.1)");
507
560
 
561
+ // The MAC key is this path's content-encryption key: every recipient is given it, so it is cleared
562
+ // ONCE at the end rather than per recipient, and only after the MAC has been computed over it.
508
563
  var macKey = nodeCrypto.randomBytes(MAC_KEY_OCTETS);
509
- var recips = [];
510
- for (var i = 0; i < recipients.length; i++) recips.push(await _buildRecipient(macKey, recipients[i], opts));
511
- var riNodes = recips.map(_taggedRecipient);
512
-
513
- var digestName = opts.digestAlgorithm || mac.node;
514
- var digestAlgTagged = null, authAttrsDer = null, preimage;
515
- if (withAttrs) {
516
- if (!SUPPORTED_DIGEST[digestName]) throw _err("cms/bad-input", "unsupported digestAlgorithm " + JSON.stringify(digestName) + " (sha256/384/512)");
517
- // sec. 9.2: content-type (== eContentType) + message-digest (== digest(content)) attributes,
518
- // SET-OF-sorted, MACed under the EXPLICIT SET OF tag (0x31) but transmitted [2] IMPLICIT (0xA2).
519
- var mdDigest = nodeCrypto.createHash(digestName).update(contentBytes).digest();
520
- var pairs = [
521
- b.sequence([b.oid(O("contentType")), b.setOf([b.oid(O(contentType))])]),
522
- b.sequence([b.oid(O("messageDigest")), b.setOf([b.octetString(mdDigest)])]),
523
- ];
524
- if (opts.authAttrs && opts.authAttrs.length) pairs = pairs.concat(opts.authAttrs);
525
- // Every authAttr (auto-built or caller-supplied) MUST be a well-formed Attribute SEQUENCE
526
- // { type OBJECT IDENTIFIER, values SET OF } and each type appears at most once (RFC 5652) -- so a
527
- // malformed or duplicate caller attribute is rejected BEFORE it is MACed and emitted, never left to
528
- // fail an operator's parser downstream.
529
- var seenTypes = {};
530
- pairs.forEach(function (p) {
531
- var node;
532
- try { node = asn1.decode(p); } catch (e) { throw _err("cms/bad-input", "an authenticated attribute is not well-formed DER", e); }
533
- if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children || node.children.length !== 2 ||
534
- node.children[1].tagClass !== "universal" || node.children[1].tagNumber !== asn1.TAGS.SET ||
535
- !node.children[1].children || node.children[1].children.length < 1) {
536
- throw _err("cms/bad-input", "an authenticated attribute must be an Attribute SEQUENCE { type, non-empty SET OF value } (RFC 5652)");
537
- }
538
- var t;
539
- try { t = asn1.read.oid(node.children[0]); } catch (e) { throw _err("cms/bad-input", "an authenticated attribute type is not an OBJECT IDENTIFIER", e); }
540
- if (seenTypes[t]) throw _err("cms/bad-input", "authenticated attributes must not repeat an attribute type (RFC 5652): " + t);
541
- seenTypes[t] = 1;
542
- });
543
- var setOf = b.setOf(pairs);
544
- preimage = setOf; // MAC over the 0x31 SET OF
545
- authAttrsDer = b.contextConstructed(2, setOf.subarray(_tlvHeaderLen(setOf))); // [2] IMPLICIT on the wire
546
- digestAlgTagged = b.contextConstructed(1, b.oid(O(digestName))); // [1] IMPLICIT DigestAlgorithmIdentifier
547
- } else {
548
- preimage = contentBytes; // MAC over the eContent value octets
549
- }
550
-
551
- var hmacKey = await subtle.importKey("raw", macKey, { name: "HMAC", hash: mac.wc }, false, ["sign"]);
552
- var macValue = Buffer.from(await subtle.sign({ name: "HMAC" }, hmacKey, preimage));
564
+ try {
565
+ var recips = [];
566
+ for (var i = 0; i < recipients.length; i++) recips.push(await _buildRecipient(macKey, recipients[i], opts));
567
+ var riNodes = recips.map(_taggedRecipient);
568
+
569
+ var digestName = opts.digestAlgorithm || mac.node;
570
+ var digestAlgTagged = null, authAttrsDer = null, preimage;
571
+ if (withAttrs) {
572
+ if (!SUPPORTED_DIGEST[digestName]) throw _err("cms/bad-input", "unsupported digestAlgorithm " + JSON.stringify(digestName) + " (sha256/384/512)");
573
+ // sec. 9.2: content-type (== eContentType) + message-digest (== digest(content)) attributes,
574
+ // SET-OF-sorted, MACed under the EXPLICIT SET OF tag (0x31) but transmitted [2] IMPLICIT (0xA2).
575
+ var mdDigest = nodeCrypto.createHash(digestName).update(contentBytes).digest();
576
+ var pairs = [
577
+ b.sequence([b.oid(O("contentType")), b.setOf([b.oid(O(contentType))])]),
578
+ b.sequence([b.oid(O("messageDigest")), b.setOf([b.octetString(mdDigest)])]),
579
+ ];
580
+ if (opts.authAttrs && opts.authAttrs.length) pairs = pairs.concat(opts.authAttrs);
581
+ // Every authAttr (auto-built or caller-supplied) MUST be a well-formed Attribute SEQUENCE
582
+ // { type OBJECT IDENTIFIER, values SET OF } and each type appears at most once (RFC 5652) -- so a
583
+ // malformed or duplicate caller attribute is rejected BEFORE it is MACed and emitted, never left to
584
+ // fail an operator's parser downstream.
585
+ var seenTypes = {};
586
+ pairs.forEach(function (p) {
587
+ var node;
588
+ try { node = asn1.decode(p); } catch (e) { throw _err("cms/bad-input", "an authenticated attribute is not well-formed DER", e); }
589
+ if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children || node.children.length !== 2 ||
590
+ node.children[1].tagClass !== "universal" || node.children[1].tagNumber !== asn1.TAGS.SET ||
591
+ !node.children[1].children || node.children[1].children.length < 1) {
592
+ throw _err("cms/bad-input", "an authenticated attribute must be an Attribute SEQUENCE { type, non-empty SET OF value } (RFC 5652)");
593
+ }
594
+ var t;
595
+ try { t = asn1.read.oid(node.children[0]); } catch (e) { throw _err("cms/bad-input", "an authenticated attribute type is not an OBJECT IDENTIFIER", e); }
596
+ if (seenTypes[t]) throw _err("cms/bad-input", "authenticated attributes must not repeat an attribute type (RFC 5652): " + t);
597
+ seenTypes[t] = 1;
598
+ });
599
+ var setOf = b.setOf(pairs);
600
+ preimage = setOf; // MAC over the 0x31 SET OF
601
+ authAttrsDer = b.contextConstructed(2, setOf.subarray(_tlvHeaderLen(setOf))); // [2] IMPLICIT on the wire
602
+ digestAlgTagged = b.contextConstructed(1, b.oid(O(digestName))); // [1] IMPLICIT DigestAlgorithmIdentifier
603
+ } else {
604
+ preimage = contentBytes; // MAC over the eContent value octets
605
+ }
553
606
 
554
- var eci = b.sequence([b.oid(O(contentType)), b.explicit(0, b.octetString(contentBytes))]);
555
- var kids = [b.integer(0n), b.setOf(riNodes), _algId(mac.oid)];
556
- if (digestAlgTagged) kids.push(digestAlgTagged);
557
- kids.push(eci);
558
- if (authAttrsDer) kids.push(authAttrsDer);
559
- kids.push(b.octetString(macValue));
560
- var ci = b.sequence([b.oid(O("authData")), b.explicit(0, b.sequence(kids))]);
561
- // Self-verify: the emitted AuthenticatedData MUST re-parse clean through the strict parser, so a
562
- // caller-supplied authenticated attribute with a recognized type but extra RFC 5652 constraints (a
563
- // misplaced type, a signing-time whose value is not a Time) is rejected HERE at build time rather
564
- // than by the recipient's parser after the fact.
565
- try { schemaCms.parse(ci); } catch (e) { throw (e instanceof CmsError) ? e : _err("cms/bad-input", "the supplied authenticated attributes produced an invalid AuthenticatedData", e); }
566
- return opts.pem ? schemaCms.pemEncode(ci, "CMS") : ci;
607
+ var hmacKey = await subtle.importKey("raw", macKey, { name: "HMAC", hash: mac.wc }, false, ["sign"]);
608
+ var macValue = Buffer.from(await subtle.sign({ name: "HMAC" }, hmacKey, preimage));
609
+
610
+ var eci = b.sequence([b.oid(O(contentType)), b.explicit(0, b.octetString(contentBytes))]);
611
+ var kids = [b.integer(0n), b.setOf(riNodes), _algId(mac.oid)];
612
+ if (digestAlgTagged) kids.push(digestAlgTagged);
613
+ kids.push(eci);
614
+ if (authAttrsDer) kids.push(authAttrsDer);
615
+ kids.push(b.octetString(macValue));
616
+ var ci = b.sequence([b.oid(O("authData")), b.explicit(0, b.sequence(kids))]);
617
+ // Self-verify: the emitted AuthenticatedData MUST re-parse clean through the strict parser, so a
618
+ // caller-supplied authenticated attribute with a recognized type but extra RFC 5652 constraints (a
619
+ // misplaced type, a signing-time whose value is not a Time) is rejected HERE at build time rather
620
+ // than by the recipient's parser after the fact.
621
+ try { schemaCms.parse(ci); } catch (e) { throw (e instanceof CmsError) ? e : _err("cms/bad-input", "the supplied authenticated attributes produced an invalid AuthenticatedData", e); }
622
+ return opts.pem ? schemaCms.pemEncode(ci, "CMS") : ci;
623
+ } finally {
624
+ guard.secret.zeroize(macKey, CmsError, "cms/bad-input", "the message-authentication key");
625
+ }
567
626
  }
568
627
 
569
628
  module.exports = { encrypt: encrypt, authenticate: authenticate };
package/lib/hpke.js CHANGED
@@ -94,13 +94,29 @@ function _expand(hash, Nh, prk, info, len) {
94
94
  if (len > 255 * Nh) throw _err("hpke/export-length", "requested length " + len + " exceeds 255*Nh");
95
95
  var out = [], t = Buffer.alloc(0), n = Math.ceil(len / Nh);
96
96
  for (var i = 1; i <= n; i++) {
97
- t = nodeCrypto.createHmac(hash, prk).update(concat([t, info, Buffer.from([i])])).digest();
97
+ // The feedback input carries the PREVIOUS output block, so each round's concatenation holds
98
+ // secret material and is cleared once the HMAC has absorbed it.
99
+ var fed = concat([t, info, Buffer.from([i])]);
100
+ t = nodeCrypto.createHmac(hash, prk).update(fed).digest();
101
+ guard.secret.zeroize(fed, HpkeError, "hpke/bad-input", "an expand feedback input");
98
102
  out.push(t);
99
103
  }
100
- return concat(out).subarray(0, len);
104
+ // An exact-sized owning buffer, not a VIEW over the joined blocks: this is keying material, and a
105
+ // caller clearing what it received must not leave the unused tail of the final HMAC block behind
106
+ // it. The accumulator and each block are cleared here.
107
+ var joined = concat(out);
108
+ var exact = Buffer.alloc(len);
109
+ joined.copy(exact, 0, 0, len);
110
+ guard.secret.zeroize(joined, HpkeError, "hpke/bad-input", "the KDF accumulator");
111
+ for (var oi = 0; oi < out.length; oi++) guard.secret.zeroize(out[oi], HpkeError, "hpke/bad-input", "a KDF block");
112
+ return exact;
101
113
  }
102
114
  function _labeledExtract(kdf, suiteId, salt, label, ikm) {
103
- return _extract(kdf.hash, kdf.Nh, salt, concat([HPKE_V1, suiteId, L(label), ikm]));
115
+ // The concatenation copies the input keying material -- a DH secret or a PSK -- into a fresh
116
+ // buffer, so that copy is cleared once extract has consumed it. The caller still owns .
117
+ var labeled = concat([HPKE_V1, suiteId, L(label), ikm]);
118
+ try { return _extract(kdf.hash, kdf.Nh, salt, labeled); }
119
+ finally { guard.secret.zeroize(labeled, HpkeError, "hpke/bad-input", "the labeled extract input"); }
104
120
  }
105
121
  function _labeledExpand(kdf, suiteId, prk, label, info, len) {
106
122
  return _expand(kdf.hash, kdf.Nh, prk, concat([i2osp(len, 2), HPKE_V1, suiteId, L(label), info]), len);
@@ -177,8 +193,11 @@ function _generate(kem) {
177
193
  function _kemSuiteId(kemId) { return concat([L("KEM"), i2osp(kemId, 2)]); }
178
194
  function _extractAndExpand(kem, dh, kemContext) {
179
195
  var kdf = _kdf(kem.kdf), sid = _kemSuiteId(_kemId(kem));
196
+ // The extract PRK is a secret intermediate this function allocated: it is as sensitive as the DH
197
+ // input it summarises, so it is cleared once the expand has consumed it.
180
198
  var eaePrk = _labeledExtract(kdf, sid, Buffer.alloc(0), "eae_prk", dh);
181
- return _labeledExpand(kdf, sid, eaePrk, "shared_secret", kemContext, kem.Nsecret);
199
+ try { return _labeledExpand(kdf, sid, eaePrk, "shared_secret", kemContext, kem.Nsecret); }
200
+ finally { guard.secret.zeroize(eaePrk, HpkeError, "hpke/bad-input", "the KEM extract PRK"); }
182
201
  }
183
202
  function _kemId(kem) { for (var k in KEMS) if (KEMS[k] === kem) return Number(k); return 0; }
184
203
 
@@ -189,25 +208,49 @@ function _ephemeral(kem, eph) {
189
208
  var kp = _generate(kem);
190
209
  return { skE: kp.privateKey, enc: _exportPublic(kem, kp.publicKey) };
191
210
  }
211
+ // The raw DH output is the KEM's shared secret before extraction: it is this module's allocation
212
+ // and is cleared once _extractAndExpand has consumed it, on every DHKEM arm. In the authenticated
213
+ // arms builds a THIRD copy holding both DH results, so the concatenation is cleared as
214
+ // well -- clearing only the pieces would leave the joined buffer readable.
215
+ function _wipeDh(dh) { guard.secret.zeroize(dh, HpkeError, "hpke/bad-input", "the KEM shared secret"); }
216
+ // EVERY agreement happens inside the protected region. In the authenticated modes the second
217
+ // agreement can throw on an attacker-supplied low-order sender key, and a first secret computed
218
+ // outside the region would be left behind on exactly that failure path.
192
219
  function _encap(kem, pkR, pkRm, eph) {
193
220
  var e = _ephemeral(kem, eph);
194
- var dh = _dh(e.skE, pkR);
195
- return { sharedSecret: _extractAndExpand(kem, dh, concat([e.enc, pkRm])), enc: e.enc };
221
+ var dh = null;
222
+ try {
223
+ dh = _dh(e.skE, pkR);
224
+ return { sharedSecret: _extractAndExpand(kem, dh, concat([e.enc, pkRm])), enc: e.enc };
225
+ } finally { _wipeDh(dh); }
196
226
  }
197
227
  function _decap(kem, enc, skR, pkRm) {
198
228
  var pkE = _importPublic(kem, enc);
199
- var dh = _dh(skR, pkE);
200
- return _extractAndExpand(kem, dh, concat([enc, pkRm]));
229
+ var dh = null;
230
+ try {
231
+ dh = _dh(skR, pkE);
232
+ return _extractAndExpand(kem, dh, concat([enc, pkRm]));
233
+ } finally { _wipeDh(dh); }
201
234
  }
202
235
  function _authEncap(kem, pkR, pkRm, skS, pkSm, eph) {
203
236
  var e = _ephemeral(kem, eph);
204
- var dh = concat([_dh(e.skE, pkR), _dh(skS, pkR)]);
205
- return { sharedSecret: _extractAndExpand(kem, dh, concat([e.enc, pkRm, pkSm])), enc: e.enc };
237
+ var dh1 = null, dh2 = null, dh = null;
238
+ try {
239
+ dh1 = _dh(e.skE, pkR);
240
+ dh2 = _dh(skS, pkR);
241
+ dh = concat([dh1, dh2]);
242
+ return { sharedSecret: _extractAndExpand(kem, dh, concat([e.enc, pkRm, pkSm])), enc: e.enc };
243
+ } finally { _wipeDh(dh1); _wipeDh(dh2); _wipeDh(dh); }
206
244
  }
207
245
  function _authDecap(kem, enc, skR, pkRm, pkS, pkSm) {
208
246
  var pkE = _importPublic(kem, enc);
209
- var dh = concat([_dh(skR, pkE), _dh(skR, pkS)]);
210
- return _extractAndExpand(kem, dh, concat([enc, pkRm, pkSm]));
247
+ var dh1 = null, dh2 = null, dh = null;
248
+ try {
249
+ dh1 = _dh(skR, pkE);
250
+ dh2 = _dh(skR, pkS);
251
+ dh = concat([dh1, dh2]);
252
+ return _extractAndExpand(kem, dh, concat([enc, pkRm, pkSm]));
253
+ } finally { _wipeDh(dh1); _wipeDh(dh2); _wipeDh(dh); }
211
254
  }
212
255
 
213
256
  // ---- key schedule (RFC 9180 sec. 5.1) ----------------------------------------
@@ -227,14 +270,20 @@ function _keySchedule(suite, mode, sharedSecret, info, psk, pskId, role) {
227
270
  var pskIdHash = _labeledExtract(kdf, sid, Buffer.alloc(0), "psk_id_hash", pskId);
228
271
  var infoHash = _labeledExtract(kdf, sid, Buffer.alloc(0), "info_hash", info);
229
272
  var ksc = concat([Buffer.from([mode]), pskIdHash, infoHash]);
273
+ // is the key schedule's master PRK -- every key below is derived from it, so it is the
274
+ // most valuable intermediate here and is cleared once the derivations are done.
230
275
  var secret = _labeledExtract(kdf, sid, sharedSecret, "secret", psk);
231
- var exporterSecret = _labeledExpand(kdf, sid, secret, "exp", ksc, kdf.Nh);
232
- var key = null, baseNonce = null;
233
- if (!aead.exportOnly) {
234
- key = _labeledExpand(kdf, sid, secret, "key", ksc, aead.Nk);
235
- baseNonce = _labeledExpand(kdf, sid, secret, "base_nonce", ksc, aead.Nn);
276
+ try {
277
+ var exporterSecret = _labeledExpand(kdf, sid, secret, "exp", ksc, kdf.Nh);
278
+ var key = null, baseNonce = null;
279
+ if (!aead.exportOnly) {
280
+ key = _labeledExpand(kdf, sid, secret, "key", ksc, aead.Nk);
281
+ baseNonce = _labeledExpand(kdf, sid, secret, "base_nonce", ksc, aead.Nn);
282
+ }
283
+ return new Context(suite, key, baseNonce, exporterSecret, role);
284
+ } finally {
285
+ guard.secret.zeroize(secret, HpkeError, "hpke/bad-input", "the key-schedule PRK");
236
286
  }
237
- return new Context(suite, key, baseNonce, exporterSecret, role);
238
287
  }
239
288
 
240
289
  // ---- AEAD context (RFC 9180 sec. 5.2 / 5.3) ----------------------------------
@@ -246,6 +295,12 @@ function Context(suite, key, baseNonce, exporterSecret, role) {
246
295
  this._suite = suite; this._key = key; this._baseNonce = baseNonce;
247
296
  this._exporterSecret = exporterSecret; this._seq = 0n; this._role = role;
248
297
  }
298
+ // Clear the context's key material. The single-shot entry points below build a context, use it once
299
+ // and drop it, so without this the AEAD key, base nonce and exporter secret stay readable for the
300
+ // process lifetime -- the multi-message API hands the context to the caller, who owns its lifetime.
301
+ Context.prototype._dispose = function () {
302
+ guard.secret.zeroizeAll([this._key, this._baseNonce, this._exporterSecret], HpkeError, "hpke/bad-input", "the HPKE context key material");
303
+ };
249
304
  Context.prototype._nonce = function () {
250
305
  var aead = this._suite.aead;
251
306
  var seqBytes = i2osp(this._seq, aead.Nn);
@@ -431,7 +486,10 @@ function _setupR(ids, enc, skR, opts) {
431
486
  } else {
432
487
  ss = _decap(kem, _buf(enc), r.key, r.pkm);
433
488
  }
434
- return _keySchedule(suite, mode, ss, _buf(opts.info), _buf(opts.psk), _buf(opts.pskId), "R");
489
+ // The recipient derives this shared secret and never hands it back (unlike setupS, whose caller
490
+ // receives it and owns it from then on), so it is cleared once the key schedule has consumed it.
491
+ try { return _keySchedule(suite, mode, ss, _buf(opts.info), _buf(opts.psk), _buf(opts.pskId), "R"); }
492
+ finally { guard.secret.zeroize(ss, HpkeError, "hpke/bad-input", "the KEM shared secret"); }
435
493
  }
436
494
 
437
495
  /**
@@ -453,8 +511,21 @@ function _setupR(ids, enc, skR, opts) {
453
511
  * var out = pki.hpke.seal(ids, pkR, {}, Buffer.from("aad"), Buffer.from("msg"));
454
512
  */
455
513
  function seal(ids, pkR, opts, aad, pt) {
456
- var s = _setupS(ids, pkR, opts);
514
+ // _setupS itself can reject after allocating (an unusable AEAD, a bad psk pairing), so the whole
515
+ // call is inside the protected region rather than only what follows a successful setup.
516
+ var s = null;
517
+ try {
518
+ s = _setupS(ids, pkR, opts);
519
+ // The single-shot wrapper returns neither the setup result nor its shared secret, so no caller can
520
+ // own that buffer -- it is cleared here along with the context. setupS itself still hands both to
521
+ // its caller, whose lifetime they become.
457
522
  return { enc: s.enc, ct: s.context.seal(_buf(aad), _buf(pt)) };
523
+ } finally {
524
+ if (s) {
525
+ s.context._dispose();
526
+ guard.secret.zeroize(s.sharedSecret, HpkeError, "hpke/bad-input", "the KEM shared secret");
527
+ }
528
+ }
458
529
  }
459
530
 
460
531
  /**
@@ -477,7 +548,9 @@ function seal(ids, pkR, opts, aad, pt) {
477
548
  * var pt = pki.hpke.open(ids, o.enc, { skm: skR, pkm: pkR }, {}, Buffer.alloc(0), o.ct);
478
549
  */
479
550
  function open(ids, enc, skR, opts, aad, ct) {
480
- return _setupR(ids, enc, skR, opts).open(_buf(aad), _buf(ct));
551
+ var ctx = _setupR(ids, enc, skR, opts);
552
+ try { return ctx.open(_buf(aad), _buf(ct)); }
553
+ finally { ctx._dispose(); }
481
554
  }
482
555
 
483
556
  module.exports = {