@blamejs/pki 0.4.12 → 0.4.14

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 ---------------------------------
@@ -275,15 +300,29 @@ async function _buildKemri(cek, cert, opts) {
275
300
  var pub = await subtle.importKey("spki", cert.subjectPublicKeyInfo.bytes, { name: wcName }, false, ["encapsulateBits"]);
276
301
  var kem = await subtle.encapsulateBits({ name: wcName }, pub);
277
302
  var ss = Buffer.from(kem.sharedKey), kemct = Buffer.from(kem.ciphertext);
278
- var ssKey = await subtle.importKey("raw", ss, { name: "HKDF" }, false, ["deriveBits"]);
279
- var kek = Buffer.from(await subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt: Buffer.alloc(0), info: _kemOtherInfo(wrapName, kekBytes, ukm) }, ssKey, kekBytes * 8));
280
- var encryptedKey = await _aesKwWrap(kek, cek);
281
- var rid = _rid(cert, opts.keyIdentifier);
282
- var kemriKids = [b.integer(0n), rid.node, _algId(oid.name(keyOid), "absent"), b.octetString(kemct), _algId("hkdfWithSha256", "absent"), b.integer(BigInt(kekBytes))];
283
- if (ukm) kemriKids.push(b.explicit(0, b.octetString(ukm)));
284
- kemriKids.push(_algId(wrapName, "absent"), b.octetString(encryptedKey));
285
- var kemri = b.sequence(kemriKids);
286
- return { tag: 4, node: b.sequence([b.oid(O("kem")), kemri]) };
303
+ var kek = null, kekAb = null;
304
+ try {
305
+ var ssKey = await subtle.importKey("raw", ss, { name: "HKDF" }, false, ["deriveBits"]);
306
+ kekAb = await subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt: Buffer.alloc(0), info: _kemOtherInfo(wrapName, kekBytes, ukm) }, ssKey, kekBytes * 8);
307
+ kek = Buffer.from(kekAb);
308
+ var encryptedKey = await _aesKwWrap(kek, cek);
309
+ var rid = _rid(cert, opts.keyIdentifier);
310
+ var kemriKids = [b.integer(0n), rid.node, _algId(oid.name(keyOid), "absent"), b.octetString(kemct), _algId("hkdfWithSha256", "absent"), b.integer(BigInt(kekBytes))];
311
+ if (ukm) kemriKids.push(b.explicit(0, b.octetString(ukm)));
312
+ kemriKids.push(_algId(wrapName, "absent"), b.octetString(encryptedKey));
313
+ var kemri = b.sequence(kemriKids);
314
+ return { tag: 4, node: b.sequence([b.oid(O("kem")), kemri]) };
315
+ } finally {
316
+ // RFC 9629 sec. 7 asks the SENDER to discard the shared secret and KEK once the recipient entry
317
+ // is built -- and to use a fresh secret per recipient, so this runs per call rather than once at
318
+ // the end of a multi-recipient message. In a `finally`, so a wrap or encoding failure does not
319
+ // leave them behind. The CEK is the caller's and is wiped by no one here; kemct is public.
320
+ // kem.sharedKey is the ArrayBuffer the engine returned and ss is this function's copy of it;
321
+ // both hold the secret, so both are cleared. A Uint8Array view aliases the buffer's bytes, so
322
+ // wiping the view wipes the buffer itself. kem.ciphertext is public and stays.
323
+ guard.secret.zeroizeAll([ss, kek, kem.sharedKey ? new Uint8Array(kem.sharedKey) : null, kekAb ? new Uint8Array(kekAb) : null],
324
+ CmsError, "cms/bad-input", "the KEM shared secret");
325
+ }
287
326
  }
288
327
 
289
328
  // AES-KW wrap of the CEK under a raw KEK.
@@ -305,15 +344,25 @@ function _pwriFormat(cek) {
305
344
  var blk = 16;
306
345
  var padLen = body.length % blk === 0 ? 0 : blk - (body.length % blk);
307
346
  if (body.length + padLen < 2 * blk) padLen += (2 * blk - (body.length + padLen));
308
- 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;
309
352
  }
310
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.
311
356
  var wk = _pwriFormat(cek);
312
- var c1 = nodeCrypto.createCipheriv("aes-256-cbc", kek, iv); c1.setAutoPadding(false);
313
- var pass1 = Buffer.concat([c1.update(wk), c1.final()]);
314
- var iv2 = pass1.subarray(pass1.length - 16);
315
- var c2 = nodeCrypto.createCipheriv("aes-256-cbc", kek, iv2); c2.setAutoPadding(false);
316
- 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
+ }
317
366
  }
