@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.
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 = {
package/lib/key.js CHANGED
@@ -120,11 +120,23 @@ async function encrypt(privateKey, password, opts) {
120
120
  var iterations = pbes2.assertIterations(opts.iterations == null ? 600000 : opts.iterations, _err, "key");
121
121
  var salt = opts.salt != null ? pbes2.assertSalt(guard.bytes.view(opts.salt, KeyError, "key/bad-input", "salt"), _err, "key") : nodeCrypto.randomBytes(16);
122
122
  var iv = nodeCrypto.randomBytes(16);
123
- var dk = nodeCrypto.pbkdf2Sync(pbes2.passwordBytes(password, _err, "key"), salt, iterations, keyBits / 8, prfNode);
124
- var ciphertext = pbes2.cbcEncrypt(dk, iv, der, keyBits);
125
- var epki = b.sequence([pbes2.pbes2AlgId(salt, iterations, prf, cipherName, iv), b.octetString(ciphertext)]);
126
- pkcs8.parseEncrypted(epki); // self-check: the produced EncryptedPrivateKeyInfo re-parses
127
- return opts.pem ? pkcs8.pemEncode(epki, "ENCRYPTED PRIVATE KEY") : epki;
123
+ // The derived key protects a PRIVATE KEY, so it is cleared as soon as the encryption is done. The
124
+ // caller's password is untouched -- passwordBytes returns a supplied Buffer as-is.
125
+ // A string / Uint8Array password is encoded into a buffer THIS toolkit allocated -- a credential
126
+ // copy -- so it is cleared once the derivation has consumed it. A caller-supplied Buffer is
127
+ // borrowed and left intact.
128
+ var pwK = pbes2.passwordBytesOwned(password, _err, "key");
129
+ var dk;
130
+ try { dk = nodeCrypto.pbkdf2Sync(pwK.bytes, salt, iterations, keyBits / 8, prfNode); }
131
+ finally { if (pwK.owned) guard.secret.zeroize(pwK.bytes, KeyError, "key/bad-input", "the password encoding"); }
132
+ try {
133
+ var ciphertext = pbes2.cbcEncrypt(dk, iv, der, keyBits);
134
+ var epki = b.sequence([pbes2.pbes2AlgId(salt, iterations, prf, cipherName, iv), b.octetString(ciphertext)]);
135
+ pkcs8.parseEncrypted(epki); // self-check: the produced EncryptedPrivateKeyInfo re-parses
136
+ return opts.pem ? pkcs8.pemEncode(epki, "ENCRYPTED PRIVATE KEY") : epki;
137
+ } finally {
138
+ guard.secret.zeroize(dk, KeyError, "key/bad-input", "the password-derived encryption key");
139
+ }
128
140
  }
129
141
 
130
142
  /**
@@ -176,7 +188,13 @@ function _decryptPbes2(encAlg, ciphertext, password, opts) {
176
188
  // The shared PBES2 decrypt home (strict params + the uniform decrypt-failed on a wrong key / bad pad). The
177
189
  // plaintext re-parse below is the MAC-less PBES2-CBC integrity check (RFC 8018 sec. 8): a bad pad and a
178
190
  // valid pad whose plaintext is not a PrivateKeyInfo are indistinguishable, and it MUST run (never skipped).
179
- var plaintext = pbes2.pbes2Decrypt(pbes2.passwordBytes(password, _err, "key"), encAlg.parameters, ciphertext, opts, _err, "key");
191
+ // A string / Uint8Array password is encoded into a buffer THIS toolkit allocated -- a credential
192
+ // copy -- so it is cleared once the derivation has consumed it. A caller-supplied Buffer is
193
+ // borrowed and left intact.
194
+ var pwD = pbes2.passwordBytesOwned(password, _err, "key");
195
+ var plaintext;
196
+ try { plaintext = pbes2.pbes2Decrypt(pwD.bytes, encAlg.parameters, ciphertext, opts, _err, "key"); }
197
+ finally { if (pwD.owned) guard.secret.zeroize(pwD.bytes, KeyError, "key/bad-input", "the password encoding"); }
180
198
  try { pkcs8.parse(plaintext); }
181
199
  catch (_e) { throw _err("key/decrypt-failed", "decryption failed"); }
182
200
  return plaintext;
package/lib/lint.js CHANGED
@@ -628,8 +628,10 @@ var CABF_TLS_RULES = [
628
628
 
629
629
  // RFC 9935 -- ML-KEM public keys in X.509 certificates. The OID is the sole authority for the
630
630
  // parameter set; the SPKI BIT STRING is the raw ek, exactly 384k+32 octets for that OID.
631
+ // Encapsulation-key lengths come from the shared ML-KEM parameter registry (FIPS 203 Table 3),
632
+ // keyed by NAME because an SPKI surfaces its algorithm by name on this path.
631
633
  var ML_KEM_EK_LEN = {};
632
- ["id-ml-kem-512", "id-ml-kem-768", "id-ml-kem-1024"].forEach(function (n, i) { ML_KEM_EK_LEN[n] = [800, 1184, 1568][i]; });
634
+ ["id-ml-kem-512", "id-ml-kem-768", "id-ml-kem-1024"].forEach(function (n) { ML_KEM_EK_LEN[n] = oid.kemParams(n).ek; });
633
635
  function _isMlKem(cert) {
634
636
  var spki = cert.subjectPublicKeyInfo;
635
637
  return !!(spki && spki.algorithm && ML_KEM_EK_LEN[spki.algorithm.name] !== undefined);
package/lib/oid.js CHANGED
@@ -693,11 +693,46 @@ function paramsMustBeAbsent(dotted) {
693
693
  return _PARAMS_ABSENT.has(dotted);
694
694
  }
695
695
 
696
+ // ---- ML-KEM parameter sets (FIPS 203 Table 3) --------------------------------
697
+ //
698
+ // ONE row per parameter set, resolvable by dotted OID or by registered name, because the
699
+ // consumers hold different currencies: the CMS codec and the decrypt path key by OID, the
700
+ // linter keys by the SPKI algorithm's name. Before this table the same FIPS 203 constants
701
+ // lived in three modules -- schema-cms's ciphertext lengths, webcrypto's {ek, dk}, and
702
+ // lint's encapsulation-key lengths -- so a fourth consumer (a composite KEM, an HPKE row,
703
+ // HQC by analogy) meant a fourth copy, and the ek values were already duplicated verbatim
704
+ // in two of them. A parameter set is a property OF the algorithm identifier, so it lives
705
+ // beside the registry that resolves one rather than in whichever module needed it first.
706
+ //
707
+ // ek : encapsulation-key octets dk : decapsulation-key octets
708
+ // ct : ciphertext octets ss : shared-secret octets
709
+ // A null-prototype table: a plain object would answer kemParams("toString") with a function
710
+ // inherited from Object.prototype, so the documented fail-closed contract would hold for every
711
+ // input EXCEPT the handful every object already carries -- exactly the ones an untrusted
712
+ // identifier might be.
713
+ var KEM_PARAMS = Object.create(null);
714
+ [["id-ml-kem-512", 800, 1632, 768, 32],
715
+ ["id-ml-kem-768", 1184, 2400, 1088, 32],
716
+ ["id-ml-kem-1024", 1568, 3168, 1568, 32]].forEach(function (r) {
717
+ // FROZEN, because these rows are reachable from the public surface AND one of them now governs a
718
+ // security check: the engine reads .ct to enforce the FIPS 203 sec. 7.3 ciphertext length. A shared
719
+ // mutable row would let any code in the process rewrite that bound once, for every later call --
720
+ // a parameter set is a fact about the algorithm, not a setting an application may retune.
721
+ var row = Object.freeze({ ek: r[1], dk: r[2], ct: r[3], ss: r[4] });
722
+ KEM_PARAMS[byName(r[0])] = row; // by dotted OID
723
+ KEM_PARAMS[r[0]] = row; // and by registered name -- the same frozen row
724
+ });
725
+ Object.freeze(KEM_PARAMS);
726
+ // kemParams(oidOrName) -> the row, or undefined for anything that is not an ML-KEM
727
+ // parameter set. Undefined is the caller's signal to fail closed; it never guesses a size.
728
+ function kemParams(oidOrName) { return KEM_PARAMS[oidOrName]; }
729
+
696
730
  module.exports = {
697
731
  name: name,
698
732
  byName: byName,
699
733
  has: has,
700
734
  paramsMustBeAbsent: paramsMustBeAbsent,
735
+ kemParams: kemParams,
701
736
  register: register,
702
737
  registerFamily: registerFamily,
703
738
  all: all,
package/lib/pbes2.js CHANGED
@@ -38,10 +38,17 @@ var CONTENT_KEYBITS = {}, CONTENT_MODE = {};
38
38
 
39
39
  // A password is an octet string (RFC 8018 sec. 2): a string is UTF-8-encoded deterministically (correct for
40
40
  // non-ASCII, and byte-identical to OpenSSL), a Buffer/Uint8Array used verbatim.
41
- function passwordBytes(p, E, prefix) {
42
- if (Buffer.isBuffer(p)) return p;
43
- if (p instanceof Uint8Array) return Buffer.from(p);
44
- if (typeof p === "string") return Buffer.from(p, "utf8");
41
+ function passwordBytes(p, E, prefix) { return passwordBytesOwned(p, E, prefix).bytes; }
42
+
43
+ // The same conversion, reporting OWNERSHIP. A string or Uint8Array password is encoded into a fresh
44
+ // Buffer this toolkit allocated -- a credential copy that must be cleared once the derivation has
45
+ // consumed it -- while a caller-supplied Buffer is BORROWED and must be left intact. Without this
46
+ // distinction a call site can only be safe by leaving every password encoding readable, which is
47
+ // the common case, since a password arrives as a string far more often than as a Buffer.
48
+ function passwordBytesOwned(p, E, prefix) {
49
+ if (Buffer.isBuffer(p)) return { bytes: p, owned: false };
50
+ if (p instanceof Uint8Array) return { bytes: Buffer.from(p), owned: true };
51
+ if (typeof p === "string") return { bytes: Buffer.from(p, "utf8"), owned: true };
45
52
  throw E(prefix + "/bad-input", "a password must be a string, Buffer, or Uint8Array");
46
53
  }
47
54
 
@@ -148,8 +155,19 @@ function pbes2Encrypt(pwBytes, plaintext, opts, E, prefix) {
148
155
  var iterations = assertIterations(opts.iterations == null ? 2048 : opts.iterations, E, prefix);
149
156
  var salt = opts.salt != null ? assertSalt(opts.salt, E, prefix) : nodeCrypto.randomBytes(16);
150
157
  var iv = opts.iv != null ? opts.iv : nodeCrypto.randomBytes(16);
158
+ // The derived key is this module's allocation and protects the caller's plaintext -- a private key
159
+ // in the pkcs8 / pkcs12 callers -- so it is cleared once the encryption has consumed it. pwBytes is
160
+ // NOT: the caller formatted it and may still own the buffer.
151
161
  var key = nodeCrypto.pbkdf2Sync(pwBytes, salt, iterations, keyBits / 8, prfNode);
152
- return { algId: pbes2AlgId(salt, iterations, prf, cipherName, iv), ct: cbcEncrypt(key, iv, plaintext, keyBits) };
162
+ try {
163
+ return { algId: pbes2AlgId(salt, iterations, prf, cipherName, iv), ct: cbcEncrypt(key, iv, plaintext, keyBits) };
164
+ } finally {
165
+ // E is the caller's error FACTORY, not a class; guard-secret delegates its only throw to
166
+ // guard-bytes, which constructs. yields the error the factory returns, so the
167
+ // currency is compatible -- and the throw is unreachable here regardless, because a freshly
168
+ // allocated pbkdf2 buffer cannot be detached.
169
+ guard.secret.zeroize(key, E, prefix + "/bad-input", "the password-derived encryption key");
170
+ }
153
171
  }
154
172
 
155
173
  // PBES2 AES-CBC DECRYPT: parse the PBES2 params (PBKDF2 kdf + AES-CBC scheme), derive the key from
@@ -182,6 +200,7 @@ function pbes2Decrypt(pwBytes, params, ciphertext, opts, E, prefix) {
182
200
  var dk = nodeCrypto.pbkdf2Sync(pwBytes, pb.salt, pb.iterations, keyBits / 8, pb.prfNode);
183
201
  try { return cbcDecrypt(dk, iv, ciphertext, keyBits); }
184
202
  catch (_e) { throw E(prefix + "/decrypt-failed", "decryption failed"); }
203
+ finally { guard.secret.zeroize(dk, E, prefix + "/bad-input", "the password-derived decryption key"); }
185
204
  }
186
205
 
187
206
  // ---- PBMAC1 (RFC 9579 / RFC 8018 App. A.5) : PBKDF2 -> HMAC over a message --
@@ -207,14 +226,24 @@ function pbmac1(pwBytes, salt, iterationCount, keyLength, prfHash, macHash, mess
207
226
  return subtle.importKey("raw", pwBytes, { name: "PBKDF2" }, false, ["deriveBits"]).then(function (baseKey) {
208
227
  return subtle.deriveBits({ name: "PBKDF2", salt: salt, iterations: iterationCount, hash: prfHash }, baseKey, keyLength * 8);
209
228
  }).then(function (bits) {
210
- return subtle.importKey("raw", Buffer.from(bits), { name: "HMAC", hash: macHash }, false, ["sign"]).then(function (hmacKey) {
229
+ // The derived MAC key is this function's allocation, in two forms: the ArrayBuffer deriveBits
230
+ // returned and the Buffer view over it that is imported. Clearing the view clears both, and it
231
+ // runs whether the signing succeeds or throws -- the sibling PBES2 encrypt/decrypt above clear
232
+ // their derived keys, and every PBMAC1 consumer (PKCS#12 build and verification) inherits this.
233
+ var mk = Buffer.from(bits);
234
+ return subtle.importKey("raw", mk, { name: "HMAC", hash: macHash }, false, ["sign"]).then(function (hmacKey) {
211
235
  return subtle.sign({ name: "HMAC" }, hmacKey, message);
236
+ }).finally(function () {
237
+ // pbmac1 takes no caller error factory, and this key is engine-adjacent, so the engine's
238
+ // typed class is the right currency -- a raw TypeError must never escape a lib boundary.
239
+ // Unreachable in practice: a freshly allocated Buffer cannot be detached.
240
+ guard.secret.zeroize(mk, webcrypto.WebCryptoError, "webcrypto/operation", "the PBMAC1 key");
212
241
  });
213
242
  }).then(function (sig) { return Buffer.from(sig); });
214
243
  }
215
244
 
216
245
  module.exports = {
217
- passwordBytes: passwordBytes, assertIterations: assertIterations, assertSalt: assertSalt,
246
+ passwordBytes: passwordBytes, passwordBytesOwned: passwordBytesOwned, assertIterations: assertIterations, assertSalt: assertSalt,
218
247
  prfNodeByName: prfNodeByName, prfNodeByOid: prfNodeByOid,
219
248
  pbkdf2ParamsSeq: pbkdf2ParamsSeq, pbes2AlgId: pbes2AlgId, parsePbkdf2Params: parsePbkdf2Params,
220
249
  requireChildren: requireChildren, seqChildren: seqChildren,
@@ -100,7 +100,17 @@ var DIGEST_NAME = { sha1: "sha1", sha256: "sha256", sha384: "sha384", sha512: "s
100
100
  // RFC 7292 App. B.1: a PKCS#12 password is the BMPString (UTF-16BE) encoding of the string plus a trailing
101
101
  // 2-byte NULL terminator. "Beavis" -> 14 bytes. A non-BMP scalar (surrogate) is rejected. A Buffer/Uint8Array
102
102
  // is taken verbatim as already-formatted bytes (an escape hatch for a caller that pre-encodes).
103
- function _p12Password(pw) {
103
+ function _p12Password(pw) { return _p12PasswordOwned(pw).bytes; }
104
+
105
+ // The same encoding, reporting OWNERSHIP -- mirroring pbes2.passwordBytesOwned. A caller-supplied
106
+ // Buffer is returned AS-IS and is BORROWED: clearing it would destroy the caller's own credential,
107
+ // which is a worse defect than leaving a copy readable. Every other input is re-encoded into a
108
+ // buffer this module allocated, which it must clear once a derivation has consumed it.
109
+ function _p12PasswordOwned(pw) {
110
+ if (Buffer.isBuffer(pw)) return { bytes: pw, owned: false };
111
+ return { bytes: _p12Encode(pw), owned: true };
112
+ }
113
+ function _p12Encode(pw) {
104
114
  if (pw == null) pw = "";
105
115
  if (Buffer.isBuffer(pw)) return pw;
106
116
  if (pw instanceof Uint8Array) return Buffer.from(pw);
@@ -148,16 +158,43 @@ function _blockFill(src, blockSize) {
148
158
  function _p12Kdf(hashName, id, pwBytes, salt, iterations, nBytes) {
149
159
  var uv = P12_KDF_UV[hashName], u = uv.u, v = uv.v;
150
160
  var D = Buffer.alloc(v, id); // step 1: v copies of the ID diversifier
151
- var I = Buffer.concat([_blockFill(salt, v), _blockFill(pwBytes, v)]); // steps 2-4: I = S || P
161
+ // Each fill is a full block-repeated copy of the salt / password; only their concatenation is
162
+ // used, so the sources are cleared rather than abandoned.
163
+ var sFill = _blockFill(salt, v), pFill = _blockFill(pwBytes, v);
164
+ var I = Buffer.concat([sFill, pFill]); // steps 2-4: I = S || P
165
+ guard.secret.zeroizeAll([sFill, pFill], Pkcs12Error, "pkcs12/bad-input", "a key-derivation fill");
152
166
  var c = Math.ceil(nBytes / u); // step 5
153
- var out = Buffer.alloc(0);
167
+ // The accumulator is allocated ONCE at its final size. Growing it with Buffer.concat per round
168
+ // would abandon a password-derived copy of everything derived so far on every iteration, and
169
+ // those copies could not be reached to clear. Each intermediate digest is cleared as it is
170
+ // superseded, so only the value actually in use is ever readable.
171
+ var out = Buffer.alloc(c * u);
154
172
  for (var i = 0; i < c; i++) { // step 6
155
173
  var A = Buffer.concat([D, I]);
156
- for (var r = 0; r < iterations; r++) A = nodeCrypto.createHash(hashName).update(A).digest(); // 6A: A = H^r(D||I)
157
- out = Buffer.concat([out, A]); // step 7 (accumulate A_1..A_c)
158
- if (i < c - 1) _kdfStepC(I, _blockFill(A, v), v); // 6B+6C: modify I for the next block
174
+ for (var r = 0; r < iterations; r++) { // 6A: A = H^r(D||I)
175
+ var prev = A;
176
+ A = nodeCrypto.createHash(hashName).update(A).digest();
177
+ guard.secret.zeroize(prev, Pkcs12Error, "pkcs12/bad-input", "a key-derivation intermediate");
178
+ }
179
+ A.copy(out, i * u); // step 7 (accumulate A_1..A_c)
180
+ if (i < c - 1) { // 6B+6C: modify I for the next block
181
+ var B = _blockFill(A, v); // a full password-derived block
182
+ _kdfStepC(I, B, v);
183
+ guard.secret.zeroize(B, Pkcs12Error, "pkcs12/bad-input", "a key-derivation intermediate");
184
+ }
185
+ guard.secret.zeroize(A, Pkcs12Error, "pkcs12/bad-input", "a key-derivation block");
159
186
  }
160
- return out.subarray(0, nBytes); // step 8: first nBytes of A
187
+ // I is S || P -- it carries the password bytes -- and is finished with here.
188
+ guard.secret.zeroize(I, Pkcs12Error, "pkcs12/bad-input", "the key-derivation input block");
189
+ // Step 8 is "the first nBytes of A", but returning a SUBARRAY would hand back a view over a
190
+ // larger allocation: a caller clearing what it received would leave the rest of the final digest
191
+ // block -- password-derived material -- readable in the same backing buffer (15 unused bytes
192
+ // behind a 5-byte RC2 key, 16 behind a 24-byte 3DES key). Copy into an exact-sized buffer the
193
+ // caller wholly owns, and clear the oversized accumulator here, where it was allocated.
194
+ var exact = Buffer.alloc(nBytes);
195
+ out.copy(exact, 0, 0, nBytes);
196
+ guard.secret.zeroize(out, Pkcs12Error, "pkcs12/bad-input", "the key-derivation accumulator");
197
+ return exact;
161
198
  }
162
199
 
163
200
  // RFC 7292 App. B.2 step 6C: treat I as consecutive v-byte blocks and set each I_j = (I_j + B + 1) mod 2^(8v)
@@ -344,10 +381,18 @@ async function _buildMacData(macOpts, sharedPassword, authSafeDer) {
344
381
  var node = DIGEST_NAME[hash];
345
382
  if (!node || !P12_KDF_UV[node]) throw _err("pkcs12/unsupported-algorithm", "unsupported classic MAC hash " + JSON.stringify(hash) + " (sha1 / sha256 / sha384 / sha512)");
346
383
  var iter = _assertMacIter(macOpts.iterations == null ? DEFAULT_MAC_ITER : macOpts.iterations, CLASSIC_MAC_MAX_ITERATIONS);
347
- var macKey = _p12Kdf(node, 3, _p12Password(password), salt, iter, P12_KDF_UV[node].u); // classic KDF -> BMPString
348
- var digest = nodeCrypto.createHmac(node, macKey).update(authSafeDer).digest();
349
- var digestInfo = b.sequence([b.sequence([b.oid(O(node)), b.nullValue()]), b.octetString(digest)]);
350
- return b.sequence([digestInfo, b.octetString(salt), b.integer(BigInt(iter))]);
384
+ // _p12Password always ALLOCATES (it re-encodes to BMPString + NULL), so its result is this
385
+ // module's own credential copy and is cleared once the derivation has consumed it.
386
+ var macPw = _p12PasswordOwned(password);
387
+ var macKey = _p12Kdf(node, 3, macPw.bytes, salt, iter, P12_KDF_UV[node].u); // classic KDF -> BMPString
388
+ if (macPw.owned) guard.secret.zeroize(macPw.bytes, Pkcs12Error, "pkcs12/bad-input", "the password encoding");
389
+ try {
390
+ var digest = nodeCrypto.createHmac(node, macKey).update(authSafeDer).digest();
391
+ var digestInfo = b.sequence([b.sequence([b.oid(O(node)), b.nullValue()]), b.octetString(digest)]);
392
+ return b.sequence([digestInfo, b.octetString(salt), b.integer(BigInt(iter))]);
393
+ } finally {
394
+ guard.secret.zeroize(macKey, Pkcs12Error, "pkcs12/bad-input", "the password-derived MAC key");
395
+ }
351
396
  }
352
397
  if (algorithm === "pbmac1") {
353
398
  var prf = PBMAC1_PRF[hash];
@@ -495,8 +540,11 @@ async function verifyMac(pfx, password, opts) {
495
540
  if (!P12_KDF_UV[node]) throw _err("pkcs12/unsupported-algorithm", "unsupported classic MAC hash " + m.mac.hashName);
496
541
  // The iteration count and salt are attacker-controlled work factors: bound them BEFORE the KDF runs.
497
542
  _capWork(m.mac.iterations, m.mac.macSalt, opts, undefined, CLASSIC_MAC_MAX_ITERATIONS);
498
- var macKey = _p12Kdf(node, 3, _p12Password(password), m.mac.macSalt, m.mac.iterations, P12_KDF_UV[node].u); // classic KDF -> BMPString
499
- computed = nodeCrypto.createHmac(node, macKey).update(m.macedBytes).digest();
543
+ var vMacPw = _p12PasswordOwned(password);
544
+ var macKey = _p12Kdf(node, 3, vMacPw.bytes, m.mac.macSalt, m.mac.iterations, P12_KDF_UV[node].u); // classic KDF -> BMPString
545
+ if (vMacPw.owned) guard.secret.zeroize(vMacPw.bytes, Pkcs12Error, "pkcs12/bad-input", "the password encoding");
546
+ try { computed = nodeCrypto.createHmac(node, macKey).update(m.macedBytes).digest(); }
547
+ finally { guard.secret.zeroize(macKey, Pkcs12Error, "pkcs12/bad-input", "the password-derived MAC key"); }
500
548
  } else {
501
549
  var kdf = m.mac.pbmac1.kdf;
502
550
  var prfWc = PRF_WC[kdf.prfName];
@@ -693,17 +741,22 @@ function _decryptLegacyPbe(ea, ct, password, opts, budget) {
693
741
  var u = P12_KDF_UV[scheme.hash].u;
694
742
  budget.rounds -= (Math.ceil(scheme.keyLen / u) + Math.ceil(scheme.ivLen / u)) * iterations;
695
743
  if (budget.rounds < 0) throw _err("pkcs12/iteration-limit", "the store's aggregate legacy PBE key-derivation work exceeds the budget (a hostile many-bag store)");
696
- var p12pw = _p12Password(password); // App. B.1 BMPString + NULL (NOT the PBES2 UTF-8 encoding)
697
- var keyM = _p12Kdf(scheme.hash, 1, p12pw, salt, iterations, scheme.keyLen);
698
- var iv = _p12Kdf(scheme.hash, 2, p12pw, salt, iterations, scheme.ivLen);
744
+ var p12pw = _p12PasswordOwned(password); // App. B.1 BMPString + NULL (NOT the PBES2 UTF-8 encoding)
745
+ var keyM = _p12Kdf(scheme.hash, 1, p12pw.bytes, salt, iterations, scheme.keyLen);
746
+ var iv = _p12Kdf(scheme.hash, 2, p12pw.bytes, salt, iterations, scheme.ivLen);
747
+ if (p12pw.owned) guard.secret.zeroize(p12pw.bytes, Pkcs12Error, "pkcs12/bad-input", "the password encoding");
699
748
  // BOTH ciphers funnel through ONE uniform failure (same code AND message), so a bad-pad (secret-dependent)
700
749
  // and a valid-pad-wrong-content outcome are indistinguishable -- no CBC padding oracle (RFC 8018 sec. 8). RC2
701
750
  // is unavailable in node; the in-tree RFC 2268 primitive fills the gap and its typed reject is normalized here.
751
+ // keyM is a password-derived key this function allocated -- the PBES2 arm of the same dispatch
752
+ // clears its equivalent, so this arm owes it too, on the failing path an attacker drives. The
753
+ // derived IV is not secret and is left alone.
702
754
  try {
703
755
  if (scheme.rc2) return rc2.cbcDecrypt(keyM, scheme.rc2, iv, ct, _err, "pkcs12/decrypt-failed");
704
756
  var d = nodeCrypto.createDecipheriv(scheme.cipher, keyM, iv);
705
757
  return Buffer.concat([d.update(ct), d.final()]);
706
758
  } catch (_e) { throw _err("pkcs12/decrypt-failed", "decryption failed"); }
759
+ finally { guard.secret.zeroize(keyM, Pkcs12Error, "pkcs12/bad-input", "the password-derived decryption key"); }
707
760
  }
708
761
 
709
762
  // Decrypt a PBES2 (RFC 8018) or legacy-PBE (RFC 7292 App. C) bag/safe -- dispatch on the encryptionAlgorithm
package/lib/schema-cms.js CHANGED
@@ -106,10 +106,10 @@ WRAP_KEK_LENGTHS[oid.byName("aes256-wrap")] = 32;
106
106
  // ML-KEM OID -> the exact ciphertext (kemct) length in octets (FIPS 203). A
107
107
  // recognized ML-KEM kem carries a fixed-size ciphertext; any other length can
108
108
  // never decapsulate. (The params-absent rule rides the shared oid registry.)
109
+ // Ciphertext lengths come from the shared ML-KEM parameter registry (FIPS 203 Table 3) rather
110
+ // than a local copy, so this codec and the crypto engine cannot disagree about a parameter set.
109
111
  var KEM_CT_LENGTHS = {};
110
- KEM_CT_LENGTHS[oid.byName("id-ml-kem-512")] = 768;
111
- KEM_CT_LENGTHS[oid.byName("id-ml-kem-768")] = 1088;
112
- KEM_CT_LENGTHS[oid.byName("id-ml-kem-1024")] = 1568;
112
+ ["id-ml-kem-512", "id-ml-kem-768", "id-ml-kem-1024"].forEach(function (n) { KEM_CT_LENGTHS[oid.byName(n)] = oid.kemParams(n).ct; });
113
113
 
114
114
  // Recognized AEAD content-encryption OIDs -> the AES-GCM/CCM parameter shape + the
115
115
  // legal ICVlen set (RFC 5084). An unrecognized content-encryption OID surfaces its