@blamejs/pki 0.4.13 → 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 +32 -1
- package/README.md +1 -1
- package/lib/cms-decrypt.js +123 -60
- package/lib/cms-encrypt.js +198 -139
- package/lib/hpke.js +94 -21
- package/lib/key.js +24 -6
- package/lib/pbes2.js +36 -7
- package/lib/pkcs12-build.js +69 -16
- package/lib/webcrypto.js +154 -62
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/cms-encrypt.js
CHANGED
|
@@ -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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
262
|
+
if (pwOwn.owned) guard.secret.zeroize(password, CmsError, "cms/bad-input", "the password encoding");
|
|
263
|
+
try {
|
|
264
|
+
// The RFC 3211 double-CBC wrap under an inner AES-256-CBC whose IV is carried in the
|
|
265
|
+
// keyEncryptionAlgorithm parameter (id-alg-PWRI-KEK parameter = the inner cipher AlgorithmIdentifier).
|
|
266
|
+
var iv = nodeCrypto.randomBytes(16);
|
|
267
|
+
var encryptedKey = _pwriWrapIv(kek, cek, iv);
|
|
268
|
+
// PBKDF2-params as keyDerivationAlgorithm [0] IMPLICIT.
|
|
269
|
+
var kdfParams = pbes2.pbkdf2ParamsSeq(salt, iterations, prf);
|
|
270
|
+
var kdfAlg = b.contextConstructed(0, Buffer.concat([b.oid(O("pbkdf2")), kdfParams]));
|
|
271
|
+
var keyEncAlg = b.sequence([b.oid(O("id-alg-PWRI-KEK")), b.sequence([b.oid(O("aes256-CBC")), b.octetString(iv)])]);
|
|
272
|
+
return { tag: 3, node: b.sequence([b.integer(0n), kdfAlg, keyEncAlg, b.octetString(encryptedKey)]) };
|
|
273
|
+
} finally {
|
|
274
|
+
guard.secret.zeroize(kek, CmsError, "cms/bad-input", "the password-derived key-encryption key");
|
|
275
|
+
}
|
|
251
276
|
}
|
|
252
277
|
|
|
253
278
|
// ---- kemri (ML-KEM ori) : RFC 9629 + 9936 ---------------------------------
|
|
@@ -319,15 +344,25 @@ function _pwriFormat(cek) {
|
|
|
319
344
|
var blk = 16;
|
|
320
345
|
var padLen = body.length % blk === 0 ? 0 : blk - (body.length % blk);
|
|
321
346
|
if (body.length + padLen < 2 * blk) padLen += (2 * blk - (body.length + padLen));
|
|
322
|
-
|
|
347
|
+
// and each hold a plaintext copy of the CEK; only the padded result is returned, so
|
|
348
|
+
// the intermediates are cleared here rather than abandoned.
|
|
349
|
+
var wrapped = Buffer.concat([body, nodeCrypto.randomBytes(padLen)]);
|
|
350
|
+
guard.secret.zeroizeAll([check, body], CmsError, "cms/bad-input", "a PWRI formatting intermediate");
|
|
351
|
+
return wrapped;
|
|
323
352
|
}
|
|
324
353
|
function _pwriWrapIv(kek, cek, iv) {
|
|
354
|
+
// wk is the formatted plaintext -- a complete copy of the CEK -- and is cleared once the first
|
|
355
|
+
// encryption pass has consumed it. pass1 is already ciphertext.
|
|
325
356
|
var wk = _pwriFormat(cek);
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
357
|
+
try {
|
|
358
|
+
var c1 = nodeCrypto.createCipheriv("aes-256-cbc", kek, iv); c1.setAutoPadding(false);
|
|
359
|
+
var pass1 = Buffer.concat([c1.update(wk), c1.final()]);
|
|
360
|
+
var iv2 = pass1.subarray(pass1.length - 16);
|
|
361
|
+
var c2 = nodeCrypto.createCipheriv("aes-256-cbc", kek, iv2); c2.setAutoPadding(false);
|
|
362
|
+
return Buffer.concat([c2.update(pass1), c2.final()]);
|
|
363
|
+
} finally {
|
|
364
|
+
guard.secret.zeroize(wk, CmsError, "cms/bad-input", "the PWRI plaintext block");
|
|
365
|
+
}
|
|
331
366
|
}
|
|
332
367
|
|
|
333
368
|
var PRF_HASH = { hmacWithSHA1: "SHA-1", hmacWithSHA256: "SHA-256", hmacWithSHA384: "SHA-384", hmacWithSHA512: "SHA-512" };
|
|
@@ -376,16 +411,23 @@ async function encrypt(content, recipients, opts) {
|
|
|
376
411
|
var contentType = opts.contentType || "data";
|
|
377
412
|
var cek = nodeCrypto.randomBytes(ca.keyBits / 8);
|
|
378
413
|
|
|
379
|
-
//
|
|
380
|
-
|
|
414
|
+
// The CEK protects the content for EVERY recipient, so unlike a per-recipient shared secret it is
|
|
415
|
+
// cleared ONCE, after the last recipient entry is built and the content is encrypted -- wiping it
|
|
416
|
+
// inside the recipient loop would destroy the key the remaining recipients must be given.
|
|
417
|
+
try {
|
|
418
|
+
// EncryptedData: a single non-array { cek } or { password } descriptor, no RecipientInfos.
|
|
419
|
+
if (!Array.isArray(recipients)) return _encryptedData(contentBytes, recipients, ca, contentType, opts, cek);
|
|
381
420
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
421
|
+
if (!recipients.length) throw _err("cms/bad-input", "at least one recipient is required (RFC 5652 sec. 6.1)");
|
|
422
|
+
var recips = [];
|
|
423
|
+
for (var i = 0; i < recipients.length; i++) recips.push(await _buildRecipient(cek, recipients[i], opts));
|
|
424
|
+
var riNodes = recips.map(_taggedRecipient);
|
|
386
425
|
|
|
387
|
-
|
|
388
|
-
|
|
426
|
+
if (ca.aead) return _emit(_authEnvelopedData(contentBytes, cek, ca, contentType, opts, riNodes, recips), "authEnvelopedData", opts);
|
|
427
|
+
return _emit(_envelopedData(contentBytes, cek, ca, contentType, riNodes, recips), "envelopedData", opts);
|
|
428
|
+
} finally {
|
|
429
|
+
guard.secret.zeroize(cek, CmsError, "cms/bad-input", "the content-encryption key");
|
|
430
|
+
}
|
|
389
431
|
}
|
|
390
432
|
|
|
391
433
|
function _emit(inner, ctName, opts) {
|
|
@@ -445,16 +487,27 @@ function _encryptedData(contentBytes, desc, ca, contentType, opts, cek) {
|
|
|
445
487
|
}
|
|
446
488
|
|
|
447
489
|
function _encryptedDataPbes2(contentBytes, desc, ca, contentType, iv, opts) {
|
|
448
|
-
|
|
490
|
+
// A string / Uint8Array password is encoded into a buffer THIS toolkit allocated -- a credential
|
|
491
|
+
// copy -- so it is cleared once the derivation has consumed it. A caller-supplied Buffer is
|
|
492
|
+
// borrowed and left intact.
|
|
493
|
+
var pwOwn2 = pbes2.passwordBytesOwned(desc.password, _err, "cms");
|
|
494
|
+
var password = pwOwn2.bytes;
|
|
449
495
|
var iterations = pbes2.assertIterations(desc.iterations == null ? 600000 : desc.iterations, _err, "cms");
|
|
450
496
|
var salt = desc.salt ? pbes2.assertSalt(guard.bytes.view(desc.salt, CmsError, "cms/bad-input", "salt"), _err, "cms") : nodeCrypto.randomBytes(16);
|
|
451
497
|
var prf = desc.prf || "hmacWithSHA256";
|
|
498
|
+
// The password-derived content key is this function's allocation; `password` may be the caller's
|
|
499
|
+
// own buffer (passwordBytes passes a Buffer straight through) and is left alone.
|
|
452
500
|
var key = nodeCrypto.pbkdf2Sync(password, salt, iterations, ca.keyBits / 8, pbes2.prfNodeByName(prf, _err, "cms"));
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
501
|
+
if (pwOwn2.owned) guard.secret.zeroize(password, CmsError, "cms/bad-input", "the password encoding");
|
|
502
|
+
try {
|
|
503
|
+
var enc = pbes2.cbcEncrypt(key, iv, contentBytes, ca.keyBits);
|
|
504
|
+
var contentAlg = pbes2.pbes2AlgId(salt, iterations, prf, ca.oid, iv);
|
|
505
|
+
var eci = b.sequence([b.oid(O(contentType)), contentAlg, b.contextPrimitive(0, enc)]);
|
|
506
|
+
var inner = b.sequence([b.integer(0n), eci]);
|
|
507
|
+
return _emit(inner, "encryptedData", { pem: opts && opts.pem != null ? opts.pem : desc.pem });
|
|
508
|
+
} finally {
|
|
509
|
+
guard.secret.zeroize(key, CmsError, "cms/bad-input", "the password-derived content-encryption key");
|
|
510
|
+
}
|
|
458
511
|
}
|
|
459
512
|
|
|
460
513
|
// ---- content-encryption primitives ----------------------------------------
|
|
@@ -505,65 +558,71 @@ async function authenticate(content, recipients, opts) {
|
|
|
505
558
|
// RFC 5652 sec. 9.1: authAttrs MUST be present when the eContentType is not id-data.
|
|
506
559
|
if (contentType !== "data" && !withAttrs) throw _err("cms/bad-input", "AuthenticatedData with a non-data contentType requires authenticated attributes (RFC 5652 sec. 9.1)");
|
|
507
560
|
|
|
561
|
+
// The MAC key is this path's content-encryption key: every recipient is given it, so it is cleared
|
|
562
|
+
// ONCE at the end rather than per recipient, and only after the MAC has been computed over it.
|
|
508
563
|
var macKey = nodeCrypto.randomBytes(MAC_KEY_OCTETS);
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
if (
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
node.
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
var hmacKey = await subtle.importKey("raw", macKey, { name: "HMAC", hash: mac.wc }, false, ["sign"]);
|
|
552
|
-
var macValue = Buffer.from(await subtle.sign({ name: "HMAC" }, hmacKey, preimage));
|
|
564
|
+
try {
|
|
565
|
+
var recips = [];
|
|
566
|
+
for (var i = 0; i < recipients.length; i++) recips.push(await _buildRecipient(macKey, recipients[i], opts));
|
|
567
|
+
var riNodes = recips.map(_taggedRecipient);
|
|
568
|
+
|
|
569
|
+
var digestName = opts.digestAlgorithm || mac.node;
|
|
570
|
+
var digestAlgTagged = null, authAttrsDer = null, preimage;
|
|
571
|
+
if (withAttrs) {
|
|
572
|
+
if (!SUPPORTED_DIGEST[digestName]) throw _err("cms/bad-input", "unsupported digestAlgorithm " + JSON.stringify(digestName) + " (sha256/384/512)");
|
|
573
|
+
// sec. 9.2: content-type (== eContentType) + message-digest (== digest(content)) attributes,
|
|
574
|
+
// SET-OF-sorted, MACed under the EXPLICIT SET OF tag (0x31) but transmitted [2] IMPLICIT (0xA2).
|
|
575
|
+
var mdDigest = nodeCrypto.createHash(digestName).update(contentBytes).digest();
|
|
576
|
+
var pairs = [
|
|
577
|
+
b.sequence([b.oid(O("contentType")), b.setOf([b.oid(O(contentType))])]),
|
|
578
|
+
b.sequence([b.oid(O("messageDigest")), b.setOf([b.octetString(mdDigest)])]),
|
|
579
|
+
];
|
|
580
|
+
if (opts.authAttrs && opts.authAttrs.length) pairs = pairs.concat(opts.authAttrs);
|
|
581
|
+
// Every authAttr (auto-built or caller-supplied) MUST be a well-formed Attribute SEQUENCE
|
|
582
|
+
// { type OBJECT IDENTIFIER, values SET OF } and each type appears at most once (RFC 5652) -- so a
|
|
583
|
+
// malformed or duplicate caller attribute is rejected BEFORE it is MACed and emitted, never left to
|
|
584
|
+
// fail an operator's parser downstream.
|
|
585
|
+
var seenTypes = {};
|
|
586
|
+
pairs.forEach(function (p) {
|
|
587
|
+
var node;
|
|
588
|
+
try { node = asn1.decode(p); } catch (e) { throw _err("cms/bad-input", "an authenticated attribute is not well-formed DER", e); }
|
|
589
|
+
if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children || node.children.length !== 2 ||
|
|
590
|
+
node.children[1].tagClass !== "universal" || node.children[1].tagNumber !== asn1.TAGS.SET ||
|
|
591
|
+
!node.children[1].children || node.children[1].children.length < 1) {
|
|
592
|
+
throw _err("cms/bad-input", "an authenticated attribute must be an Attribute SEQUENCE { type, non-empty SET OF value } (RFC 5652)");
|
|
593
|
+
}
|
|
594
|
+
var t;
|
|
595
|
+
try { t = asn1.read.oid(node.children[0]); } catch (e) { throw _err("cms/bad-input", "an authenticated attribute type is not an OBJECT IDENTIFIER", e); }
|
|
596
|
+
if (seenTypes[t]) throw _err("cms/bad-input", "authenticated attributes must not repeat an attribute type (RFC 5652): " + t);
|
|
597
|
+
seenTypes[t] = 1;
|
|
598
|
+
});
|
|
599
|
+
var setOf = b.setOf(pairs);
|
|
600
|
+
preimage = setOf; // MAC over the 0x31 SET OF
|
|
601
|
+
authAttrsDer = b.contextConstructed(2, setOf.subarray(_tlvHeaderLen(setOf))); // [2] IMPLICIT on the wire
|
|
602
|
+
digestAlgTagged = b.contextConstructed(1, b.oid(O(digestName))); // [1] IMPLICIT DigestAlgorithmIdentifier
|
|
603
|
+
} else {
|
|
604
|
+
preimage = contentBytes; // MAC over the eContent value octets
|
|
605
|
+
}
|
|
553
606
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
607
|
+
var hmacKey = await subtle.importKey("raw", macKey, { name: "HMAC", hash: mac.wc }, false, ["sign"]);
|
|
608
|
+
var macValue = Buffer.from(await subtle.sign({ name: "HMAC" }, hmacKey, preimage));
|
|
609
|
+
|
|
610
|
+
var eci = b.sequence([b.oid(O(contentType)), b.explicit(0, b.octetString(contentBytes))]);
|
|
611
|
+
var kids = [b.integer(0n), b.setOf(riNodes), _algId(mac.oid)];
|
|
612
|
+
if (digestAlgTagged) kids.push(digestAlgTagged);
|
|
613
|
+
kids.push(eci);
|
|
614
|
+
if (authAttrsDer) kids.push(authAttrsDer);
|
|
615
|
+
kids.push(b.octetString(macValue));
|
|
616
|
+
var ci = b.sequence([b.oid(O("authData")), b.explicit(0, b.sequence(kids))]);
|
|
617
|
+
// Self-verify: the emitted AuthenticatedData MUST re-parse clean through the strict parser, so a
|
|
618
|
+
// caller-supplied authenticated attribute with a recognized type but extra RFC 5652 constraints (a
|
|
619
|
+
// misplaced type, a signing-time whose value is not a Time) is rejected HERE at build time rather
|
|
620
|
+
// than by the recipient's parser after the fact.
|
|
621
|
+
try { schemaCms.parse(ci); } catch (e) { throw (e instanceof CmsError) ? e : _err("cms/bad-input", "the supplied authenticated attributes produced an invalid AuthenticatedData", e); }
|
|
622
|
+
return opts.pem ? schemaCms.pemEncode(ci, "CMS") : ci;
|
|
623
|
+
} finally {
|
|
624
|
+
guard.secret.zeroize(macKey, CmsError, "cms/bad-input", "the message-authentication key");
|
|
625
|
+
}
|
|
567
626
|
}
|
|
568
627
|
|
|
569
628
|
module.exports = { encrypt: encrypt, authenticate: authenticate };
|
package/lib/hpke.js
CHANGED
|
@@ -94,13 +94,29 @@ function _expand(hash, Nh, prk, info, len) {
|
|
|
94
94
|
if (len > 255 * Nh) throw _err("hpke/export-length", "requested length " + len + " exceeds 255*Nh");
|
|
95
95
|
var out = [], t = Buffer.alloc(0), n = Math.ceil(len / Nh);
|
|
96
96
|
for (var i = 1; i <= n; i++) {
|
|
97
|
-
|
|
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 = {
|