318
367
 
319
368
  var PRF_HASH = { hmacWithSHA1: "SHA-1", hmacWithSHA256: "SHA-256", hmacWithSHA384: "SHA-384", hmacWithSHA512: "SHA-512" };
@@ -362,16 +411,23 @@ async function encrypt(content, recipients, opts) {
362
411
  var contentType = opts.contentType || "data";
363
412
  var cek = nodeCrypto.randomBytes(ca.keyBits / 8);
364
413
 
365
- // EncryptedData: a single non-array { cek } or { password } descriptor, no RecipientInfos.
366
- if (!Array.isArray(recipients)) return _encryptedData(contentBytes, recipients, ca, contentType, opts, cek);
367
-
368
- if (!recipients.length) throw _err("cms/bad-input", "at least one recipient is required (RFC 5652 sec. 6.1)");
369
- var recips = [];
370
- for (var i = 0; i < recipients.length; i++) recips.push(await _buildRecipient(cek, recipients[i], opts));
371
- var riNodes = recips.map(_taggedRecipient);
372
-
373
- if (ca.aead) return _emit(_authEnvelopedData(contentBytes, cek, ca, contentType, opts, riNodes, recips), "authEnvelopedData", opts);
374
- return _emit(_envelopedData(contentBytes, cek, ca, contentType, riNodes, recips), "envelopedData", opts);
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);
420
+
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);
425
+
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
+ }
375
431
  }
376
432
 
377
433
  function _emit(inner, ctName, opts) {
@@ -431,16 +487,27 @@ function _encryptedData(contentBytes, desc, ca, contentType, opts, cek) {
431
487
  }
432
488
 
433
489
  function _encryptedDataPbes2(contentBytes, desc, ca, contentType, iv, opts) {
434
- 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;
435
495
  var iterations = pbes2.assertIterations(desc.iterations == null ? 600000 : desc.iterations, _err, "cms");
436
496
  var salt = desc.salt ? pbes2.assertSalt(guard.bytes.view(desc.salt, CmsError, "cms/bad-input", "salt"), _err, "cms") : nodeCrypto.randomBytes(16);
437
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.
438
500
  var key = nodeCrypto.pbkdf2Sync(password, salt, iterations, ca.keyBits / 8, pbes2.prfNodeByName(prf, _err, "cms"));
439
- var enc = pbes2.cbcEncrypt(key, iv, contentBytes, ca.keyBits);
440
- var contentAlg = pbes2.pbes2AlgId(salt, iterations, prf, ca.oid, iv);
441
- var eci = b.sequence([b.oid(O(contentType)), contentAlg, b.contextPrimitive(0, enc)]);
442
- var inner = b.sequence([b.integer(0n), eci]);
443
- 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
+ }
444
511
  }
445
512
 
446
513
  // ---- content-encryption primitives ----------------------------------------
