@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/key.js
CHANGED
|
@@ -120,11 +120,23 @@ async function encrypt(privateKey, password, opts) {
|
|
|
120
120
|
var iterations = pbes2.assertIterations(opts.iterations == null ? 600000 : opts.iterations, _err, "key");
|
|
121
121
|
var salt = opts.salt != null ? pbes2.assertSalt(guard.bytes.view(opts.salt, KeyError, "key/bad-input", "salt"), _err, "key") : nodeCrypto.randomBytes(16);
|
|
122
122
|
var iv = nodeCrypto.randomBytes(16);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
123
|
+
// The derived key protects a PRIVATE KEY, so it is cleared as soon as the encryption is done. The
|
|
124
|
+
// caller's password is untouched -- passwordBytes returns a supplied Buffer as-is.
|
|
125
|
+
// A string / Uint8Array password is encoded into a buffer THIS toolkit allocated -- a credential
|
|
126
|
+
// copy -- so it is cleared once the derivation has consumed it. A caller-supplied Buffer is
|
|
127
|
+
// borrowed and left intact.
|
|
128
|
+
var pwK = pbes2.passwordBytesOwned(password, _err, "key");
|
|
129
|
+
var dk;
|
|
130
|
+
try { dk = nodeCrypto.pbkdf2Sync(pwK.bytes, salt, iterations, keyBits / 8, prfNode); }
|
|
131
|
+
finally { if (pwK.owned) guard.secret.zeroize(pwK.bytes, KeyError, "key/bad-input", "the password encoding"); }
|
|
132
|
+
try {
|
|
133
|
+
var ciphertext = pbes2.cbcEncrypt(dk, iv, der, keyBits);
|
|
134
|
+
var epki = b.sequence([pbes2.pbes2AlgId(salt, iterations, prf, cipherName, iv), b.octetString(ciphertext)]);
|
|
135
|
+
pkcs8.parseEncrypted(epki); // self-check: the produced EncryptedPrivateKeyInfo re-parses
|
|
136
|
+
return opts.pem ? pkcs8.pemEncode(epki, "ENCRYPTED PRIVATE KEY") : epki;
|
|
137
|
+
} finally {
|
|
138
|
+
guard.secret.zeroize(dk, KeyError, "key/bad-input", "the password-derived encryption key");
|
|
139
|
+
}
|
|
128
140
|
}
|
|
129
141
|
|
|
130
142
|
/**
|
|
@@ -176,7 +188,13 @@ function _decryptPbes2(encAlg, ciphertext, password, opts) {
|
|
|
176
188
|
// The shared PBES2 decrypt home (strict params + the uniform decrypt-failed on a wrong key / bad pad). The
|
|
177
189
|
// plaintext re-parse below is the MAC-less PBES2-CBC integrity check (RFC 8018 sec. 8): a bad pad and a
|
|
178
190
|
// valid pad whose plaintext is not a PrivateKeyInfo are indistinguishable, and it MUST run (never skipped).
|
|
179
|
-
|
|
191
|
+
// A string / Uint8Array password is encoded into a buffer THIS toolkit allocated -- a credential
|
|
192
|
+
// copy -- so it is cleared once the derivation has consumed it. A caller-supplied Buffer is
|
|
193
|
+
// borrowed and left intact.
|
|
194
|
+
var pwD = pbes2.passwordBytesOwned(password, _err, "key");
|
|
195
|
+
var plaintext;
|
|
196
|
+
try { plaintext = pbes2.pbes2Decrypt(pwD.bytes, encAlg.parameters, ciphertext, opts, _err, "key"); }
|
|
197
|
+
finally { if (pwD.owned) guard.secret.zeroize(pwD.bytes, KeyError, "key/bad-input", "the password encoding"); }
|
|
180
198
|
try { pkcs8.parse(plaintext); }
|
|
181
199
|
catch (_e) { throw _err("key/decrypt-failed", "decryption failed"); }
|
|
182
200
|
return plaintext;
|
package/lib/pbes2.js
CHANGED
|
@@ -38,10 +38,17 @@ var CONTENT_KEYBITS = {}, CONTENT_MODE = {};
|
|
|
38
38
|
|
|
39
39
|
// A password is an octet string (RFC 8018 sec. 2): a string is UTF-8-encoded deterministically (correct for
|
|
40
40
|
// non-ASCII, and byte-identical to OpenSSL), a Buffer/Uint8Array used verbatim.
|
|
41
|
-
function passwordBytes(p, E, prefix) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
function passwordBytes(p, E, prefix) { return passwordBytesOwned(p, E, prefix).bytes; }
|
|
42
|
+
|
|
43
|
+
// The same conversion, reporting OWNERSHIP. A string or Uint8Array password is encoded into a fresh
|
|
44
|
+
// Buffer this toolkit allocated -- a credential copy that must be cleared once the derivation has
|
|
45
|
+
// consumed it -- while a caller-supplied Buffer is BORROWED and must be left intact. Without this
|
|
46
|
+
// distinction a call site can only be safe by leaving every password encoding readable, which is
|
|
47
|
+
// the common case, since a password arrives as a string far more often than as a Buffer.
|
|
48
|
+
function passwordBytesOwned(p, E, prefix) {
|
|
49
|
+
if (Buffer.isBuffer(p)) return { bytes: p, owned: false };
|
|
50
|
+
if (p instanceof Uint8Array) return { bytes: Buffer.from(p), owned: true };
|
|
51
|
+
if (typeof p === "string") return { bytes: Buffer.from(p, "utf8"), owned: true };
|
|
45
52
|
throw E(prefix + "/bad-input", "a password must be a string, Buffer, or Uint8Array");
|
|
46
53
|
}
|
|
47
54
|
|
|
@@ -148,8 +155,19 @@ function pbes2Encrypt(pwBytes, plaintext, opts, E, prefix) {
|
|
|
148
155
|
var iterations = assertIterations(opts.iterations == null ? 2048 : opts.iterations, E, prefix);
|
|
149
156
|
var salt = opts.salt != null ? assertSalt(opts.salt, E, prefix) : nodeCrypto.randomBytes(16);
|
|
150
157
|
var iv = opts.iv != null ? opts.iv : nodeCrypto.randomBytes(16);
|
|
158
|
+
// The derived key is this module's allocation and protects the caller's plaintext -- a private key
|
|
159
|
+
// in the pkcs8 / pkcs12 callers -- so it is cleared once the encryption has consumed it. pwBytes is
|
|
160
|
+
// NOT: the caller formatted it and may still own the buffer.
|
|
151
161
|
var key = nodeCrypto.pbkdf2Sync(pwBytes, salt, iterations, keyBits / 8, prfNode);
|
|
152
|
-
|
|
162
|
+
try {
|
|
163
|
+
return { algId: pbes2AlgId(salt, iterations, prf, cipherName, iv), ct: cbcEncrypt(key, iv, plaintext, keyBits) };
|
|
164
|
+
} finally {
|
|
165
|
+
// E is the caller's error FACTORY, not a class; guard-secret delegates its only throw to
|
|
166
|
+
// guard-bytes, which constructs. yields the error the factory returns, so the
|
|
167
|
+
// currency is compatible -- and the throw is unreachable here regardless, because a freshly
|
|
168
|
+
// allocated pbkdf2 buffer cannot be detached.
|
|
169
|
+
guard.secret.zeroize(key, E, prefix + "/bad-input", "the password-derived encryption key");
|
|
170
|
+
}
|
|
153
171
|
}
|
|
154
172
|
|
|
155
173
|
// PBES2 AES-CBC DECRYPT: parse the PBES2 params (PBKDF2 kdf + AES-CBC scheme), derive the key from
|
|
@@ -182,6 +200,7 @@ function pbes2Decrypt(pwBytes, params, ciphertext, opts, E, prefix) {
|
|
|
182
200
|
var dk = nodeCrypto.pbkdf2Sync(pwBytes, pb.salt, pb.iterations, keyBits / 8, pb.prfNode);
|
|
183
201
|
try { return cbcDecrypt(dk, iv, ciphertext, keyBits); }
|
|
184
202
|
catch (_e) { throw E(prefix + "/decrypt-failed", "decryption failed"); }
|
|
203
|
+
finally { guard.secret.zeroize(dk, E, prefix + "/bad-input", "the password-derived decryption key"); }
|
|
185
204
|
}
|
|
186
205
|
|
|
187
206
|
// ---- PBMAC1 (RFC 9579 / RFC 8018 App. A.5) : PBKDF2 -> HMAC over a message --
|
|
@@ -207,14 +226,24 @@ function pbmac1(pwBytes, salt, iterationCount, keyLength, prfHash, macHash, mess
|
|
|
207
226
|
return subtle.importKey("raw", pwBytes, { name: "PBKDF2" }, false, ["deriveBits"]).then(function (baseKey) {
|
|
208
227
|
return subtle.deriveBits({ name: "PBKDF2", salt: salt, iterations: iterationCount, hash: prfHash }, baseKey, keyLength * 8);
|
|
209
228
|
}).then(function (bits) {
|
|
210
|
-
|
|
229
|
+
// The derived MAC key is this function's allocation, in two forms: the ArrayBuffer deriveBits
|
|
230
|
+
// returned and the Buffer view over it that is imported. Clearing the view clears both, and it
|
|
231
|
+
// runs whether the signing succeeds or throws -- the sibling PBES2 encrypt/decrypt above clear
|
|
232
|
+
// their derived keys, and every PBMAC1 consumer (PKCS#12 build and verification) inherits this.
|
|
233
|
+
var mk = Buffer.from(bits);
|
|
234
|
+
return subtle.importKey("raw", mk, { name: "HMAC", hash: macHash }, false, ["sign"]).then(function (hmacKey) {
|
|
211
235
|
return subtle.sign({ name: "HMAC" }, hmacKey, message);
|
|
236
|
+
}).finally(function () {
|
|
237
|
+
// pbmac1 takes no caller error factory, and this key is engine-adjacent, so the engine's
|
|
238
|
+
// typed class is the right currency -- a raw TypeError must never escape a lib boundary.
|
|
239
|
+
// Unreachable in practice: a freshly allocated Buffer cannot be detached.
|
|
240
|
+
guard.secret.zeroize(mk, webcrypto.WebCryptoError, "webcrypto/operation", "the PBMAC1 key");
|
|
212
241
|
});
|
|
213
242
|
}).then(function (sig) { return Buffer.from(sig); });
|
|
214
243
|
}
|
|
215
244
|
|
|
216
245
|
module.exports = {
|
|
217
|
-
passwordBytes: passwordBytes, assertIterations: assertIterations, assertSalt: assertSalt,
|
|
246
|
+
passwordBytes: passwordBytes, passwordBytesOwned: passwordBytesOwned, assertIterations: assertIterations, assertSalt: assertSalt,
|
|
218
247
|
prfNodeByName: prfNodeByName, prfNodeByOid: prfNodeByOid,
|
|
219
248
|
pbkdf2ParamsSeq: pbkdf2ParamsSeq, pbes2AlgId: pbes2AlgId, parsePbkdf2Params: parsePbkdf2Params,
|
|
220
249
|
requireChildren: requireChildren, seqChildren: seqChildren,
|
package/lib/pkcs12-build.js
CHANGED
|
@@ -100,7 +100,17 @@ var DIGEST_NAME = { sha1: "sha1", sha256: "sha256", sha384: "sha384", sha512: "s
|
|
|
100
100
|
// RFC 7292 App. B.1: a PKCS#12 password is the BMPString (UTF-16BE) encoding of the string plus a trailing
|
|
101
101
|
// 2-byte NULL terminator. "Beavis" -> 14 bytes. A non-BMP scalar (surrogate) is rejected. A Buffer/Uint8Array
|
|
102
102
|
// is taken verbatim as already-formatted bytes (an escape hatch for a caller that pre-encodes).
|
|
103
|
-
function _p12Password(pw) {
|
|
103
|
+
function _p12Password(pw) { return _p12PasswordOwned(pw).bytes; }
|
|
104
|
+
|
|
105
|
+
// The same encoding, reporting OWNERSHIP -- mirroring pbes2.passwordBytesOwned. A caller-supplied
|
|
106
|
+
// Buffer is returned AS-IS and is BORROWED: clearing it would destroy the caller's own credential,
|
|
107
|
+
// which is a worse defect than leaving a copy readable. Every other input is re-encoded into a
|
|
108
|
+
// buffer this module allocated, which it must clear once a derivation has consumed it.
|
|
109
|
+
function _p12PasswordOwned(pw) {
|
|
110
|
+
if (Buffer.isBuffer(pw)) return { bytes: pw, owned: false };
|
|
111
|
+
return { bytes: _p12Encode(pw), owned: true };
|
|
112
|
+
}
|
|
113
|
+
function _p12Encode(pw) {
|
|
104
114
|
if (pw == null) pw = "";
|
|
105
115
|
if (Buffer.isBuffer(pw)) return pw;
|
|
106
116
|
if (pw instanceof Uint8Array) return Buffer.from(pw);
|
|
@@ -148,16 +158,43 @@ function _blockFill(src, blockSize) {
|
|
|
148
158
|
function _p12Kdf(hashName, id, pwBytes, salt, iterations, nBytes) {
|
|
149
159
|
var uv = P12_KDF_UV[hashName], u = uv.u, v = uv.v;
|
|
150
160
|
var D = Buffer.alloc(v, id); // step 1: v copies of the ID diversifier
|
|
151
|
-
|
|
161
|
+
// Each fill is a full block-repeated copy of the salt / password; only their concatenation is
|
|
162
|
+
// used, so the sources are cleared rather than abandoned.
|
|
163
|
+
var sFill = _blockFill(salt, v), pFill = _blockFill(pwBytes, v);
|
|
164
|
+
var I = Buffer.concat([sFill, pFill]); // steps 2-4: I = S || P
|
|
165
|
+
guard.secret.zeroizeAll([sFill, pFill], Pkcs12Error, "pkcs12/bad-input", "a key-derivation fill");
|
|
152
166
|
var c = Math.ceil(nBytes / u); // step 5
|
|
153
|
-
|
|
167
|
+
// The accumulator is allocated ONCE at its final size. Growing it with Buffer.concat per round
|
|
168
|
+
// would abandon a password-derived copy of everything derived so far on every iteration, and
|
|
169
|
+
// those copies could not be reached to clear. Each intermediate digest is cleared as it is
|
|
170
|
+
// superseded, so only the value actually in use is ever readable.
|
|
171
|
+
var out = Buffer.alloc(c * u);
|
|
154
172
|
for (var i = 0; i < c; i++) { // step 6
|
|
155
173
|
var A = Buffer.concat([D, I]);
|
|
156
|
-
for (var r = 0; r < iterations; r++)
|
|
157
|
-
|
|
158
|
-
|
|
174
|
+
for (var r = 0; r < iterations; r++) { // 6A: A = H^r(D||I)
|
|
175
|
+
var prev = A;
|
|
176
|
+
A = nodeCrypto.createHash(hashName).update(A).digest();
|
|
177
|
+
guard.secret.zeroize(prev, Pkcs12Error, "pkcs12/bad-input", "a key-derivation intermediate");
|
|
178
|
+
}
|
|
179
|
+
A.copy(out, i * u); // step 7 (accumulate A_1..A_c)
|
|
180
|
+
if (i < c - 1) { // 6B+6C: modify I for the next block
|
|
181
|
+
var B = _blockFill(A, v); // a full password-derived block
|
|
182
|
+
_kdfStepC(I, B, v);
|
|
183
|
+
guard.secret.zeroize(B, Pkcs12Error, "pkcs12/bad-input", "a key-derivation intermediate");
|
|
184
|
+
}
|
|
185
|
+
guard.secret.zeroize(A, Pkcs12Error, "pkcs12/bad-input", "a key-derivation block");
|
|
159
186
|
}
|
|
160
|
-
|
|
187
|
+
// I is S || P -- it carries the password bytes -- and is finished with here.
|
|
188
|
+
guard.secret.zeroize(I, Pkcs12Error, "pkcs12/bad-input", "the key-derivation input block");
|
|
189
|
+
// Step 8 is "the first nBytes of A", but returning a SUBARRAY would hand back a view over a
|
|
190
|
+
// larger allocation: a caller clearing what it received would leave the rest of the final digest
|
|
191
|
+
// block -- password-derived material -- readable in the same backing buffer (15 unused bytes
|
|
192
|
+
// behind a 5-byte RC2 key, 16 behind a 24-byte 3DES key). Copy into an exact-sized buffer the
|
|
193
|
+
// caller wholly owns, and clear the oversized accumulator here, where it was allocated.
|
|
194
|
+
var exact = Buffer.alloc(nBytes);
|
|
195
|
+
out.copy(exact, 0, 0, nBytes);
|
|
196
|
+
guard.secret.zeroize(out, Pkcs12Error, "pkcs12/bad-input", "the key-derivation accumulator");
|
|
197
|
+
return exact;
|
|
161
198
|
}
|
|
162
199
|
|
|
163
200
|
// RFC 7292 App. B.2 step 6C: treat I as consecutive v-byte blocks and set each I_j = (I_j + B + 1) mod 2^(8v)
|
|
@@ -344,10 +381,18 @@ async function _buildMacData(macOpts, sharedPassword, authSafeDer) {
|
|
|
344
381
|
var node = DIGEST_NAME[hash];
|
|
345
382
|
if (!node || !P12_KDF_UV[node]) throw _err("pkcs12/unsupported-algorithm", "unsupported classic MAC hash " + JSON.stringify(hash) + " (sha1 / sha256 / sha384 / sha512)");
|
|
346
383
|
var iter = _assertMacIter(macOpts.iterations == null ? DEFAULT_MAC_ITER : macOpts.iterations, CLASSIC_MAC_MAX_ITERATIONS);
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
var
|
|
350
|
-
|
|
384
|
+
// _p12Password always ALLOCATES (it re-encodes to BMPString + NULL), so its result is this
|
|
385
|
+
// module's own credential copy and is cleared once the derivation has consumed it.
|
|
386
|
+
var macPw = _p12PasswordOwned(password);
|
|
387
|
+
var macKey = _p12Kdf(node, 3, macPw.bytes, salt, iter, P12_KDF_UV[node].u); // classic KDF -> BMPString
|
|
388
|
+
if (macPw.owned) guard.secret.zeroize(macPw.bytes, Pkcs12Error, "pkcs12/bad-input", "the password encoding");
|
|
389
|
+
try {
|
|
390
|
+
var digest = nodeCrypto.createHmac(node, macKey).update(authSafeDer).digest();
|
|
391
|
+
var digestInfo = b.sequence([b.sequence([b.oid(O(node)), b.nullValue()]), b.octetString(digest)]);
|
|
392
|
+
return b.sequence([digestInfo, b.octetString(salt), b.integer(BigInt(iter))]);
|
|
393
|
+
} finally {
|
|
394
|
+
guard.secret.zeroize(macKey, Pkcs12Error, "pkcs12/bad-input", "the password-derived MAC key");
|
|
395
|
+
}
|
|
351
396
|
}
|
|
352
397
|
if (algorithm === "pbmac1") {
|
|
353
398
|
var prf = PBMAC1_PRF[hash];
|
|
@@ -495,8 +540,11 @@ async function verifyMac(pfx, password, opts) {
|
|
|
495
540
|
if (!P12_KDF_UV[node]) throw _err("pkcs12/unsupported-algorithm", "unsupported classic MAC hash " + m.mac.hashName);
|
|
496
541
|
// The iteration count and salt are attacker-controlled work factors: bound them BEFORE the KDF runs.
|
|
497
542
|
_capWork(m.mac.iterations, m.mac.macSalt, opts, undefined, CLASSIC_MAC_MAX_ITERATIONS);
|
|
498
|
-
var
|
|
499
|
-
|
|
543
|
+
var vMacPw = _p12PasswordOwned(password);
|
|
544
|
+
var macKey = _p12Kdf(node, 3, vMacPw.bytes, m.mac.macSalt, m.mac.iterations, P12_KDF_UV[node].u); // classic KDF -> BMPString
|
|
545
|
+
if (vMacPw.owned) guard.secret.zeroize(vMacPw.bytes, Pkcs12Error, "pkcs12/bad-input", "the password encoding");
|
|
546
|
+
try { computed = nodeCrypto.createHmac(node, macKey).update(m.macedBytes).digest(); }
|
|
547
|
+
finally { guard.secret.zeroize(macKey, Pkcs12Error, "pkcs12/bad-input", "the password-derived MAC key"); }
|
|
500
548
|
} else {
|
|
501
549
|
var kdf = m.mac.pbmac1.kdf;
|
|
502
550
|
var prfWc = PRF_WC[kdf.prfName];
|
|
@@ -693,17 +741,22 @@ function _decryptLegacyPbe(ea, ct, password, opts, budget) {
|
|
|
693
741
|
var u = P12_KDF_UV[scheme.hash].u;
|
|
694
742
|
budget.rounds -= (Math.ceil(scheme.keyLen / u) + Math.ceil(scheme.ivLen / u)) * iterations;
|
|
695
743
|
if (budget.rounds < 0) throw _err("pkcs12/iteration-limit", "the store's aggregate legacy PBE key-derivation work exceeds the budget (a hostile many-bag store)");
|
|
696
|
-
var p12pw =
|
|
697
|
-
var keyM = _p12Kdf(scheme.hash, 1, p12pw, salt, iterations, scheme.keyLen);
|
|
698
|
-
var iv = _p12Kdf(scheme.hash, 2, p12pw, salt, iterations, scheme.ivLen);
|
|
744
|
+
var p12pw = _p12PasswordOwned(password); // App. B.1 BMPString + NULL (NOT the PBES2 UTF-8 encoding)
|
|
745
|
+
var keyM = _p12Kdf(scheme.hash, 1, p12pw.bytes, salt, iterations, scheme.keyLen);
|
|
746
|
+
var iv = _p12Kdf(scheme.hash, 2, p12pw.bytes, salt, iterations, scheme.ivLen);
|
|
747
|
+
if (p12pw.owned) guard.secret.zeroize(p12pw.bytes, Pkcs12Error, "pkcs12/bad-input", "the password encoding");
|
|
699
748
|
// BOTH ciphers funnel through ONE uniform failure (same code AND message), so a bad-pad (secret-dependent)
|
|
700
749
|
// and a valid-pad-wrong-content outcome are indistinguishable -- no CBC padding oracle (RFC 8018 sec. 8). RC2
|
|
701
750
|
// is unavailable in node; the in-tree RFC 2268 primitive fills the gap and its typed reject is normalized here.
|
|
751
|
+
// keyM is a password-derived key this function allocated -- the PBES2 arm of the same dispatch
|
|
752
|
+
// clears its equivalent, so this arm owes it too, on the failing path an attacker drives. The
|
|
753
|
+
// derived IV is not secret and is left alone.
|
|
702
754
|
try {
|
|
703
755
|
if (scheme.rc2) return rc2.cbcDecrypt(keyM, scheme.rc2, iv, ct, _err, "pkcs12/decrypt-failed");
|
|
704
756
|
var d = nodeCrypto.createDecipheriv(scheme.cipher, keyM, iv);
|
|
705
757
|
return Buffer.concat([d.update(ct), d.final()]);
|
|
706
758
|
} catch (_e) { throw _err("pkcs12/decrypt-failed", "decryption failed"); }
|
|
759
|
+
finally { guard.secret.zeroize(keyM, Pkcs12Error, "pkcs12/bad-input", "the password-derived decryption key"); }
|
|
707
760
|
}
|
|
708
761
|
|
|
709
762
|
// Decrypt a PBES2 (RFC 8018) or legacy-PBE (RFC 7292 App. C) bag/safe -- dispatch on the encryptionAlgorithm
|
package/lib/webcrypto.js
CHANGED
|
@@ -467,25 +467,31 @@ SubtleCrypto.prototype.encrypt = async function encrypt(algorithm, key, data) {
|
|
|
467
467
|
var iv = _toBuf(alg.iv, "AES-GCM iv");
|
|
468
468
|
var aad = alg.additionalData ? _toBuf(alg.additionalData, "AES-GCM aad") : null;
|
|
469
469
|
return _toArrayBuffer(_runCipher(function () {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
470
|
+
return _withSecretBytes(key, function (kb) {
|
|
471
|
+
var cipher = nodeCrypto.createCipheriv("aes-" + key.algorithm.length + "-gcm", kb, iv, { authTagLength: (alg.tagLength || 128) / 8 });
|
|
472
|
+
if (aad) cipher.setAAD(aad);
|
|
473
|
+
var ct = Buffer.concat([cipher.update(buf), cipher.final()]);
|
|
474
|
+
return Buffer.concat([ct, cipher.getAuthTag()]);
|
|
475
|
+
});
|
|
474
476
|
}, "encrypt"));
|
|
475
477
|
}
|
|
476
478
|
if (name === "AES-CBC") {
|
|
477
479
|
var cbcIv = _toBuf(alg.iv, "AES-CBC iv");
|
|
478
480
|
return _toArrayBuffer(_runCipher(function () {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
+
return _withSecretBytes(key, function (kb) {
|
|
482
|
+
var c2 = nodeCrypto.createCipheriv("aes-" + key.algorithm.length + "-cbc", kb, cbcIv);
|
|
483
|
+
return Buffer.concat([c2.update(buf), c2.final()]);
|
|
484
|
+
});
|
|
481
485
|
}, "encrypt"));
|
|
482
486
|
}
|
|
483
487
|
if (name === "AES-CTR") {
|
|
484
488
|
_requireCtrLength128(alg);
|
|
485
489
|
var ctrCounter = _toBuf(alg.counter, "AES-CTR counter");
|
|
486
490
|
return _toArrayBuffer(_runCipher(function () {
|
|
487
|
-
|
|
488
|
-
|
|
491
|
+
return _withSecretBytes(key, function (kb) {
|
|
492
|
+
var c3 = nodeCrypto.createCipheriv("aes-" + key.algorithm.length + "-ctr", kb, ctrCounter);
|
|
493
|
+
return Buffer.concat([c3.update(buf), c3.final()]);
|
|
494
|
+
});
|
|
489
495
|
}, "encrypt"));
|
|
490
496
|
}
|
|
491
497
|
throw new WebCryptoError("webcrypto/not-supported", "encrypt: unsupported algorithm " + JSON.stringify(name));
|
|
@@ -499,11 +505,16 @@ SubtleCrypto.prototype.decrypt = async function decrypt(algorithm, key, data) {
|
|
|
499
505
|
if (!ENCRYPT_DECRYPT_NAMES[name]) throw new WebCryptoError("webcrypto/not-supported", "decrypt: unsupported algorithm " + JSON.stringify(name));
|
|
500
506
|
_requireAlgMatch(alg, key, "decrypt");
|
|
501
507
|
if (name === "RSA-OAEP") {
|
|
502
|
-
|
|
508
|
+
// For a CMS key-transport recipient this plaintext IS the recovered content-encryption key, and
|
|
509
|
+
// _toArrayBuffer copies it -- so the node-allocated buffer is finished with the moment the copy
|
|
510
|
+
// exists, and nothing outside this function can reach it to clear it.
|
|
511
|
+
var oaepOut = nodeCrypto.privateDecrypt({
|
|
503
512
|
key: key._handle, padding: nodeCrypto.constants.RSA_PKCS1_OAEP_PADDING,
|
|
504
513
|
oaepHash: _hashNode(key.algorithm.hash, "decrypt"),
|
|
505
514
|
oaepLabel: alg.label ? _toBuf(alg.label, "decrypt label") : undefined,
|
|
506
|
-
}, buf)
|
|
515
|
+
}, buf);
|
|
516
|
+
try { return _toArrayBuffer(oaepOut); }
|
|
517
|
+
finally { guard.secret.zeroize(oaepOut, WebCryptoError, "webcrypto/operation", "the RSA-OAEP decryption output"); }
|
|
507
518
|
}
|
|
508
519
|
if (name === "AES-GCM") {
|
|
509
520
|
var tagLen = (alg.tagLength || 128) / 8;
|
|
@@ -513,31 +524,71 @@ SubtleCrypto.prototype.decrypt = async function decrypt(algorithm, key, data) {
|
|
|
513
524
|
var tag = buf.subarray(buf.length - tagLen);
|
|
514
525
|
var gcmAad = alg.additionalData ? _toBuf(alg.additionalData, "AES-GCM aad") : null;
|
|
515
526
|
return _toArrayBuffer(_runCipher(function () {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
527
|
+
return _withSecretBytes(key, function (kb) {
|
|
528
|
+
var d = nodeCrypto.createDecipheriv("aes-" + key.algorithm.length + "-gcm", kb, iv, { authTagLength: tagLen });
|
|
529
|
+
if (gcmAad) d.setAAD(gcmAad);
|
|
530
|
+
d.setAuthTag(tag);
|
|
531
|
+
return Buffer.concat([d.update(ct), d.final()]);
|
|
532
|
+
});
|
|
520
533
|
}, "decrypt"));
|
|
521
534
|
}
|
|
522
535
|
if (name === "AES-CBC") {
|
|
523
536
|
var cbcIv2 = _toBuf(alg.iv, "AES-CBC iv");
|
|
524
537
|
return _toArrayBuffer(_runCipher(function () {
|
|
525
|
-
|
|
526
|
-
|
|
538
|
+
return _withSecretBytes(key, function (kb) {
|
|
539
|
+
var d2 = nodeCrypto.createDecipheriv("aes-" + key.algorithm.length + "-cbc", kb, cbcIv2);
|
|
540
|
+
return Buffer.concat([d2.update(buf), d2.final()]);
|
|
541
|
+
});
|
|
527
542
|
}, "decrypt"));
|
|
528
543
|
}
|
|
529
544
|
if (name === "AES-CTR") {
|
|
530
545
|
_requireCtrLength128(alg);
|
|
531
546
|
var ctrCounter2 = _toBuf(alg.counter, "AES-CTR counter");
|
|
532
547
|
return _toArrayBuffer(_runCipher(function () {
|
|
533
|
-
|
|
534
|
-
|
|
548
|
+
return _withSecretBytes(key, function (kb) {
|
|
549
|
+
var d3 = nodeCrypto.createDecipheriv("aes-" + key.algorithm.length + "-ctr", kb, ctrCounter2);
|
|
550
|
+
return Buffer.concat([d3.update(buf), d3.final()]);
|
|
551
|
+
});
|
|
535
552
|
}, "decrypt"));
|
|
536
553
|
}
|
|
537
554
|
throw new WebCryptoError("webcrypto/not-supported", "decrypt: unsupported algorithm " + JSON.stringify(name));
|
|
538
555
|
};
|
|
539
556
|
|
|
540
|
-
|
|
557
|
+
// _withSecretBytes(key, fn) -- export a secret key's raw material, hand it to fn, and wipe
|
|
558
|
+
// the export the moment fn is done with it (NIST SP 800-227 RS5 / sec. 4.2, RFC 9629 sec. 7).
|
|
559
|
+
//
|
|
560
|
+
// export() allocates a FRESH Buffer this module owns; the CryptoKey keeps its own copy inside
|
|
561
|
+
// the node KeyObject, so wiping the export never destroys the caller's key or the key itself.
|
|
562
|
+
// Node's cipher and KDF constructors copy the key into their own context, so the export's
|
|
563
|
+
// useful life ends when the operation returns.
|
|
564
|
+
//
|
|
565
|
+
// This is the ONLY way to reach raw secret key material: there is deliberately no unwiped
|
|
566
|
+
// `_secretBytes(key)` primitive to call, because a new consumer that forgot the wipe was the
|
|
567
|
+
// live defect this replaced -- the AES content-encryption paths and two of the three KDF arms
|
|
568
|
+
// each exported the key and left it readable while their sibling arms did not.
|
|
569
|
+
//
|
|
570
|
+
// When fn returns a PROMISE the wipe rides the settlement rather than the callback's return.
|
|
571
|
+
// Node's async primitives copy their inputs when the job is queued, so an eager wipe would not
|
|
572
|
+
// corrupt today's derivations -- but that is an implementation detail of the provider, not a
|
|
573
|
+
// documented guarantee, and the export's useful life is the operation's, not the callback's.
|
|
574
|
+
// Deferring costs nothing and keeps the rule true for any future asynchronous consumer.
|
|
575
|
+
function _withSecretBytes(key, fn) {
|
|
576
|
+
var kb = key._handle.export();
|
|
577
|
+
// Set once the export's lifetime has been handed to the promise handlers below, so the
|
|
578
|
+
// `finally` does not clear a buffer the pending operation is still entitled to.
|
|
579
|
+
var deferred = false;
|
|
580
|
+
try {
|
|
581
|
+
var out = fn(kb);
|
|
582
|
+
if (out && typeof out.then === "function") {
|
|
583
|
+
deferred = true;
|
|
584
|
+
return out.then(function (v) { _wipeExport(kb); return v; }, function (e) { _wipeExport(kb); throw e; });
|
|
585
|
+
}
|
|
586
|
+
return out;
|
|
587
|
+
} finally {
|
|
588
|
+
if (!deferred) guard.secret.zeroize(kb, WebCryptoError, "webcrypto/operation", "the exported key material");
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function _wipeExport(kb) { guard.secret.zeroize(kb, WebCryptoError, "webcrypto/operation", "the exported key material"); }
|
|
541
592
|
|
|
542
593
|
// AES-CTR: node always treats the full 128-bit block as the counter and
|
|
543
594
|
// never reads the spec's `length` (counter-width) parameter. A length < 128
|
|
@@ -595,41 +646,62 @@ async function _deriveBitsRaw(alg, key, length) {
|
|
|
595
646
|
if (name === "ECDH" || name === "X25519" || name === "X448") {
|
|
596
647
|
_requireAlgMatch(alg, alg.public, name + " public key");
|
|
597
648
|
_requireOwnKey(alg.public, "deriveBits public key");
|
|
649
|
+
// The provider hands back z (ECDH) / mz (X25519, X448) in a Buffer this module owns. It is
|
|
650
|
+
// the raw key-agreement shared secret -- the same class of material as a KEM shared secret --
|
|
651
|
+
// so it is wiped once the caller's copy exists, on EVERY exit including the over-request
|
|
652
|
+
// throw. _toArrayBuffer copies via ArrayBuffer.slice, so the returned bits are unaffected.
|
|
598
653
|
var secret = nodeCrypto.diffieHellman({ privateKey: key._handle, publicKey: alg.public._handle });
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
654
|
+
try {
|
|
655
|
+
if (length == null) return _toArrayBuffer(secret);
|
|
656
|
+
_requireDeriveLength(length, name);
|
|
657
|
+
// subarray clamps at the end of the secret, so an unchecked over-request
|
|
658
|
+
// would silently return fewer bytes than asked. W3C deriveBits: throw an
|
|
659
|
+
// OperationError when the requested length cannot be satisfied.
|
|
660
|
+
if (length / 8 > secret.length) {
|
|
661
|
+
throw new WebCryptoError("webcrypto/operation", name + ": requested " + length + " bits but the shared secret has " + (secret.length * 8));
|
|
662
|
+
}
|
|
663
|
+
return _toArrayBuffer(secret.subarray(0, length / 8));
|
|
664
|
+
} finally {
|
|
665
|
+
guard.secret.zeroize(secret, WebCryptoError, "webcrypto/operation", "the raw key-agreement shared secret");
|
|
606
666
|
}
|
|
607
|
-
return _toArrayBuffer(secret.subarray(0, length / 8));
|
|
608
667
|
}
|
|
609
668
|
if (name === "HKDF") {
|
|
610
669
|
_requireDeriveLength(length, "HKDF");
|
|
611
|
-
//
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
|
|
615
|
-
try {
|
|
670
|
+
// The export is a fresh Buffer this module owns -- for a KEM flow it is the shared secret
|
|
671
|
+
// itself. A controllable allocation, not one of the runtime-internal copies the best-effort
|
|
672
|
+
// caveat covers, so it is wiped once the derivation has consumed it.
|
|
673
|
+
return _withSecretBytes(key, function (ikm) {
|
|
616
674
|
var derived = nodeCrypto.hkdfSync(_hashNode(alg.hash, "HKDF"), ikm, _toBuf(alg.salt, "HKDF salt"), _toBuf(alg.info || Buffer.alloc(0), "HKDF info"), length / 8);
|
|
617
675
|
return derived instanceof ArrayBuffer ? derived : _toArrayBuffer(Buffer.from(derived));
|
|
618
|
-
}
|
|
619
|
-
guard.secret.zeroize(ikm, WebCryptoError, "webcrypto/operation", "the HKDF input key material");
|
|
620
|
-
}
|
|
676
|
+
});
|
|
621
677
|
}
|
|
622
678
|
if (name === "PBKDF2") {
|
|
623
679
|
_requireDeriveLength(length, "PBKDF2");
|
|
624
|
-
|
|
625
|
-
|
|
680
|
+
// The derivation is asynchronous; _withSecretBytes defers the wipe to the promise's
|
|
681
|
+
// settlement so the export outlives the operation rather than the callback.
|
|
682
|
+
return _withSecretBytes(key, function (pw) {
|
|
683
|
+
return _pbkdf2Async(pw, _toBuf(alg.salt, "PBKDF2 salt"), alg.iterations, length / 8, _hashNode(alg.hash, "PBKDF2"))
|
|
684
|
+
.then(function (out) {
|
|
685
|
+
// _toArrayBuffer COPIES, so the node-allocated derivation buffer is finished with the
|
|
686
|
+
// moment the copy exists -- and nothing else can reach it to clear it.
|
|
687
|
+
try { return _toArrayBuffer(out); }
|
|
688
|
+
finally { guard.secret.zeroize(out, WebCryptoError, "webcrypto/operation", "the PBKDF2 output"); }
|
|
689
|
+
});
|
|
690
|
+
});
|
|
626
691
|
}
|
|
627
692
|
if (name === "X963KDF") {
|
|
628
693
|
// ANSI-X9.63 / SEC1 sec. 3.6.1 single-step KDF: K = H(Z || INT32(counter) || SharedInfo)
|
|
629
694
|
// concatenated over counter = 1, 2, ... The RFC 5753 kari KEK derivation; the base key holds
|
|
630
695
|
// the ECDH shared secret Z, alg.info holds the DER ECC-CMS-SharedInfo.
|
|
631
696
|
_requireDeriveLength(length, "X963KDF");
|
|
632
|
-
|
|
697
|
+
// The base key here HOLDS the ECDH shared secret Z of an RFC 5753 kari, so this export is
|
|
698
|
+
// exactly as sensitive as the raw agreement secret wiped above.
|
|
699
|
+
return _withSecretBytes(key, function (z) {
|
|
700
|
+
var derived = _x963Kdf(_hashNode(alg.hash, "X963KDF"), z, _toBuf(alg.info || Buffer.alloc(0), "X963KDF SharedInfo"), length / 8);
|
|
701
|
+
// Same as PBKDF2 above: the copy is what the caller receives, so the derived buffer is cleared.
|
|
702
|
+
try { return _toArrayBuffer(derived); }
|
|
703
|
+
finally { guard.secret.zeroize(derived, WebCryptoError, "webcrypto/operation", "the X9.63 KDF output"); }
|
|
704
|
+
});
|
|
633
705
|
}
|
|
634
706
|
throw new WebCryptoError("webcrypto/not-supported", "deriveBits: unsupported algorithm " + JSON.stringify(name));
|
|
635
707
|
}
|
|
@@ -644,7 +716,16 @@ function _x963Kdf(hashNode, z, sharedInfo, lenBytes) {
|
|
|
644
716
|
var h = nodeCrypto.createHash(hashNode).update(z).update(ctr).update(sharedInfo).digest();
|
|
645
717
|
blocks.push(h); got += h.length; counter += 1;
|
|
646
718
|
}
|
|
647
|
-
|
|
719
|
+
// Return an exact-sized buffer the caller wholly owns, not a VIEW over the joined blocks: a
|
|
720
|
+
// caller clearing what it received would otherwise leave the unused tail of the final digest
|
|
721
|
+
// block -- derived from the shared secret -- readable behind it. The accumulator and the
|
|
722
|
+
// per-block digests are cleared here, where they were allocated.
|
|
723
|
+
var joined = Buffer.concat(blocks);
|
|
724
|
+
var exact = Buffer.alloc(lenBytes);
|
|
725
|
+
joined.copy(exact, 0, 0, lenBytes);
|
|
726
|
+
guard.secret.zeroize(joined, WebCryptoError, "webcrypto/operation", "the KDF accumulator");
|
|
727
|
+
for (var bi = 0; bi < blocks.length; bi++) guard.secret.zeroize(blocks[bi], WebCryptoError, "webcrypto/operation", "a KDF block");
|
|
728
|
+
return exact;
|
|
648
729
|
}
|
|
649
730
|
|
|
650
731
|
SubtleCrypto.prototype.deriveBits = async function deriveBits(algorithm, key, length) {
|
|
@@ -686,8 +767,12 @@ SubtleCrypto.prototype.deriveKey = async function deriveKey(algorithm, baseKey,
|
|
|
686
767
|
// material; a KDF base has no implicit output size and fails closed.
|
|
687
768
|
bits = dk.length != null ? dk.length : null;
|
|
688
769
|
}
|
|
770
|
+
// The derived bits are the KEY -- importKey copies them into a KeyObject, so this buffer is a
|
|
771
|
+
// transient copy nothing else can reach once the CryptoKey exists, and it is cleared on the
|
|
772
|
+
// failing import too (a bad derivedKeyType is caller-controlled).
|
|
689
773
|
var raw = await _deriveBitsRaw(alg, baseKey, bits);
|
|
690
|
-
return this.importKey("raw", raw, dk, extractable, keyUsages);
|
|
774
|
+
try { return await this.importKey("raw", raw, dk, extractable, keyUsages); }
|
|
775
|
+
finally { guard.secret.zeroize(new Uint8Array(raw), WebCryptoError, "webcrypto/operation", "the derived key material"); }
|
|
691
776
|
};
|
|
692
777
|
|
|
693
778
|
// ML-KEM (FIPS 203) key encapsulation over Node's crypto.encapsulate/decapsulate.
|
|
@@ -766,8 +851,12 @@ SubtleCrypto.prototype.decapsulateBits = async function decapsulateBits(algorith
|
|
|
766
851
|
};
|
|
767
852
|
|
|
768
853
|
SubtleCrypto.prototype.wrapKey = async function wrapKey(format, key, wrappingKey, wrapAlgorithm) {
|
|
854
|
+
// is the PLAINTEXT serialization of the key being wrapped -- the very material the wrap
|
|
855
|
+
// exists to protect. It is this function's allocation and is cleared once the wrap has consumed
|
|
856
|
+
// it, on the delegated branch as well as the AES-KW one.
|
|
769
857
|
var exported = await this.exportKey(format, key);
|
|
770
858
|
var bytes = (format === "jwk") ? Buffer.from(JSON.stringify(exported)) : Buffer.from(exported);
|
|
859
|
+
try {
|
|
771
860
|
var alg = _normalizeAlg(wrapAlgorithm, "wrapKey");
|
|
772
861
|
_requireUsage(wrappingKey, "wrapKey");
|
|
773
862
|
if (alg.name !== "AES-KW" && !ENCRYPT_DECRYPT_NAMES[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "wrapKey: unsupported algorithm " + JSON.stringify(alg.name));
|
|
@@ -782,17 +871,24 @@ SubtleCrypto.prototype.wrapKey = async function wrapKey(format, key, wrappingKey
|
|
|
782
871
|
// The mirror of unwrapKey below: in a KEM flow this export is the SENDER's copy of the same
|
|
783
872
|
// key-encryption key, so leaving it unwiped would keep a full copy of the KEK alive for the
|
|
784
873
|
// process lifetime and make the wipes the CMS layer performs pointless in the encrypt direction.
|
|
785
|
-
var wkBytes = null;
|
|
786
874
|
try {
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
875
|
+
return _withSecretBytes(wrappingKey, function (wkBytes) {
|
|
876
|
+
var c = nodeCrypto.createCipheriv("aes" + wrappingKey.algorithm.length + "-wrap", wkBytes, Buffer.from("A6A6A6A6A6A6A6A6", "hex"));
|
|
877
|
+
return _toArrayBuffer(Buffer.concat([c.update(bytes), c.final()]));
|
|
878
|
+
});
|
|
790
879
|
} catch (e) { throw new WebCryptoError("webcrypto/operation", "wrapKey: AES-KW key wrap failed", e); }
|
|
791
|
-
finally { guard.secret.zeroize(wkBytes, WebCryptoError, "webcrypto/operation", "the AES-KW wrapping key"); }
|
|
792
880
|
}
|
|
793
881
|
// Delegate to a content-encryption algorithm (RSA-OAEP / AES-GCM).
|
|
794
882
|
var wrapKeyClone = _cloneWithUsage(wrappingKey, "encrypt");
|
|
795
|
-
return this.encrypt(wrapAlgorithm, wrapKeyClone, bytes);
|
|
883
|
+
return await this.encrypt(wrapAlgorithm, wrapKeyClone, bytes);
|
|
884
|
+
} finally {
|
|
885
|
+
// Clearing also clears the export it came from: for every non-jwk format
|
|
886
|
+
// Buffer.from(<ArrayBuffer>) is a VIEW over that buffer, not a copy, so one wipe covers both.
|
|
887
|
+
// A "jwk" export is the residual -- its key material lives in immutable JavaScript strings that
|
|
888
|
+
// no code can overwrite -- so wrapping a jwk serialization cannot offer this guarantee, and the
|
|
889
|
+
// documentation does not claim it does.
|
|
890
|
+
guard.secret.zeroize(bytes, WebCryptoError, "webcrypto/operation", "the exported key being wrapped");
|
|
891
|
+
}
|
|
796
892
|
};
|
|
797
893
|
|
|
798
894
|
SubtleCrypto.prototype.unwrapKey = async function unwrapKey(format, wrappedKey, unwrappingKey, unwrapAlgorithm, unwrappedKeyAlgorithm, extractable, keyUsages) {
|
|
@@ -817,16 +913,15 @@ SubtleCrypto.prototype.unwrapKey = async function unwrapKey(format, wrappedKey,
|
|
|
817
913
|
// The exported wrapping key is a Buffer this module owns; in a KEM flow it is the KEK derived
|
|
818
914
|
// from the shared secret, so it is wiped once the unwrap has consumed it -- on the failing path
|
|
819
915
|
// too, which is the one an attacker induces by tampering with the wrapped key.
|
|
820
|
-
//
|
|
821
|
-
//
|
|
822
|
-
//
|
|
823
|
-
var kwBytes = null;
|
|
916
|
+
// The export happens INSIDE the try: a key whose handle cannot be exported must still surface
|
|
917
|
+
// the typed verdict this branch promises, not a raw node TypeError -- and a non-PkiError throw
|
|
918
|
+
// would also break the fuzz-harness contract.
|
|
824
919
|
try {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
920
|
+
bytes = _withSecretBytes(unwrappingKey, function (kwBytes) {
|
|
921
|
+
var d = nodeCrypto.createDecipheriv("aes" + unwrappingKey.algorithm.length + "-wrap", kwBytes, Buffer.from("A6A6A6A6A6A6A6A6", "hex"));
|
|
922
|
+
return Buffer.concat([d.update(wrapped), d.final()]);
|
|
923
|
+
});
|
|
828
924
|
} catch (e) { throw new WebCryptoError("webcrypto/operation", "unwrapKey: AES-KW key unwrap failed (integrity or length)", e); }
|
|
829
|
-
finally { guard.secret.zeroize(kwBytes, WebCryptoError, "webcrypto/operation", "the AES-KW unwrapping key"); }
|
|
830
925
|
} else {
|
|
831
926
|
var unwrapKeyClone = _cloneWithUsage(unwrappingKey, "decrypt");
|
|
832
927
|
bytes = Buffer.from(await this.decrypt(unwrapAlgorithm, unwrapKeyClone, wrappedKey));
|
|
@@ -1101,16 +1196,13 @@ SubtleCrypto.prototype.exportKey = async function exportKey(format, key) {
|
|
|
1101
1196
|
if (!key.extractable) throw new WebCryptoError("webcrypto/invalid-access", "key is not extractable");
|
|
1102
1197
|
if (format === "jwk") return key._handle.export({ format: "jwk" });
|
|
1103
1198
|
if (key.type === "secret") {
|
|
1104
|
-
// export
|
|
1105
|
-
// ArrayBuffer.slice -- so the export
|
|
1106
|
-
//
|
|
1107
|
-
|
|
1108
|
-
try {
|
|
1199
|
+
// The export is a fresh Buffer holding the secret key, and _toArrayBuffer copies it via
|
|
1200
|
+
// ArrayBuffer.slice -- so the export is a controllable allocation nothing references once the
|
|
1201
|
+
// copy exists. The unsupported-format throw below clears it too.
|
|
1202
|
+
return _withSecretBytes(key, function (raw) {
|
|
1109
1203
|
if (format === "raw") return _toArrayBuffer(raw);
|
|
1110
1204
|
throw new WebCryptoError("webcrypto/not-supported", "exportKey: secret keys support 'raw' / 'jwk' only");
|
|
1111
|
-
}
|
|
1112
|
-
guard.secret.zeroize(raw, WebCryptoError, "webcrypto/operation", "the exported secret key");
|
|
1113
|
-
}
|
|
1205
|
+
});
|
|
1114
1206
|
}
|
|
1115
1207
|
if (format === "spki") return _toArrayBuffer(key._handle.export({ format: "der", type: "spki" }));
|
|
1116
1208
|
if (format === "pkcs8") return _toArrayBuffer(key._handle.export({ format: "der", type: "pkcs8" }));
|
package/package.json
CHANGED