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