@@ -491,65 +558,71 @@ async function authenticate(content, recipients, opts) {
491
558
  // RFC 5652 sec. 9.1: authAttrs MUST be present when the eContentType is not id-data.
492
559
  if (contentType !== "data" && !withAttrs) throw _err("cms/bad-input", "AuthenticatedData with a non-data contentType requires authenticated attributes (RFC 5652 sec. 9.1)");
493
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.
494
563
  var macKey = nodeCrypto.randomBytes(MAC_KEY_OCTETS);
495
- var recips = [];
496
- for (var i = 0; i < recipients.length; i++) recips.push(await _buildRecipient(macKey, recipients[i], opts));
497
- var riNodes = recips.map(_taggedRecipient);
498
-
499
- var digestName = opts.digestAlgorithm || mac.node;
500
- var digestAlgTagged = null, authAttrsDer = null, preimage;
501
- if (withAttrs) {
502
- if (!SUPPORTED_DIGEST[digestName]) throw _err("cms/bad-input", "unsupported digestAlgorithm " + JSON.stringify(digestName) + " (sha256/384/512)");
503
- // sec. 9.2: content-type (== eContentType) + message-digest (== digest(content)) attributes,
504
- // SET-OF-sorted, MACed under the EXPLICIT SET OF tag (0x31) but transmitted [2] IMPLICIT (0xA2).
505
- var mdDigest = nodeCrypto.createHash(digestName).update(contentBytes).digest();
506
- var pairs = [
507
- b.sequence([b.oid(O("contentType")), b.setOf([b.oid(O(contentType))])]),
508
- b.sequence([b.oid(O("messageDigest")), b.setOf([b.octetString(mdDigest)])]),
509
- ];
510
- if (opts.authAttrs && opts.authAttrs.length) pairs = pairs.concat(opts.authAttrs);
511
- // Every authAttr (auto-built or caller-supplied) MUST be a well-formed Attribute SEQUENCE
512
- // { type OBJECT IDENTIFIER, values SET OF } and each type appears at most once (RFC 5652) -- so a
513
- // malformed or duplicate caller attribute is rejected BEFORE it is MACed and emitted, never left to
514
- // fail an operator's parser downstream.
515
- var seenTypes = {};
516
- pairs.forEach(function (p) {
517
- var node;
518
- try { node = asn1.decode(p); } catch (e) { throw _err("cms/bad-input", "an authenticated attribute is not well-formed DER", e); }
519
- if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children || node.children.length !== 2 ||
520
- node.children[1].tagClass !== "universal" || node.children[1].tagNumber !== asn1.TAGS.SET ||
521
- !node.children[1].children || node.children[1].children.length < 1) {
522
- throw _err("cms/bad-input", "an authenticated attribute must be an Attribute SEQUENCE { type, non-empty SET OF value } (RFC 5652)");
523
- }
524
- var t;
525
- 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); }
526
- if (seenTypes[t]) throw _err("cms/bad-input", "authenticated attributes must not repeat an attribute type (RFC 5652): " + t);
527
- seenTypes[t] = 1;
528
- });
529
- var setOf = b.setOf(pairs);
530
- preimage = setOf; // MAC over the 0x31 SET OF
531
- authAttrsDer = b.contextConstructed(2, setOf.subarray(_tlvHeaderLen(setOf))); // [2] IMPLICIT on the wire
532
- digestAlgTagged = b.contextConstructed(1, b.oid(O(digestName))); // [1] IMPLICIT DigestAlgorithmIdentifier
533
- } else {
534
- preimage = contentBytes; // MAC over the eContent value octets
535
- }
536
-
537
- var hmacKey = await subtle.importKey("raw", macKey, { name: "HMAC", hash: mac.wc }, false, ["sign"]);
538
- 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
+ }
539
606
 
540
- var eci = b.sequence([b.oid(O(contentType)), b.explicit(0, b.octetString(contentBytes))]);
541
- var kids = [b.integer(0n), b.setOf(riNodes), _algId(mac.oid)];
542
- if (digestAlgTagged) kids.push(digestAlgTagged);
543
- kids.push(eci);
544
- if (authAttrsDer) kids.push(authAttrsDer);
545
- kids.push(b.octetString(macValue));
546
- var ci = b.sequence([b.oid(O("authData")), b.explicit(0, b.sequence(kids))]);
547
- // Self-verify: the emitted AuthenticatedData MUST re-parse clean through the strict parser, so a
548
- // caller-supplied authenticated attribute with a recognized type but extra RFC 5652 constraints (a
549
- // misplaced type, a signing-time whose value is not a Time) is rejected HERE at build time rather
550
- // than by the recipient's parser after the fact.
551
- try { schemaCms.parse(ci); } catch (e) { throw (e instanceof CmsError) ? e : _err("cms/bad-input", "the supplied authenticated attributes produced an invalid AuthenticatedData", e); }
552
- 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
+ }
553
626
  }
554
627
 
555
628
  module.exports = { encrypt: encrypt, authenticate: authenticate };
package/lib/guard-all.js CHANGED
@@ -59,6 +59,7 @@ var json = require("./guard-json");
59
59
  var identifier = require("./guard-identifier");
60
60
  var header = require("./guard-header");
61
61
  var compress = require("./guard-compress");
62
+ var secret = require("./guard-secret");
62
63
 
