@blamejs/pki 0.6.14 → 0.6.16
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 +27 -1
- package/lib/acme.js +59 -815
- package/lib/asn1-der.js +7 -245
- package/lib/attrcert-sign.js +12 -260
- package/lib/attrcert-verify.js +1 -7
- package/lib/byte-reader.js +0 -8
- package/lib/byte-writer.js +0 -11
- package/lib/cbor-det.js +0 -54
- package/lib/cmc-build.js +15 -357
- package/lib/cmc-verify.js +6 -239
- package/lib/cmp-build.js +41 -317
- package/lib/cmp-session.js +73 -760
- package/lib/cmp-verify.js +32 -488
- package/lib/cms-compress.js +3 -32
- package/lib/cms-decrypt.js +29 -294
- package/lib/cms-encrypt.js +14 -234
- package/lib/cms-sign.js +29 -324
- package/lib/cms-verify.js +9 -500
- package/lib/composite-kem.js +2 -102
- package/lib/composite-sig.js +1 -77
- package/lib/constants.js +0 -249
- package/lib/crl-sign.js +9 -237
- package/lib/crl-verify.js +1 -10
- package/lib/crmf-sign.js +19 -198
- package/lib/crmf-verify.js +1 -12
- package/lib/csr-sign.js +3 -34
- package/lib/csr-verify.js +1 -12
- package/lib/ct.js +28 -253
- package/lib/edwards-point.js +10 -44
- package/lib/est.js +28 -764
- package/lib/framework-error.js +4 -182
- package/lib/guard-all.js +1 -58
- package/lib/guard-async.js +1 -36
- package/lib/guard-bytes.js +6 -650
- package/lib/guard-compress.js +5 -102
- package/lib/guard-crypto.js +1 -40
- package/lib/guard-der.js +2 -93
- package/lib/guard-encoding.js +2 -60
- package/lib/guard-header.js +1 -36
- package/lib/guard-identifier.js +7 -531
- package/lib/guard-intrinsic.js +1 -150
- package/lib/guard-json.js +10 -73
- package/lib/guard-limits.js +1 -77
- package/lib/guard-list.js +1 -49
- package/lib/guard-name.js +3 -209
- package/lib/guard-parsed.js +7 -335
- package/lib/guard-range.js +1 -71
- package/lib/guard-secret.js +1 -102
- package/lib/guard-text.js +1 -64
- package/lib/guard-time.js +1 -97
- package/lib/hpke.js +3 -105
- package/lib/http-digest.js +14 -142
- package/lib/http-retry-after.js +1 -47
- package/lib/http-transport.js +21 -131
- package/lib/inspect.js +30 -284
- package/lib/ip-utils.js +6 -47
- package/lib/jose.js +7 -142
- package/lib/key.js +2 -67
- package/lib/lint.js +9 -194
- package/lib/merkle.js +1 -27
- package/lib/mime.js +9 -82
- package/lib/ocsp-request-verify.js +1 -14
- package/lib/ocsp-verify.js +1 -82
- package/lib/ocsp.js +11 -169
- package/lib/oid.js +5 -337
- package/lib/path-validate.js +103 -1492
- package/lib/pbes2.js +2 -89
- package/lib/pkcs12-build.js +31 -379
- package/lib/pki-build.js +64 -290
- package/lib/rc2.js +2 -18
- package/lib/rfc3339.js +4 -27
- package/lib/scep.js +140 -286
- package/lib/schema-all.js +0 -122
- package/lib/schema-attrcert.js +4 -146
- package/lib/schema-c509.js +153 -802
- package/lib/schema-cmc.js +5 -220
- package/lib/schema-cmp.js +2 -223
- package/lib/schema-cms.js +13 -427
- package/lib/schema-crl.js +5 -68
- package/lib/schema-crmf.js +6 -167
- package/lib/schema-csr.js +1 -54
- package/lib/schema-csrattrs.js +2 -87
- package/lib/schema-engine.js +0 -152
- package/lib/schema-ocsp.js +0 -117
- package/lib/schema-pkcs12.js +2 -93
- package/lib/schema-pkcs8.js +3 -35
- package/lib/schema-pkix.js +24 -655
- package/lib/schema-smime.js +0 -54
- package/lib/schema-tsp.js +3 -99
- package/lib/schema-x509.js +4 -63
- package/lib/shbs.js +11 -95
- package/lib/sign-scheme.js +2 -97
- package/lib/sigstore.js +9 -169
- package/lib/sleep.js +2 -8
- package/lib/smime.js +15 -408
- package/lib/tls-cert-compress.js +0 -62
- package/lib/trust.js +19 -185
- package/lib/tsp-sign.js +16 -270
- package/lib/validator-all.js +1 -30
- package/lib/validator-attcert.js +2 -46
- package/lib/validator-cose.js +2 -153
- package/lib/validator-keydesc.js +1 -37
- package/lib/validator-sig.js +1 -46
- package/lib/validator-tls.js +1 -88
- package/lib/validator-tpm.js +19 -122
- package/lib/verify-seam.js +1 -24
- package/lib/webauthn-mds.js +3 -400
- package/lib/webauthn.js +12 -833
- package/lib/webcrypto.js +5 -485
- package/lib/x509-sign.js +4 -108
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/cms-decrypt.js
CHANGED
|
@@ -1,18 +1,8 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright (c) blamejs contributors
|
|
3
3
|
"use strict";
|
|
4
|
-
//
|
|
5
|
-
// @internal -- the pki.cms.decrypt implementation. The operator-facing @module pki.cms + the
|
|
4
|
+
// @internal
|
|
6
5
|
// @primitive pki.cms.decrypt block live in cms-verify.js, which re-exports this function.
|
|
7
|
-
//
|
|
8
|
-
// CMS EnvelopedData / AuthEnvelopedData / EncryptedData decryption (RFC 5652/5083/5084/3560/5753/
|
|
9
|
-
// 8418/9629/9936/3211/8018), the consuming side of pki.cms.encrypt. Three stages behind one
|
|
10
|
-
// oracle-free choke point: (1) select the recipient (identifier matching, typed + distinct),
|
|
11
|
-
// (2) acquire the CEK (per-arm unwrap/decap/derive), (3) open the content (AEAD verify / CBC
|
|
12
|
-
// decrypt+unpad). Every secret-dependent failure in stages 2-3 collapses to the single uniform
|
|
13
|
-
// verdict `cms/decrypt-failed` (Bleichenbacher / EFAIL / PWRI-check-byte oracle freedom); the
|
|
14
|
-
// ktri v1.5 arm substitutes a random CEK on any decode fault (RFC 3218 implicit rejection) so its
|
|
15
|
-
// failure emerges at stage 3 identically to every other bad-key path.
|
|
16
6
|
|
|
17
7
|
var nodeCrypto = require("crypto");
|
|
18
8
|
var asn1 = require("./asn1-der");
|
|
@@ -31,20 +21,15 @@ var subtle = webcrypto.webcrypto.subtle;
|
|
|
31
21
|
var CmsError = frameworkError.CmsError;
|
|
32
22
|
var WRAP_KEK_LENGTHS = schemaCms.WRAP_KEK_LENGTHS;
|
|
33
23
|
var KEM_CT_LENGTHS = schemaCms.KEM_CT_LENGTHS;
|
|
34
|
-
// The parser's own AEAD table: the single place that decides whether a content-encryption OID names
|
|
35
|
-
// an authenticated encryption algorithm, so the container-vs-mode rule below cannot drift from the
|
|
36
|
-
// parameter validation that accepted the message.
|
|
37
24
|
var AEAD_ALGS = schemaCms.AEAD_ALGS;
|
|
38
25
|
|
|
39
26
|
function O(n) { return oid.byName(n); }
|
|
40
27
|
function _err(code, message, cause) { return new CmsError(code, message, cause); }
|
|
41
|
-
// The single uniform secret-dependent-failure verdict. No cause chaining that distinguishes the site.
|
|
42
28
|
function _fail() { return new CmsError("cms/decrypt-failed", "the CMS content could not be decrypted (uniform by design -- padding / integrity / key-unwrap failures are indistinguishable to defeat oracles)"); }
|
|
43
29
|
|
|
44
|
-
var CONTENT_KEYBITS = pbes2.CONTENT_KEYBITS;
|
|
45
|
-
var CONTENT_MODE = pbes2.CONTENT_MODE;
|
|
30
|
+
var CONTENT_KEYBITS = pbes2.CONTENT_KEYBITS;
|
|
31
|
+
var CONTENT_MODE = pbes2.CONTENT_MODE;
|
|
46
32
|
|
|
47
|
-
// ---- entry -----------------------------------------------------------------
|
|
48
33
|
async function decrypt(input, keyMaterial, opts) {
|
|
49
34
|
opts = opts || {};
|
|
50
35
|
if (keyMaterial == null || typeof keyMaterial !== "object") throw _err("cms/bad-input", "decrypt requires a key-material object");
|
|
@@ -56,24 +41,7 @@ async function decrypt(input, keyMaterial, opts) {
|
|
|
56
41
|
return decryptEnvelopedData(parsed, keyMaterial, opts, ct);
|
|
57
42
|
}
|
|
58
43
|
|
|
59
|
-
//
|
|
60
|
-
// the CEK, open the content, oracle-free (every secret-dependent failure collapses to cms/decrypt-failed).
|
|
61
|
-
// @internal -- not re-exported on pki.cms. Taking the parsed structure in place of DER is a deliberate seam
|
|
62
|
-
// for a sibling composer that already holds a strictly-walked content node it must not re-serialize/re-parse:
|
|
63
|
-
// pki.pkcs12.open drives this off the pkcs12 parser's walkEnvelopedData result so an RFC 7292 privacy safe
|
|
64
|
-
// whose EnvelopedData is BER-encoded (indefinite lengths, some NSS output the pkcs12 parser accepts)
|
|
65
|
-
// opens without hitting the public decrypt()'s strict-DER re-parse. Callers pass a trusted parse result only.
|
|
66
|
-
// `authenticated` answers a question about the content and the key that opened it. It
|
|
67
|
-
// cannot answer the question an operator hears in the word -- who sent this -- and no
|
|
68
|
-
// recipient type this toolkit supports authenticates an individual originator: a ktri or
|
|
69
|
-
// ephemeral-static kari message is minted by anyone holding the recipient's PUBLIC key,
|
|
70
|
-
// and a password or KEK message is minted by any co-recipient sharing the secret. The
|
|
71
|
-
// origin question therefore gets its own field instead of being left to the reader's
|
|
72
|
-
// inference. originatorInfo rides outside the AEAD's authenticated data, so it is a hint
|
|
73
|
-
// the sender chose, never evidence, and it is surfaced saying so rather than dropped.
|
|
74
|
-
// Every verdict this module returns carries all three, including the unauthenticated
|
|
75
|
-
// ciphers: a field present on some returns and absent on others is read as a capability
|
|
76
|
-
// difference, and the caller cannot tell "no origin" from "this path forgot to say".
|
|
44
|
+
// @internal
|
|
77
45
|
function _originFields(res, authenticatedBy, originatorInfo) {
|
|
78
46
|
res.authenticatedBy = authenticatedBy || null;
|
|
79
47
|
res.originAuthenticated = false;
|
|
@@ -86,31 +54,16 @@ async function decryptEnvelopedData(parsed, keyMaterial, opts, contentTypeName)
|
|
|
86
54
|
if (keyMaterial == null || typeof keyMaterial !== "object") throw _err("cms/bad-input", "decrypt requires a key-material object");
|
|
87
55
|
var ct = contentTypeName || "envelopedData";
|
|
88
56
|
var recips = parsed.recipientInfos || [];
|
|
89
|
-
var candidates = _selectCandidates(recips, keyMaterial, opts);
|
|
57
|
+
var candidates = _selectCandidates(recips, keyMaterial, opts);
|
|
90
58
|
var eci = parsed.encryptedContentInfo;
|
|
91
|
-
// The content cipher's mode is a structural property of the message, readable from the algorithm
|
|
92
|
-
// identifier with no key material at all, so it is decided here, before any recipient is tried.
|
|
93
|
-
// Deciding it per candidate would make a structural verdict depend on key acquisition succeeding
|
|
94
|
-
// first: several password or KEK recipients can match the same key material, so the distinct
|
|
95
|
-
// cms/unsupported-algorithm would be swallowed by the ambiguous-candidate loop below and the caller
|
|
96
|
-
// told their key was wrong, after repeating a PBKDF2 derivation for every candidate to learn it.
|
|
97
59
|
_assertContentCipherMode(eci, ct);
|
|
98
|
-
// Password- and KEK-based recipients carry no rid, so several may match the same key material; try
|
|
99
|
-
// each until one yields a CEK that opens the content. A single candidate propagates its exact
|
|
100
|
-
// verdict; multiple ambiguous candidates collapse to the uniform verdict so nothing is leaked.
|
|
101
60
|
for (var ci = 0; ci < candidates.length; ci++) {
|
|
102
61
|
try {
|
|
103
|
-
_assertSupported(candidates[ci].ri, keyMaterial);
|
|
104
|
-
var cek = await _acquireCek(candidates[ci].ri, keyMaterial, opts);
|
|
105
|
-
// The recovered CEK is cleared only after the content is open: it is the key the next stage
|
|
106
|
-
// needs, so an earlier wipe would break decryption instead of protecting it. The returned object
|
|
107
|
-
// is built before the `finally` runs and holds the plaintext, not the key.
|
|
62
|
+
_assertSupported(candidates[ci].ri, keyMaterial);
|
|
63
|
+
var cek = await _acquireCek(candidates[ci].ri, keyMaterial, opts);
|
|
108
64
|
try {
|
|
109
|
-
// Lazy CBC streaming (the memory win) is safe only with one candidate; several ambiguous
|
|
110
|
-
// candidates need the content decrypt to run eagerly so a wrong recipient is rejected and the
|
|
111
|
-
// next tried, exactly as the buffered path disambiguates.
|
|
112
65
|
var streamMode = opts.stream === true ? (candidates.length > 1 ? "eager" : "lazy") : null;
|
|
113
|
-
var content = await _openContent(parsed, eci, cek, ct, streamMode);
|
|
66
|
+
var content = await _openContent(parsed, eci, cek, ct, streamMode);
|
|
114
67
|
return _originFields({
|
|
115
68
|
content: content,
|
|
116
69
|
contentType: eci.contentType, contentTypeName: oid.name(eci.contentType) || eci.contentType,
|
|
@@ -122,15 +75,13 @@ async function decryptEnvelopedData(parsed, keyMaterial, opts, contentTypeName)
|
|
|
122
75
|
guard.secret.zeroize(cek, CmsError, "cms/bad-input", "the recovered content-encryption key");
|
|
123
76
|
}
|
|
124
77
|
} catch (e) {
|
|
125
|
-
if (candidates.length === 1) throw e;
|
|
78
|
+
if (candidates.length === 1) throw e;
|
|
126
79
|
}
|
|
127
80
|
}
|
|
128
|
-
throw _fail();
|
|
81
|
+
throw _fail();
|
|
129
82
|
}
|
|
130
83
|
|
|
131
84
|
function _parse(input) {
|
|
132
|
-
// Always drive the strict parser over caller-supplied bytes: an arbitrary object that merely
|
|
133
|
-
// carries a contentTypeName is not a trusted parse result and must not bypass DER validation.
|
|
134
85
|
return schemaCms.parse(_toDer(input));
|
|
135
86
|
}
|
|
136
87
|
function _toDer(input) {
|
|
@@ -139,9 +90,6 @@ function _toDer(input) {
|
|
|
139
90
|
throw _err("cms/bad-input", "input must be a DER Buffer, Uint8Array, or PEM string");
|
|
140
91
|
}
|
|
141
92
|
|
|
142
|
-
// ---- stage 1: recipient selection (not secret-dependent) -------------------
|
|
143
|
-
// Returns every recipient the key material may target. An explicit recipientIndex or an rid match
|
|
144
|
-
// yields exactly one; unlabeled pwri / kekri recipients yield all matches for the caller to try.
|
|
145
93
|
function _selectCandidates(recips, km, opts) {
|
|
146
94
|
if (!recips.length) throw _err("cms/no-matching-recipient", "the message carries no RecipientInfos");
|
|
147
95
|
if (opts.recipientIndex != null) {
|
|
@@ -155,7 +103,7 @@ function _selectCandidates(recips, km, opts) {
|
|
|
155
103
|
for (var k = 0; k < recips.length; k++) {
|
|
156
104
|
var r = recips[k];
|
|
157
105
|
if (want === "asym" && (r.type === "ktri" || r.type === "kari" || r.type === "ori")) {
|
|
158
|
-
if (cert && _ridMatches(r, cert)) out.push({ ri: r, index: k });
|
|
106
|
+
if (cert && _ridMatches(r, cert)) out.push({ ri: r, index: k });
|
|
159
107
|
} else if (want === "pwri" && r.type === "pwri") { out.push({ ri: r, index: k }); }
|
|
160
108
|
else if (want === "kekri" && r.type === "kekri") {
|
|
161
109
|
if (km.kekId == null || _kekIdMatches(r, km.kekId)) out.push({ ri: r, index: k });
|
|
@@ -171,7 +119,7 @@ function _riKindForKey(km) {
|
|
|
171
119
|
throw _err("cms/bad-input", "key material needs { key, cert }, { password }, { kek }, or { cek }");
|
|
172
120
|
}
|
|
173
121
|
function _assertSupported(ri, km) {
|
|
174
|
-
if (ri.type === "kari" && ri.keyEncryptionAlgorithm && intrinsic.stringIndexOf(intrinsic.toLowerCase(ri.keyEncryptionAlgorithm.name || ""), "mqv") !== -1) throw _err("cms/unsupported-algorithm", "ECMQV kari is not supported");
|
|
122
|
+
if (ri.type === "kari" && ri.keyEncryptionAlgorithm && intrinsic.stringIndexOf(intrinsic.toLowerCase(ri.keyEncryptionAlgorithm.name || ""), "mqv") !== -1) throw _err("cms/unsupported-algorithm", "ECMQV kari is not supported");
|
|
175
123
|
if (ri.type === "ori") {
|
|
176
124
|
if (ri.oriType !== O("kem")) throw _err("cms/unsupported-recipient-type", "unsupported OtherRecipientInfo type " + ri.oriType);
|
|
177
125
|
if (ri.kemri && ri.kemri.kem && ri.kemri.kem.oid === O("id-kem-rsa")) throw _err("cms/unsupported-algorithm", "RSA-KEM is not supported");
|
|
@@ -180,8 +128,6 @@ function _assertSupported(ri, km) {
|
|
|
180
128
|
}
|
|
181
129
|
|
|
182
130
|
function _ridMatches(ri, cert) {
|
|
183
|
-
// A kari carries a RecipientEncryptedKey per agreeing recipient (RFC 5652 sec. 6.2.2); this
|
|
184
|
-
// recipient may be at ANY position, so match against every rek, not just element 0.
|
|
185
131
|
if (ri.type === "kari") return !!_kariRekFor(ri, cert);
|
|
186
132
|
var rid = ri.rid || (ri.kemri && ri.kemri.rid);
|
|
187
133
|
if (!rid) return false;
|
|
@@ -198,7 +144,7 @@ function _ridEq(rid, cert) {
|
|
|
198
144
|
catch (_e) { return false; }
|
|
199
145
|
}
|
|
200
146
|
if (rid.subjectKeyIdentifier) { var ski = _skiOf(cert); return !!ski && Buffer.compare(ski, rid.subjectKeyIdentifier) === 0; }
|
|
201
|
-
return false;
|
|
147
|
+
return false;
|
|
202
148
|
}
|
|
203
149
|
function _skiOf(cert) {
|
|
204
150
|
var exts = cert.extensions || [];
|
|
@@ -213,11 +159,8 @@ function _kekIdMatches(ri, kekId) {
|
|
|
213
159
|
return Buffer.compare(id, guard.bytes.view(kekId, CmsError, "cms/bad-input", "kekId")) === 0;
|
|
214
160
|
}
|
|
215
161
|
|
|
216
|
-
// Codes that name a structural / resource / config fault and MUST NOT be masked by the uniform
|
|
217
|
-
// secret-dependent verdict (they leak nothing about the key or plaintext).
|
|
218
162
|
var _passThrough = { "cms/unsupported-algorithm": 1, "cms/unsupported-recipient-type": 1, "cms/bad-input": 1, "cms/iteration-limit": 1, "cms/missing-key-derivation": 1, "cms/no-encrypted-content": 1 };
|
|
219
163
|
|
|
220
|
-
// ---- stage 2: acquire the CEK (uniform failure) ----------------------------
|
|
221
164
|
async function _acquireCek(ri, km, opts) {
|
|
222
165
|
try {
|
|
223
166
|
if (ri.type === "ktri") return await _ktriCek(ri, km);
|
|
@@ -226,21 +169,15 @@ async function _acquireCek(ri, km, opts) {
|
|
|
226
169
|
if (ri.type === "pwri") return await _pwriCek(ri, km, opts);
|
|
227
170
|
if (ri.type === "ori") return await _kemriCek(ri, km);
|
|
228
171
|
} catch (e) {
|
|
229
|
-
// Structural / resource / config faults keep their own code; only secret-dependent failures
|
|
230
|
-
// (a bad unwrap, a wrong key, a padding fault) collapse to the uniform verdict.
|
|
231
172
|
if (e instanceof CmsError && _passThrough[e.code]) throw e;
|
|
232
173
|
throw _fail();
|
|
233
174
|
}
|
|
234
|
-
// Coverage residual: unreachable, since _selectCandidates only returns one of the five handled types.
|
|
235
175
|
throw _err("cms/unsupported-recipient-type", "unsupported recipient type " + ri.type);
|
|
236
176
|
}
|
|
237
177
|
|
|
238
|
-
// ktri: OAEP or PKCS#1 v1.5 (v1.5 = decrypt-only + RFC 3218 implicit rejection).
|
|
239
178
|
async function _ktriCek(ri, km) {
|
|
240
179
|
var kea = ri.keyEncryptionAlgorithm;
|
|
241
180
|
var k = _normKeyDer(km.key);
|
|
242
|
-
// The wipe is in a `finally` so it happens on the reject path too: a message crafted to fail the
|
|
243
|
-
// unwrap must not be a way to leave the key copy in memory.
|
|
244
181
|
try {
|
|
245
182
|
if (kea.oid === O("rsaesOaep")) {
|
|
246
183
|
var hash = _oaepHashFromParams(kea.parameters);
|
|
@@ -248,32 +185,20 @@ async function _ktriCek(ri, km) {
|
|
|
248
185
|
return Buffer.from(await subtle.decrypt({ name: "RSA-OAEP" }, pub, ri.encryptedKey));
|
|
249
186
|
}
|
|
250
187
|
if (kea.oid === O("rsaEncryption")) {
|
|
251
|
-
// RFC 3218 sec. 2.3.2 implicit rejection: NEVER surface a v1.5 failure here. Any decode fault
|
|
252
|
-
// yields a fresh random CEK of the content-alg length; the mismatch emerges at stage 3.
|
|
253
188
|
var keyObj = nodeCrypto.createPrivateKey({ key: k.der, format: "der", type: "pkcs8" });
|
|
254
189
|
try { return nodeCrypto.privateDecrypt({ key: keyObj, padding: nodeCrypto.constants.RSA_PKCS1_PADDING }, ri.encryptedKey); }
|
|
255
190
|
catch (_e) {
|
|
256
|
-
return null;
|
|
191
|
+
return null;
|
|
257
192
|
}
|
|
258
193
|
}
|
|
259
|
-
// Coverage residual: reachable only from a hostile message (our encrypt emits only OAEP; OpenSSL
|
|
260
|
-
// emits OAEP or rsaEncryption): a fail-closed reject the fuzz harness exercises.
|
|
261
194
|
throw _err("cms/unsupported-algorithm", "unsupported ktri keyEncryptionAlgorithm " + kea.oid);
|
|
262
195
|
} finally {
|
|
263
196
|
_releaseKeyDer(k);
|
|
264
197
|
}
|
|
265
198
|
}
|
|
266
199
|
|
|
267
|
-
// kari: reconstruct Z from the originatorKey + recipient private key, KDF -> KEK, AES-KW unwrap.
|
|
268
200
|
async function _kariCek(ri, km) {
|
|
269
|
-
// The copy is taken and the protected region opens IMMEDIATELY. Everything between them would
|
|
270
|
-
// otherwise be an unprotected window, and it is not a narrow one: reading the wrap algorithm, the
|
|
271
|
-
// originator's key, the recipient's certificate and the matching RecipientEncryptedKey all parse
|
|
272
|
-
// attacker-supplied structure and all throw on malformed input. A message crafted to fail any one
|
|
273
|
-
// of them would be a way to leave the key copy in the heap, the exact outcome the wipe is for.
|
|
274
201
|
var k = _normKeyDer(km.key);
|
|
275
|
-
// The agreement secret and the KEK derived from it are both allocated here; one `finally` clears
|
|
276
|
-
// whichever branch produced them, including when the unwrap below throws on a tampered key.
|
|
277
202
|
var kek, z, mz;
|
|
278
203
|
try {
|
|
279
204
|
var keyDer = k.der;
|
|
@@ -281,8 +206,6 @@ async function _kariCek(ri, km) {
|
|
|
281
206
|
var wrapAlg = _kariWrap(kea);
|
|
282
207
|
var scheme = kea.oid;
|
|
283
208
|
var origSpki = _originatorSpki(ri.originator);
|
|
284
|
-
// Unwrap this recipient's own RecipientEncryptedKey (matched by rid), not element 0; a kari may list
|
|
285
|
-
// several recipients under one ephemeral key.
|
|
286
209
|
var rek = (km.cert != null && _kariRekFor(ri, x509.parse(_normCertDer(km.cert)))) || ri.recipientEncryptedKeys[0];
|
|
287
210
|
var kekBytes = WRAP_KEK_LENGTHS[wrapAlg.oid];
|
|
288
211
|
if (!kekBytes) throw _err("cms/unsupported-algorithm", "unsupported kari key-wrap");
|
|
@@ -294,13 +217,8 @@ async function _kariCek(ri, km) {
|
|
|
294
217
|
mz = Buffer.from(await subtle.deriveBits({ name: mont.name, public: origPub }, recipPriv, null));
|
|
295
218
|
if (mz.every(function (x) { return x === 0; })) throw _fail();
|
|
296
219
|
var mzKey = await subtle.importKey("raw", mz, { name: "HKDF" }, false, ["deriveBits"]);
|
|
297
|
-
// RFC 8418 sec. 2.2: a present ukm serves as both the HKDF salt and the ECC-CMS-SharedInfo
|
|
298
|
-
// entityUInfo, so mirror the producer and both sides derive the same KEK as a conformant peer.
|
|
299
220
|
kek = Buffer.from(await subtle.deriveBits({ name: "HKDF", hash: mont.hkdf, salt: ukm || Buffer.alloc(0), info: _eccSharedInfo(wrapAlg.name, ukm, kekBytes) }, mzKey, kekBytes * 8));
|
|
300
221
|
} else {
|
|
301
|
-
// RFC 5753 sec. 7.1 permits the originator EC key to omit its curve parameters, inheriting the
|
|
302
|
-
// curve from the recipient's certificate; resolve the curve from the recipient (authoritative)
|
|
303
|
-
// and rebuild the originator SPKI with explicit parameters so importKey can consume it.
|
|
304
222
|
var origAlg = asn1.decode(origSpki).children[0];
|
|
305
223
|
var origHasParams = origAlg.children.length > 1;
|
|
306
224
|
var curveOid = (km.cert != null && _ecCurveFromCert(km.cert)) || (origHasParams ? asn1.read.oid(origAlg.children[1]) : null);
|
|
@@ -320,13 +238,11 @@ async function _kariCek(ri, km) {
|
|
|
320
238
|
}
|
|
321
239
|
}
|
|
322
240
|
|
|
323
|
-
// kekri: AES-KW unwrap under the caller-supplied KEK.
|
|
324
241
|
async function _kekriCek(ri, km) {
|
|
325
242
|
var kek = guard.bytes.view(km.kek, CmsError, "cms/bad-input", "kek");
|
|
326
243
|
return await _aesKwUnwrap(kek, ri.encryptedKey);
|
|
327
244
|
}
|
|
328
245
|
|
|
329
|
-
// pwri: PBKDF2 -> KEK, RFC 3211 double-CBC unwrap.
|
|
330
246
|
async function _pwriCek(ri, km, opts) {
|
|
331
247
|
var kdf = ri.keyDerivationAlgorithm;
|
|
332
248
|
if (!kdf) throw _err("cms/missing-key-derivation", "the pwri recipient has no keyDerivationAlgorithm (externally-supplied KEK is not supported)");
|
|
@@ -334,19 +250,11 @@ async function _pwriCek(ri, km, opts) {
|
|
|
334
250
|
var pb = pbes2.parsePbkdf2Params(kdf.parameters, opts, _err, "cms");
|
|
335
251
|
var kea = ri.keyEncryptionAlgorithm;
|
|
336
252
|
if (kea.oid !== O("id-alg-PWRI-KEK")) throw _err("cms/unsupported-algorithm", "unsupported pwri key-encryption " + kea.oid);
|
|
337
|
-
var inner = asn1.decode(kea.parameters);
|
|
253
|
+
var inner = asn1.decode(kea.parameters);
|
|
338
254
|
var innerOid = asn1.read.oid(inner.children[0]);
|
|
339
255
|
var innerBits = CONTENT_KEYBITS[innerOid];
|
|
340
|
-
// The mode comes from the OID-keyed table, not from the OID's display NAME: pki.oid.register() can
|
|
341
|
-
// override a built-in name, and a name-matched mode check would then admit a non-CBC inner cipher.
|
|
342
256
|
if (!innerBits || CONTENT_MODE[innerOid] !== "cbc") throw _err("cms/unsupported-algorithm", "unsupported pwri inner cipher");
|
|
343
257
|
var iv = asn1.read.octetString(inner.children[1]);
|
|
344
|
-
// The derived KEK is ours and is cleared once the unwrap has consumed it, on the failing path
|
|
345
|
-
// too, which is the one an attacker induces by tampering with the wrapped key. The caller's
|
|
346
|
-
// password is not touched: passwordBytes returns a caller-supplied Buffer as-is.
|
|
347
|
-
// A string / Uint8Array password is encoded into a buffer this toolkit allocated, a credential
|
|
348
|
-
// copy, so it is cleared once the derivation has consumed it. A caller-supplied Buffer is
|
|
349
|
-
// borrowed and left intact.
|
|
350
258
|
var pw = pbes2.passwordBytesOwned(km.password, _err, "cms");
|
|
351
259
|
var kek;
|
|
352
260
|
try { kek = nodeCrypto.pbkdf2Sync(pw.bytes, pb.salt, pb.iterations, innerBits / 8, pb.prfNode); }
|
|
@@ -358,34 +266,23 @@ async function _pwriCek(ri, km, opts) {
|
|
|
358
266
|
}
|
|
359
267
|
}
|
|
360
268
|
|
|
361
|
-
// kemri (ML-KEM ori): decapsulate -> ss, HKDF(CMSORIforKEMOtherInfo) -> KEK, AES-KW unwrap.
|
|
362
269
|
async function _kemriCek(ri, km) {
|
|
363
270
|
var k = ri.kemri;
|
|
364
271
|
var wcName = _mlkemName(k.kem.oid);
|
|
365
272
|
if (!wcName) throw _err("cms/unsupported-algorithm", "unsupported KEM " + k.kem.oid);
|
|
366
|
-
// Honor the declared key-derivation function: we implement HKDF-SHA256 (RFC 9629), so a message
|
|
367
|
-
// naming any other KDF is rejected, never silently derived with the wrong one.
|
|
368
273
|
if (k.kdf.oid !== O("hkdfWithSha256")) throw _err("cms/unsupported-algorithm", "unsupported KEM key-derivation " + k.kdf.oid);
|
|
369
274
|
var wantCt = KEM_CT_LENGTHS[k.kem.oid];
|
|
370
275
|
var kemct = k.kemct;
|
|
371
|
-
|
|
372
|
-
// already rejects a wrong kemct length (cms/bad-kem-ciphertext) and a kekLength != wrap size
|
|
373
|
-
// (cms/kek-length-mismatch). These are defense-in-depth on the consumer path (M32 / M29).
|
|
374
|
-
if (wantCt && kemct.length !== wantCt) throw _fail(); // M32: exact ct length before decap
|
|
276
|
+
if (wantCt && kemct.length !== wantCt) throw _fail();
|
|
375
277
|
var kekBytes = Number(k.kekLength);
|
|
376
278
|
var wrapAlg = k.wrap;
|
|
377
|
-
if (WRAP_KEK_LENGTHS[wrapAlg.oid] !== kekBytes) throw _fail();
|
|
378
|
-
// The decapsulation key copy is released as soon as the engine has imported it: the import is
|
|
379
|
-
// the only thing that reads it, so its lifetime does not need to span the decapsulation below.
|
|
279
|
+
if (WRAP_KEK_LENGTHS[wrapAlg.oid] !== kekBytes) throw _fail();
|
|
380
280
|
var keyCopy = _normKeyDer(km.key);
|
|
381
281
|
var priv;
|
|
382
282
|
try { priv = await subtle.importKey("pkcs8", keyCopy.der, { name: wcName }, false, ["decapsulateBits"]); }
|
|
383
283
|
finally { _releaseKeyDer(keyCopy); }
|
|
384
284
|
var ss = null, kek = null, ssAb = null, kekAb = null;
|
|
385
285
|
try {
|
|
386
|
-
// The engine hands back an ArrayBuffer it allocated, and the Buffer below is a copy of it. Both
|
|
387
|
-
// hold the secret, so both are wiped: clearing only the copy this function made would leave the
|
|
388
|
-
// engine's return value readable, which is the same omission one level up.
|
|
389
286
|
ssAb = await subtle.decapsulateBits({ name: wcName }, priv, kemct);
|
|
390
287
|
ss = Buffer.from(ssAb);
|
|
391
288
|
var ssKey = await subtle.importKey("raw", ss, { name: "HKDF" }, false, ["deriveBits"]);
|
|
@@ -393,32 +290,11 @@ async function _kemriCek(ri, km) {
|
|
|
393
290
|
kek = Buffer.from(kekAb);
|
|
394
291
|
return await _aesKwUnwrap(kek, k.encryptedKey);
|
|
395
292
|
} finally {
|
|
396
|
-
// NIST SP 800-227 RS5 / sec. 4.2 and RFC 9629 sec. 7: the shared secret and the KEK it derives
|
|
397
|
-
// are destroyed as soon as they stop being needed. In a `finally`, so an unwrap that fails
|
|
398
|
-
// (the common case under a wrong key or a tampered encryptedKey) leaves nothing behind either;
|
|
399
|
-
// a wipe on the success path alone would keep the secret exactly when an attacker made it fail.
|
|
400
|
-
// Only buffers allocated here are wiped: km.key, the message bytes and the returned CEK are the
|
|
401
|
-
// caller's or the caller's to consume.
|
|
402
|
-
// A Uint8Array view aliases the ArrayBuffer's own bytes, so wiping the view wipes the buffer
|
|
403
|
-
// the engine returned -- no further copy is made in order to destroy one.
|
|
404
293
|
guard.secret.zeroizeAll([ss, kek, ssAb ? new Uint8Array(ssAb) : null, kekAb ? new Uint8Array(kekAb) : null],
|
|
405
294
|
CmsError, "cms/bad-input", "the KEM shared secret");
|
|
406
295
|
}
|
|
407
296
|
}
|
|
408
297
|
|
|
409
|
-
// The declared cipher MODE must match the container's authentication model: an AEAD cipher belongs in
|
|
410
|
-
// AuthEnvelopedData and a plain CBC cipher in EnvelopedData (RFC 5083 sec. 2). Key length alone does
|
|
411
|
-
// not separate them, so without this an EnvelopedData naming AES-GCM was opened as unauthenticated CBC
|
|
412
|
-
// while the result still reported the AEAD algorithm, telling the caller the content was authenticated
|
|
413
|
-
// when nothing had authenticated it. The mirror case dereferenced AEAD parameters the parser leaves
|
|
414
|
-
// null for a non-AEAD algorithm, faulting instead of failing closed.
|
|
415
|
-
//
|
|
416
|
-
// The line drawn here is AEAD-vs-not, which is the line RFC 5083 draws. GCM-vs-CBC is a narrower
|
|
417
|
-
// line this toolkit draws, because AES-GCM is the only AEAD it implements, and stating the narrower
|
|
418
|
-
// one as though the RFC required it sends an operator to argue with a specification that says the
|
|
419
|
-
// opposite: RFC 5084 sec. 3.1 defines AES-CCM for this content type. So an AEAD reaching the right
|
|
420
|
-
// container passes here even when decryption will later decline it, and it is _openContent that says
|
|
421
|
-
// why, by name. An unresolvable OID is left to _openContent too, as the unsupported algorithm it is.
|
|
422
298
|
function _assertContentCipherMode(eci, ct) {
|
|
423
299
|
var oidStr = eci.contentEncryptionAlgorithm.oid;
|
|
424
300
|
var isAead = intrinsic.hasOwn(AEAD_ALGS, oidStr);
|
|
@@ -431,51 +307,26 @@ function _assertContentCipherMode(eci, ct) {
|
|
|
431
307
|
throw _err("cms/unsupported-algorithm", "contentEncryptionAlgorithm " + oidStr + " is an authenticated encryption algorithm, which belongs in an authEnvelopedData rather than a " + ct + " (RFC 5083 sec. 2)");
|
|
432
308
|
}
|
|
433
309
|
|
|
434
|
-
// ---- stage 3: open the content (uniform failure) ---------------------------
|
|
435
310
|
async function _openContent(parsed, eci, cek, ct, streamMode) {
|
|
436
311
|
var alg = eci.contentEncryptionAlgorithm;
|
|
437
312
|
var keyBits = intrinsic.hasOwn(CONTENT_KEYBITS, alg.oid) ? CONTENT_KEYBITS[alg.oid] : 0;
|
|
438
313
|
if (!keyBits) {
|
|
439
|
-
// AES-CCM parses completely here -- registered OIDs, the sec. 3.1 nonce bound, the seven legal
|
|
440
|
-
// ICV lengths -- and is then declined, because AES-GCM is the only AEAD this toolkit implements
|
|
441
|
-
// for content encryption. Saying only "unsupported <oid>" is what an operator also sees for an
|
|
442
|
-
// OID nothing in the toolkit recognizes, and the two call for different things: one is a
|
|
443
|
-
// capability gap to route around, the other is a malformed or exotic message.
|
|
444
314
|
if (intrinsic.hasOwn(AEAD_ALGS, alg.oid) && AEAD_ALGS[alg.oid] === "ccm") {
|
|
445
315
|
throw _err("cms/unsupported-algorithm", "contentEncryptionAlgorithm " + alg.oid + " is AES-CCM. RFC 5084 sec. 3.1 permits it for this content type; this toolkit implements AES-GCM only for content encryption");
|
|
446
316
|
}
|
|
447
317
|
throw _err("cms/unsupported-algorithm", "unsupported contentEncryptionAlgorithm " + alg.oid);
|
|
448
318
|
}
|
|
449
319
|
if (eci.encryptedContent == null) throw _err("cms/no-encrypted-content", "the message has no encryptedContent (detached; supply it out of band)");
|
|
450
|
-
// A null CEK (v1.5 implicit rejection) or a wrong-length CEK -> a fresh random key of the right
|
|
451
|
-
// length, so the failure surfaces here as the uniform verdict, never earlier.
|
|
452
|
-
//
|
|
453
|
-
// The substitute is this function's own allocation, so this function clears it: reassigning the
|
|
454
|
-
// parameter does not change the caller's variable, so the caller's `finally` would wipe the
|
|
455
|
-
// original null / wrong-length value and leave the substitute behind. It is held separately
|
|
456
|
-
// and `cek` is left alone here, because on the normal path `cek` is the caller's recovered key
|
|
457
|
-
// and is still in use after this returns.
|
|
458
320
|
var substitute = null;
|
|
459
321
|
if (cek == null || cek.length !== keyBits / 8) { substitute = nodeCrypto.randomBytes(keyBits / 8); cek = substitute; }
|
|
460
322
|
try {
|
|
461
323
|
if (ct === "authEnvelopedData") {
|
|
462
324
|
var aad = parsed.authAttrsBytes != null ? _explicitSetOf(parsed.authAttrsBytes) : Buffer.alloc(0);
|
|
463
|
-
// GCM authenticates before it yields: the tag is verified here, so a forged message throws
|
|
464
|
-
// cms/decrypt-failed now and the streamed form never yields a chunk of unverified plaintext
|
|
465
|
-
// (RFC 5083 sec. 1). The stream is therefore a chunker over the already-verified plaintext -- no
|
|
466
|
-
// memory win for GCM, which is the correct trade for the integrity guarantee.
|
|
467
325
|
var pt = _gcmOpen(cek, parsed.aead.nonce, eci.encryptedContent, parsed.mac, aad, keyBits, parsed.aead.icvLen);
|
|
468
326
|
return streamMode ? _bufferChunks(pt) : pt;
|
|
469
327
|
}
|
|
470
328
|
var iv = asn1.read.octetString(asn1.decode(alg.parameters));
|
|
471
329
|
if (iv.length !== 16) throw _fail();
|
|
472
|
-
// A lazy CBC stream defers the padding check to iteration. The ambiguous-candidate loop cannot use a
|
|
473
|
-
// deferred check to reject a wrong recipient and try the next, so lazy streaming is used ONLY when a
|
|
474
|
-
// single candidate remains (a wrong key then has nothing to fall back to, so its uniform failure at
|
|
475
|
-
// iteration is the right verdict). "lazy" is the real memory win: the whole plaintext is never held,
|
|
476
|
-
// and the caller opted into receiving chunks before the final() padding check. With several ambiguous
|
|
477
|
-
// candidates the decrypt runs eagerly HERE -- it IS the disambiguator -- and the validated plaintext
|
|
478
|
-
// is then chunked, so the fallback the buffered path relies on is preserved.
|
|
479
330
|
if (streamMode === "lazy") return _cbcStream(cek, iv, eci.encryptedContent, keyBits);
|
|
480
331
|
if (streamMode === "eager") return _bufferChunks(pbes2.cbcDecrypt(cek, iv, eci.encryptedContent, keyBits, CmsError, "cms/decrypt-failed"));
|
|
481
332
|
return pbes2.cbcDecrypt(cek, iv, eci.encryptedContent, keyBits, CmsError, "cms/decrypt-failed");
|
|
@@ -487,22 +338,13 @@ async function _openContent(parsed, eci, cek, ct, streamMode) {
|
|
|
487
338
|
}
|
|
488
339
|
}
|
|
489
340
|
function _gcmOpen(cek, nonce, ct, tag, aad, keyBits, icvLen) {
|
|
490
|
-
// Coverage residual: unreachable via a parsed message, since the strict parser rejects any
|
|
491
|
-
// AuthEnvelopedData whose mac length != aes-ICVlen (cms/mac-length-mismatch) before we get here.
|
|
492
341
|
if (!tag || tag.length !== icvLen) throw _fail();
|
|
493
342
|
var d = nodeCrypto.createDecipheriv("aes-" + keyBits + "-gcm", cek, nonce, { authTagLength: icvLen });
|
|
494
343
|
d.setAuthTag(tag);
|
|
495
344
|
if (aad && aad.length) d.setAAD(aad);
|
|
496
|
-
// final() throws on a tag mismatch, and update() has already produced the whole plaintext by
|
|
497
|
-
// then. RFC 5083 sec. 1 requires a receiver whose integrity check fails to DESTROY the plaintext,
|
|
498
|
-
// not merely withhold it, so the intermediate is cleared on both exits.
|
|
499
345
|
return guard.secret.cipherFinish(d, ct, CmsError, "cms/decrypt-failed", "the recovered content");
|
|
500
346
|
}
|
|
501
347
|
|
|
502
|
-
// ---- streaming content (opts.stream): `content` becomes an async iterable of plaintext chunks -------
|
|
503
|
-
// The already-verified GCM plaintext, yielded a chunk at a time so the streamed and buffered forms have
|
|
504
|
-
// the same `content` shape. GCM holds the whole plaintext (it was verified before this ran), so this is
|
|
505
|
-
// a chunker, not a memory win -- the win is CBC's, below.
|
|
506
348
|
async function* _bufferChunks(buf) {
|
|
507
349
|
var chunk = C.BYTES.kib(64);
|
|
508
350
|
for (var off = 0; off < buf.length; off += chunk) {
|
|
@@ -510,11 +352,6 @@ async function* _bufferChunks(buf) {
|
|
|
510
352
|
yield intrinsic.subarray(buf, off, end < buf.length ? end : buf.length);
|
|
511
353
|
}
|
|
512
354
|
}
|
|
513
|
-
// The CBC streaming content. The engine decipher is created EAGERLY (subtle.decryptStream copies the key
|
|
514
|
-
// at creation), so the caller's post-open `cek` zeroize does not disturb it; the drive is lazy. A wrong
|
|
515
|
-
// key or bad padding surfaces at the engine's final() as an untyped throw, which this collapses to the
|
|
516
|
-
// uniform cms/decrypt-failed -- the same verdict the buffered path returns, so the stream never leaks a
|
|
517
|
-
// distinct failure reason. `await`, not `.then`: the drive dispatches through no caller-replaceable method.
|
|
518
355
|
function _cbcStream(key, iv, ct, keyBits) {
|
|
519
356
|
return _translateCbcStream(subtle.decryptStream({ algNode: "aes-" + keyBits + "-cbc", key: key, iv: iv }, ct));
|
|
520
357
|
}
|
|
@@ -523,7 +360,6 @@ async function* _translateCbcStream(engineIter) {
|
|
|
523
360
|
catch (_e) { throw _fail(); }
|
|
524
361
|
}
|
|
525
362
|
|
|
526
|
-
// ---- EncryptedData (sec. 8) ------------------------------------------------
|
|
527
363
|
async function _decryptEncryptedData(parsed, km, opts) {
|
|
528
364
|
var eci = parsed.encryptedContentInfo;
|
|
529
365
|
var alg = eci.contentEncryptionAlgorithm;
|
|
@@ -540,9 +376,6 @@ async function _decryptEncryptedData(parsed, km, opts) {
|
|
|
540
376
|
}
|
|
541
377
|
async function _decryptPbes2(parsed, eci, km, opts) {
|
|
542
378
|
if (km.password == null) throw _err("cms/bad-input", "this EncryptedData needs a { password }");
|
|
543
|
-
// The PBES2 parameters are attacker-controlled structure the strict parser surfaces raw (PBES2 is
|
|
544
|
-
// not an AEAD it validates): parse them behind a structural guard so a malformed shape is a typed
|
|
545
|
-
// cms/bad-input, never a raw dereference fault (the PBES2 structure is public, not a decrypt oracle).
|
|
546
379
|
var kdf, encOid, iv, pb;
|
|
547
380
|
try {
|
|
548
381
|
var params = pbes2.seqChildren(eci.contentEncryptionAlgorithm.parameters, 2, "PBES2 parameters", _err, "cms");
|
|
@@ -558,11 +391,6 @@ async function _decryptPbes2(parsed, eci, km, opts) {
|
|
|
558
391
|
}
|
|
559
392
|
var keyBits = CONTENT_KEYBITS[encOid];
|
|
560
393
|
if (!keyBits) throw _err("cms/unsupported-algorithm", "unsupported PBES2 content cipher " + encOid);
|
|
561
|
-
// The password-derived content key is ours; the caller's password buffer is not (passwordBytes
|
|
562
|
-
// passes a supplied Buffer straight through) and is left intact.
|
|
563
|
-
// A string / Uint8Array password is encoded into a buffer this toolkit allocated, a credential
|
|
564
|
-
// copy, so it is cleared once the derivation has consumed it. A caller-supplied Buffer is
|
|
565
|
-
// borrowed and left intact.
|
|
566
394
|
var pwE = pbes2.passwordBytesOwned(km.password, _err, "cms");
|
|
567
395
|
var key;
|
|
568
396
|
try { key = nodeCrypto.pbkdf2Sync(pwE.bytes, pb.salt, pb.iterations, keyBits / 8, pb.prfNode); }
|
|
@@ -572,23 +400,11 @@ async function _decryptPbes2(parsed, eci, km, opts) {
|
|
|
572
400
|
finally { guard.secret.zeroize(key, CmsError, "cms/bad-input", "the password-derived content-encryption key"); }
|
|
573
401
|
}
|
|
574
402
|
|
|
575
|
-
// ---- shared helpers (mirror cms-encrypt's builders) ------------------------
|
|
576
403
|
async function _aesKwUnwrap(kek, wrapped) {
|
|
577
404
|
var kekKey = await subtle.importKey("raw", kek, { name: "AES-KW" }, false, ["unwrapKey"]);
|
|
578
|
-
// Import the unwrapped key as HMAC, not AES-CBC: this function exports to raw bytes, so the CryptoKey
|
|
579
|
-
// type is transient and the exported bytes are identical for an AES CEK, while HMAC accepts any wrap
|
|
580
|
-
// length, so an AuthenticatedData MAC key that is not 16/24/32 octets (e.g. a 64-octet HMAC-SHA-512
|
|
581
|
-
// key from another implementation) is recovered instead of rejected before the MAC is even checked.
|
|
582
405
|
var raw = await subtle.unwrapKey("raw", wrapped, kekKey, { name: "AES-KW" }, { name: "HMAC", hash: "SHA-256" }, true, ["sign"]);
|
|
583
|
-
// exportKey allocates an ArrayBuffer holding the unwrapped key; the Buffer below is a copy of it,
|
|
584
|
-
// and the original would otherwise stay readable with nothing referencing it. That is a
|
|
585
|
-
// controllable allocation, not one of the runtime-internal copies the best-effort caveat covers.
|
|
586
|
-
// Only the intermediate is wiped: the returned Buffer is the key the caller must use to open
|
|
587
|
-
// the content, so wiping it here would destroy the result instead of protecting it.
|
|
588
406
|
var rawAb = await subtle.exportKey("raw", raw);
|
|
589
407
|
try {
|
|
590
|
-
// An explicit copy, not Buffer.from(arrayBuffer): that form returns a view sharing the
|
|
591
|
-
// ArrayBuffer's memory, so wiping the intermediate below would zero the key being returned.
|
|
592
408
|
var view = new Uint8Array(rawAb);
|
|
593
409
|
var out = Buffer.alloc(view.length);
|
|
594
410
|
out.set(view);
|
|
@@ -609,20 +425,10 @@ function _kemOtherInfo(wrapName, kekBytes, ukm) {
|
|
|
609
425
|
if (ukm) kids.push(b.explicit(0, b.octetString(ukm)));
|
|
610
426
|
return b.sequence(kids);
|
|
611
427
|
}
|
|
612
|
-
// RFC 3211 double-CBC unwrap (M26): decrypt pass 2 (IV = last block of pass 1), then pass 1
|
|
613
|
-
// (IV = first block), validate the count + complement check bytes, with uniform failure on mismatch.
|
|
614
428
|
function _pwriUnwrap(kek, wrapped, iv, keyBits) {
|
|
615
429
|
var blk = 16, alg = "aes-" + keyBits + "-cbc";
|
|
616
430
|
if (wrapped.length < 2 * blk || wrapped.length % blk !== 0) throw _fail();
|
|
617
431
|
var n = wrapped.length;
|
|
618
|
-
// pass2 = CBC(kek, iv2 = pass1[last block], pass1). Recover pass1's last block by decrypting the
|
|
619
|
-
// last ciphertext block (ECB) and XORing the previous ciphertext block, a standard CBC last-
|
|
620
|
-
// block decrypt that needs no IV, then CBC-decrypt pass2 under that recovered iv2 to get pass1.
|
|
621
|
-
// Every intermediate below is derived from the KEK and reveals key material, so all three are
|
|
622
|
-
// named before the try and cleared in one finally. pass1 and body hold the wrapped key in the
|
|
623
|
-
// clear; lastDec is pass1's final block, recovered ahead of it. The two validation rejects are
|
|
624
|
-
// inside the same scope because they are the paths an attacker induces by tampering with the
|
|
625
|
-
// wrapped key, and they are where an unswept intermediate would otherwise be abandoned.
|
|
626
432
|
var lastDec = null, pass1 = null, body = null;
|
|
627
433
|
try {
|
|
628
434
|
var ecb = nodeCrypto.createDecipheriv("aes-" + keyBits + "-ecb", kek, Buffer.alloc(0)); ecb.setAutoPadding(false);
|
|
@@ -637,34 +443,27 @@ function _pwriUnwrap(kek, wrapped, iv, keyBits) {
|
|
|
637
443
|
if (count < 1 || count + 4 > body.length) throw _fail();
|
|
638
444
|
var cek = body.subarray(4, 4 + count);
|
|
639
445
|
var bad = 0;
|
|
640
|
-
for (var j = 0; j < 3; j++) bad |= (body[1 + j] ^ 0xff) ^ cek[j];
|
|
446
|
+
for (var j = 0; j < 3; j++) bad |= (body[1 + j] ^ 0xff) ^ cek[j];
|
|
641
447
|
if (bad !== 0) throw _fail();
|
|
642
|
-
return Buffer.from(cek);
|
|
448
|
+
return Buffer.from(cek);
|
|
643
449
|
} finally {
|
|
644
450
|
guard.secret.zeroizeAll([body, pass1, lastDec], CmsError, "cms/bad-input", "the PWRI plaintext block");
|
|
645
451
|
}
|
|
646
452
|
}
|
|
647
|
-
// Coverage residual (the unsupported-algorithm throw arm of each lookup below: _hashW3c,
|
|
648
|
-
// _x963Hash, and _originatorSpki that follow): reachable only from a fully well-formed recipient that
|
|
649
|
-
// names an inner algorithm we do not implement (a non-registry OAEP hash / kari scheme / originator
|
|
650
|
-
// form). The producer never emits one; the fuzz harness (fuzz/cms-decrypt.fuzz.js) drives these arms
|
|
651
|
-
// behaviorally under the PkiError-only contract.
|
|
652
453
|
|
|
653
454
|
function _oaepHashFromParams(paramsBytes) {
|
|
654
|
-
if (paramsBytes == null) return "SHA-1";
|
|
455
|
+
if (paramsBytes == null) return "SHA-1";
|
|
655
456
|
var node = asn1.decode(paramsBytes);
|
|
656
457
|
var hashName = "SHA-1", mgfHash = null, label = null;
|
|
657
458
|
(node.children || []).forEach(function (ch) {
|
|
658
459
|
if (ch.tagClass !== "context") return;
|
|
659
|
-
if (ch.tagNumber === 0) { hashName = _hashW3c(asn1.read.oid(ch.children[0].children[0])); }
|
|
660
|
-
else if (ch.tagNumber === 1) {
|
|
460
|
+
if (ch.tagNumber === 0) { hashName = _hashW3c(asn1.read.oid(ch.children[0].children[0])); }
|
|
461
|
+
else if (ch.tagNumber === 1) {
|
|
661
462
|
var mg = ch.children[0];
|
|
662
463
|
if (asn1.read.oid(mg.children[0]) !== O("mgf1")) throw _err("cms/unsupported-algorithm", "unsupported OAEP mask generation function");
|
|
663
464
|
mgfHash = _hashW3c(asn1.read.oid(mg.children[1].children[0]));
|
|
664
|
-
} else if (ch.tagNumber === 2) { label = asn1.read.octetString(ch.children[0].children[1]); }
|
|
465
|
+
} else if (ch.tagNumber === 2) { label = asn1.read.octetString(ch.children[0].children[1]); }
|
|
665
466
|
});
|
|
666
|
-
// WebCrypto RSA-OAEP ties the MGF1 hash to the OAEP hash and supports only an empty label, so any
|
|
667
|
-
// parameter set we cannot faithfully honor is rejected, never silently ignored.
|
|
668
467
|
if (mgfHash != null && mgfHash !== hashName) throw _err("cms/unsupported-algorithm", "the OAEP MGF1 hash must equal the OAEP hash");
|
|
669
468
|
if (label != null && label.length > 0) throw _err("cms/unsupported-algorithm", "a non-empty OAEP label is not supported");
|
|
670
469
|
return hashName;
|
|
@@ -672,7 +471,7 @@ function _oaepHashFromParams(paramsBytes) {
|
|
|
672
471
|
var HASH_W3C = {}; HASH_W3C[O("sha1")] = "SHA-1"; HASH_W3C[O("sha256")] = "SHA-256"; HASH_W3C[O("sha384")] = "SHA-384"; HASH_W3C[O("sha512")] = "SHA-512";
|
|
673
472
|
function _hashW3c(o) { if (!HASH_W3C[o]) throw _err("cms/unsupported-algorithm", "unsupported OAEP hash " + o); return HASH_W3C[o]; }
|
|
674
473
|
|
|
675
|
-
var X963_HASH = {};
|
|
474
|
+
var X963_HASH = {};
|
|
676
475
|
[["dhSinglePass-stdDH-sha1kdf-scheme", "SHA-1"], ["dhSinglePass-stdDH-sha224kdf-scheme", "SHA-224"], ["dhSinglePass-stdDH-sha256kdf-scheme", "SHA-256"], ["dhSinglePass-stdDH-sha384kdf-scheme", "SHA-384"], ["dhSinglePass-stdDH-sha512kdf-scheme", "SHA-512"],
|
|
677
476
|
["dhSinglePass-cofactorDH-sha1kdf-scheme", "SHA-1"], ["dhSinglePass-cofactorDH-sha224kdf-scheme", "SHA-224"], ["dhSinglePass-cofactorDH-sha256kdf-scheme", "SHA-256"], ["dhSinglePass-cofactorDH-sha384kdf-scheme", "SHA-384"], ["dhSinglePass-cofactorDH-sha512kdf-scheme", "SHA-512"]
|
|
678
477
|
].forEach(function (r) { X963_HASH[O(r[0])] = r[1]; });
|
|
@@ -683,9 +482,6 @@ function _kariWrap(kea) {
|
|
|
683
482
|
return { oid: wrapOid, name: oid.name(wrapOid) };
|
|
684
483
|
}
|
|
685
484
|
function _originatorSpki(originator) {
|
|
686
|
-
// originator OriginatorIdentifierOrKey CHOICE; we require originatorKey [1] (RFC 5753 MUST for
|
|
687
|
-
// ephemeral-static). The parser surfaces { form, value:{ algorithm, publicKey } }, so reconstruct
|
|
688
|
-
// the SPKI (SEQUENCE { algorithm, subjectPublicKey BIT STRING }) for importKey.
|
|
689
485
|
if (!originator || originator.form !== "originatorKey") throw _err("cms/unsupported-algorithm", "kari requires an originatorKey (ephemeral-static ECDH)");
|
|
690
486
|
var v = originator.value;
|
|
691
487
|
var algKids = [b.oid(v.algorithm.oid)];
|
|
@@ -696,13 +492,11 @@ var MONT = {}; MONT[O("X25519")] = { name: "X25519", hkdf: "SHA-256" }; MONT[O("
|
|
|
696
492
|
function _isMont(spki) { var o = asn1.read.oid(asn1.decode(spki).children[0].children[0]); return !!MONT[o]; }
|
|
697
493
|
function _montName(spki) { return MONT[asn1.read.oid(asn1.decode(spki).children[0].children[0])]; }
|
|
698
494
|
var CURVE = {}; CURVE[O("prime256v1")] = { curve: "P-256" }; CURVE[O("secp384r1")] = { curve: "P-384" }; CURVE[O("secp521r1")] = { curve: "P-521" };
|
|
699
|
-
// The recipient certificate's EC curve OID -- authoritative for the kari agreement (both keys share it).
|
|
700
495
|
function _ecCurveFromCert(cert) {
|
|
701
496
|
var spki = x509.parse(_normCertDer(cert)).subjectPublicKeyInfo;
|
|
702
497
|
if (spki.algorithm.oid !== O("ecPublicKey") || spki.algorithm.parameters == null) return null;
|
|
703
498
|
return asn1.read.oid(asn1.decode(spki.algorithm.parameters));
|
|
704
499
|
}
|
|
705
|
-
// Rebuild an EC SPKI with explicit namedCurve parameters (for an originator key that omitted them).
|
|
706
500
|
function _withEcCurveParams(spki, curveOid) {
|
|
707
501
|
var node = asn1.decode(spki);
|
|
708
502
|
var alg = node.children[0];
|
|
@@ -712,30 +506,16 @@ var MLKEM = {}; MLKEM[O("id-ml-kem-512")] = "ML-KEM-512"; MLKEM[O("id-ml-kem-768
|
|
|
712
506
|
function _mlkemName(o) { return MLKEM[o]; }
|
|
713
507
|
|
|
714
508
|
function _explicitSetOf(implicitBytes) {
|
|
715
|
-
// The AAD is the authAttrs re-encoded under the EXPLICIT SET OF tag (RFC 5083 sec. 2.2). The
|
|
716
|
-
// parser surfaces authAttrsBytes as the transmitted [1] IMPLICIT form; retag 0xA1 -> 0x31.
|
|
717
|
-
// AuthenticatedData authAttrs are transmitted [2] IMPLICIT (0xA2); the same out[0]=0x31 handles it.
|
|
718
509
|
var out = Buffer.from(implicitBytes);
|
|
719
510
|
out[0] = 0x31;
|
|
720
511
|
return out;
|
|
721
512
|
}
|
|
722
513
|
|
|
723
|
-
// ---- AuthenticatedData verification (RFC 5652 sec. 9.3) ---------------------
|
|
724
|
-
// The macAlgorithm OID name -> WebCrypto HMAC hash, and the digestAlgorithm name -> hash (data-driven).
|
|
725
514
|
var MAC_HASH = { hmacWithSHA256: "SHA-256", hmacWithSHA384: "SHA-384", hmacWithSHA512: "SHA-512" };
|
|
726
515
|
var DIGEST_WC = { sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" };
|
|
727
|
-
// The minimum conveyed MAC-key length (128-bit security floor). A ktri/pwri recipient can wrap a
|
|
728
|
-
// key of any length (unlike AES-KW's 16-octet minimum), so a hostile message could convey a tiny or
|
|
729
|
-
// empty MAC key that weakens the HMAC (or makes subtle.importKey throw); reject it fail-closed.
|
|
730
516
|
var MAC_KEY_MIN_OCTETS = 16;
|
|
731
517
|
function _isDerNull(p) { return Buffer.isBuffer(p) && p.length === 2 && p[0] === 0x05 && p[1] === 0x00; }
|
|
732
518
|
|
|
733
|
-
// Verify a CMS AuthenticatedData (RFC 5652 sec. 9): recover the MAC key through the same RecipientInfo
|
|
734
|
-
// unwrap decrypt uses, recompute the HMAC over the sec. 9.2 preimage (the re-tagged [2] authAttrs SET
|
|
735
|
-
// OF when present, else the eContent value octets), constant-time-compare to the transmitted mac, and,
|
|
736
|
-
// when authAttrs are present, independently confirm message-digest == digest(content) (sec. 9.3
|
|
737
|
-
// do-not-trust-originator). The content is returned only after every check passes. Every
|
|
738
|
-
// secret-dependent failure collapses to the uniform cms/decrypt-failed (no unwrap-success oracle).
|
|
739
519
|
async function _verifyAuthenticatedData(parsed, km, opts) {
|
|
740
520
|
var macAlg = parsed.macAlgorithm;
|
|
741
521
|
var hash = MAC_HASH[macAlg.name];
|
|
@@ -744,15 +524,11 @@ async function _verifyAuthenticatedData(parsed, km, opts) {
|
|
|
744
524
|
var content = parsed.encapContentInfo.eContent;
|
|
745
525
|
if (content == null) throw _err("cms/bad-input", "a detached AuthenticatedData (absent eContent) is not supported");
|
|
746
526
|
|
|
747
|
-
// Determine the sec. 9.2 preimage + (present mode) the message-digest claim, both structural.
|
|
748
527
|
var preimage, mdCheck = null;
|
|
749
528
|
if (parsed.authAttrsBytes) {
|
|
750
529
|
var dHash = DIGEST_WC[parsed.digestAlgorithm && parsed.digestAlgorithm.name];
|
|
751
530
|
if (!dHash) throw _err("cms/unsupported-algorithm", "unsupported digestAlgorithm " + JSON.stringify(parsed.digestAlgorithm && parsed.digestAlgorithm.name));
|
|
752
531
|
preimage = _explicitSetOf(parsed.authAttrsBytes);
|
|
753
|
-
// The parser already enforced content-type == eContentType (cms/content-type-mismatch) and the
|
|
754
|
-
// single-valued message-digest attribute; read the claimed digest from the parsed authAttrs (which
|
|
755
|
-
// decrypt re-parses from the same bytes the MAC covers).
|
|
756
532
|
var mdAttr = (parsed.authAttrs || []).filter(function (a) { return a.type === O("messageDigest"); })[0];
|
|
757
533
|
mdCheck = { hash: dHash, declared: asn1.read.octetString(asn1.decode(mdAttr.values[0])) };
|
|
758
534
|
} else {
|
|
@@ -763,29 +539,16 @@ async function _verifyAuthenticatedData(parsed, km, opts) {
|
|
|
763
539
|
for (var ci = 0; ci < candidates.length; ci++) {
|
|
764
540
|
try {
|
|
765
541
|
_assertSupported(candidates[ci].ri, km);
|
|
766
|
-
var macKey = await _acquireCek(candidates[ci].ri, km, opts);
|
|
767
|
-
// RFC 3218 implicit rejection: _ktriCek returns null on an RSA v1.5 unwrap fault, and a hostile
|
|
768
|
-
// ktri/pwri could convey a below-floor (128-bit) key; in either case substitute a fresh random
|
|
769
|
-
// key so the MAC verify still runs and fails uniformly, never a fast-path that distinguishes an
|
|
770
|
-
// invalid/short unwrap from a MAC mismatch (Bleichenbacher / weak-key oracle freedom).
|
|
771
|
-
// The substitute is held separately, never overwriting macKey: a hostile recipient can
|
|
772
|
-
// unwrap to a non-null key that is merely too short, and assigning over it would drop the only
|
|
773
|
-
// reference to that recovered secret, leaving it readable while the cleanup cleared the random
|
|
774
|
-
// replacement instead. Both are this module's allocations and both are cleared below.
|
|
542
|
+
var macKey = await _acquireCek(candidates[ci].ri, km, opts);
|
|
775
543
|
var macSubstitute = null;
|
|
776
544
|
if (macKey == null || macKey.length < MAC_KEY_MIN_OCTETS) macSubstitute = nodeCrypto.randomBytes(MAC_KEY_MIN_OCTETS);
|
|
777
|
-
// The MAC key is this path's equivalent of the content-encryption key: whether recovered from
|
|
778
|
-
// the recipient entry or substituted, it is cleared once the verify and digest checks are done
|
|
779
|
-
// -- on the failing path too, and the returned object holds a copy of the content, never the key.
|
|
780
545
|
try {
|
|
781
546
|
var key = await subtle.importKey("raw", macSubstitute || macKey, { name: "HMAC", hash: hash }, false, ["verify"]);
|
|
782
547
|
if (!(await subtle.verify({ name: "HMAC" }, key, Buffer.from(parsed.mac), preimage))) throw _fail();
|
|
783
548
|
if (mdCheck) {
|
|
784
549
|
var actual = Buffer.from(await subtle.digest(mdCheck.hash, content));
|
|
785
|
-
if (!actual.equals(mdCheck.declared)) throw _fail();
|
|
550
|
+
if (!actual.equals(mdCheck.declared)) throw _fail();
|
|
786
551
|
}
|
|
787
|
-
// Like GCM, the MAC is verified above before anything is yielded, so the stream is a chunker
|
|
788
|
-
// over the already-authenticated plaintext (uniform `content` shape, no memory win).
|
|
789
552
|
var authedContent = Buffer.from(content);
|
|
790
553
|
return _originFields({
|
|
791
554
|
content: opts.stream === true ? _bufferChunks(authedContent) : authedContent,
|
|
@@ -799,38 +562,11 @@ async function _verifyAuthenticatedData(parsed, km, opts) {
|
|
|
799
562
|
guard.secret.zeroizeAll([macKey, macSubstitute], CmsError, "cms/bad-input", "the message-authentication key");
|
|
800
563
|
}
|
|
801
564
|
} catch (e) {
|
|
802
|
-
if (candidates.length === 1) throw e;
|
|
565
|
+
if (candidates.length === 1) throw e;
|
|
803
566
|
}
|
|
804
567
|
}
|
|
805
|
-
throw _fail();
|
|
806
|
-
}
|
|
807
|
-
// Coverage residual in _verifyAuthenticatedData -- defensive branches reachable only via a hand-built
|
|
808
|
-
// malformed message (the producer never emits them) or unreachable behind an upstream guarantee:
|
|
809
|
-
// * the macAlgorithm-parameters-not-NULL reject and the unsupported-digestAlgorithm reject fire only
|
|
810
|
-
// on a hand-crafted message (this producer emits absent params + SHA-2); the behavioral guard is
|
|
811
|
-
// the frozen cms/unsupported-algorithm code, not a round-trip.
|
|
812
|
-
// * the detached (absent eContent) reject is the deferred-with-condition path (this producer always
|
|
813
|
-
// attaches the content); re-open when a detached AuthenticatedData caller appears.
|
|
814
|
-
// * `parsed.authAttrs || []` / `parsed.recipientInfos || []`: the parser always surfaces these as
|
|
815
|
-
// arrays (or rejects), so the `|| []` fallbacks never fire.
|
|
816
|
-
// * `oid.name(eContentType) || eContentType` fires only for an unregistered content type; `macAlg.name
|
|
817
|
-
// || macAlg.oid` and `digestAlgorithm.name || .oid` never reach the `.oid` fallback because the
|
|
818
|
-
// algorithm name was already validated against MAC_HASH / DIGEST_WC above.
|
|
819
|
-
// * the `macKey == null` half of the random-key substitution fires only for a hand-crafted RSA v1.5
|
|
820
|
-
// ktri (this producer emits OAEP); its behavior is identical to the tested below-floor path (a
|
|
821
|
-
// random key -> the MAC verify fails uniformly), so the < 16 vector covers the substitution.
|
|
822
|
-
// _normKeyDer(key) -> { der, owned }: the recipient private key as PKCS#8 DER, and whether the
|
|
823
|
-
// buffer is one this module made.
|
|
824
|
-
//
|
|
825
|
-
// The distinction decides who may wipe it. A caller handing in their own Buffer keeps a live
|
|
826
|
-
// reference and will use it again; wiping that would destroy the key out from under them. The other
|
|
827
|
-
// two forms produce a new buffer here (a Uint8Array is copied, a PEM string is decoded), and that
|
|
828
|
-
// buffer is a second copy of a private key which nothing else can reach, so it lives until the
|
|
829
|
-
// garbage collector happens to reuse the page unless this module clears it.
|
|
830
|
-
//
|
|
831
|
-
// Returning the flag, in place of always copying, keeps the caller's buffer un-duplicated: making our
|
|
832
|
-
// own copy of every key so we could uniformly wipe it would add a copy of the secret to solve the
|
|
833
|
-
// problem of having one.
|
|
568
|
+
throw _fail();
|
|
569
|
+
}
|
|
834
570
|
function _normKeyDer(key) {
|
|
835
571
|
if (Buffer.isBuffer(key)) return { der: key, owned: false };
|
|
836
572
|
if (key instanceof Uint8Array) return { der: Buffer.from(key), owned: true };
|
|
@@ -842,7 +578,6 @@ function _normKeyDer(key) {
|
|
|
842
578
|
}
|
|
843
579
|
throw _err("cms/bad-input", "the recipient private key must be a PKCS#8 DER Buffer or PEM string");
|
|
844
580
|
}
|
|
845
|
-
// Wipe a key buffer this module owns. A no-op for the caller's own Buffer, which is theirs.
|
|
846
581
|
function _releaseKeyDer(k) {
|
|
847
582
|
if (k && k.owned) guard.secret.zeroize(k.der, CmsError, "cms/bad-input", "the recipient private-key copy");
|
|
848
583
|
}
|