@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/CHANGELOG.md +50 -0
- package/README.md +1 -1
- package/lib/cms-decrypt.js +162 -65
- package/lib/cms-encrypt.js +223 -150
- package/lib/guard-all.js +2 -0
- package/lib/guard-secret.js +82 -0
- package/lib/hpke.js +94 -21
- package/lib/key.js +24 -6
- package/lib/lint.js +3 -1
- package/lib/oid.js +35 -0
- package/lib/pbes2.js +36 -7
- package/lib/pkcs12-build.js +69 -16
- package/lib/schema-cms.js +3 -3
- package/lib/webcrypto.js +220 -48
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
195
|
-
|
|
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 =
|
|
200
|
-
|
|
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
|
|
205
|
-
|
|
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
|
|
210
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
package/lib/pkcs12-build.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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++)
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
var
|
|
350
|
-
|
|
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
|
|
499
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|