63
64
  module.exports = {
64
65
  bytes: bytes,
@@ -73,4 +74,5 @@ module.exports = {
73
74
  identifier: identifier,
74
75
  header: header,
75
76
  compress: compress,
77
+ secret: secret,
76
78
  };
@@ -0,0 +1,82 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- no operator-facing namespace. The documented surface is the KEM
6
+ // key-establishment paths that compose this guard (pki.cms.encrypt / decrypt).
7
+ //
8
+ // guard-secret -- wipe a secret buffer the TOOLKIT ALLOCATED at the moment it
9
+ // stops being needed. NIST SP 800-227 RS5 / sec. 4.2 requires that a KEM shared
10
+ // secret and every intermediate value be destroyed as soon as they are no longer
11
+ // needed; RFC 9629 sec. 7 says the same of the KEK a KEMRecipientInfo derives.
12
+ //
13
+ // The defended class is secret lifetime, not secret disclosure: a shared secret
14
+ // or KEK left readable in the heap widens the window in which a later memory
15
+ // disclosure -- a core dump, a swapped page, a same-process read primitive --
16
+ // yields key material for traffic that was already decrypted.
17
+ //
18
+ // SCOPE, stated honestly because the docstring is the only place a reader learns
19
+ // it: this is BEST EFFORT. The runtime copies buffers into places no JS can
20
+ // reach (node's decapsulate return is copied on the way out; importKey("raw")
21
+ // copies into a KeyObject), and V8 may relocate a backing store, leaving the
22
+ // original bytes behind. Wiping the copies the toolkit holds shortens the
23
+ // window. It does not support a claim that a secret never persists in memory,
24
+ // and no operator-facing text may imply that it does.
25
+ //
26
+ // OWNERSHIP IS THE CONTRACT. Only a buffer the toolkit allocated may be wiped --
27
+ // never a caller's opts.key / opts.cert / opts.kek / opts.password, and never the
28
+ // input DER. Silently destroying a caller's own memory is a worse defect than
29
+ // leaving a secret readable, and it is the failure mode a zeroization patch
30
+ // reaches for first, so the call sites pass only their own intermediates.
31
+
32
+ var bytes = require("./guard-bytes");
33
+
34
+ // zeroize(value, ErrorClass, code, label) -> the same object, cleared.
35
+ // value : a Buffer / TypedArray the TOOLKIT allocated, or null / undefined
36
+ // (absent is a no-op so a `finally` needs no branch around it).
37
+ // ErrorClass : the caller's typed error CONSTRUCTOR, declared with
38
+ // `{ withCause: true }`. The guard family carries two currencies --
39
+ // most guards take a (code, message) factory and call it without
40
+ // `new`, while guard-bytes / guard-header take the class and
41
+ // construct it. This module's ONLY throw is the delegated re-view
42
+ // below, so it must pass what guard-bytes expects: the class, and
43
+ // one that accepts a cause, because guard-bytes threads the raw
44
+ // detach fault through as one. A plain class fails to construct at
45
+ // the single moment the caller needs a real error.
46
+ // code : the frozen domain/reason code a detached buffer rejects under.
47
+ // label : field phrase for the message.
48
+ //
49
+ // A detached ArrayBuffer cannot be written, and reaching one here means a caller
50
+ // handed over memory that was transferred away -- a real fault, not something to
51
+ // swallow, so it routes through the shared re-view guard and throws typed.
52
+ //
53
+ // The `.fill(0)` shape lives ONLY in this module: a wipe re-inlined anywhere in
54
+ // lib/ -- including a module not yet written -- is flagged, so the safe
55
+ // implementation is also the tripwire that stops the next consumer from rolling
56
+ // its own partial one.
57
+ // @enforced-by guard-shape-reinlined
58
+ // @guard-shape \.fill\s*\(\s*0\s*[,)]
59
+ function zeroize(value, ErrorClass, code, label) {
60
+ if (value === null || value === undefined) return value;
61
+ // Re-view through the shared bytes guard: it is the single place that decides
62
+ // what counts as a writable BufferSource and rejects a detached one typed.
63
+ var view = bytes.view(value, ErrorClass, code, label);
64
+ view.fill(0);
65
+ return value;
66
+ }
67
+
68
+ // zeroizeAll(list, ErrorClass, code, label) -- wipe every present member, tolerating
69
+ // holes so a `finally` can name intermediates that may not have been reached.
70
+ //
71
+ // @enforced-by behavioral -- this is a loop over zeroize, which carries the family's only
72
+ // rename-proof shape (the `.fill(0)` above). It introduces no shape of its own, so a lexical
73
+ // detector here would anchor on a renameable symbol and go silently green (drift rule sec. 3).
74
+ // The behavioural guards are guard-secret.test.js (holes tolerated, every member cleared) and the
75
+ // CMS vectors that assert the shared secret and KEK are wiped on BOTH the success and failure paths.
76
+ function zeroizeAll(list, ErrorClass, code, label) {
77
+ if (!list) return list;
78
+ for (var i = 0; i < list.length; i++) zeroize(list[i], ErrorClass, code, label);
79
+ return list;
80
+ }
81
+
82
+ module.exports = { zeroize: zeroize, zeroizeAll: zeroizeAll };