@blamejs/pki 0.3.9 → 0.3.10

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 CHANGED
@@ -4,6 +4,19 @@ All notable changes to `@blamejs/pki` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## v0.3.10 — 2026-07-23
8
+
9
+ pki.key exports, imports, and PBES2-encrypts private keys (RFC 5958, RFC 8018) over every WebCrypto algorithm.
10
+
11
+ ### Added
12
+
13
+ - pki.key.encrypt(privateKey, password, opts) encrypts a PKCS#8 private key (DER, PEM, or an extractable CryptoKey) into an RFC 5958 EncryptedPrivateKeyInfo under RFC 8018 PBES2. opts selects the cipher (aes-256-cbc default, aes-192-cbc, aes-128-cbc), the pseudorandom function (hmacWithSHA256 default, SHA-384, SHA-512, SHA-1), the iteration count (default 600000, bounded by the decryptor's cap), and the salt (16 random octets by default); opts.pem returns an ENCRYPTED PRIVATE KEY string. The plaintext is validated as a well-formed PKCS#8 structure before encryption and the output is re-parsed before return. A default pseudorandom function is omitted and keyLength is omitted, so the parameters are byte-exact with OpenSSL. RFC 5958 sec. 3, RFC 8018.
14
+ - pki.key.decrypt(encrypted, password, opts) decrypts an RFC 5958 EncryptedPrivateKeyInfo (DER or ENCRYPTED PRIVATE KEY PEM), returning the inner PrivateKeyInfo re-validated through pki.schema.pkcs8.parse. Only PBES2 with a PBKDF2 key-derivation function and an AES-CBC scheme is accepted -- PBES1, PBMAC1, scrypt, and any other algorithm are refused. The salt and iteration count are bounded before any key derivation (opts.maxIterations lowers the cap for this call, never raises it), a malformed parameter set or wrong-length IV is a distinct typed error, and -- because a MAC-less PBES2-CBC decrypt must not be a padding oracle (RFC 8018 sec. 8) -- a wrong password and a valid pad that is not a private key both surface the single uniform decrypt-failed. RFC 5958 sec. 3, RFC 8018.
15
+ - pki.key.export(key, opts) exports an extractable CryptoKey to DER or PEM: a private key as PKCS#8, a public key as SubjectPublicKeyInfo. The encoding is delegated to WebCrypto, so the algorithm-specific parameters are correct -- RSA an explicit NULL, EC a named curve, Ed25519/Ed448/X25519/X448 parameters absent (RFC 8410 sec. 3). RFC 5958, RFC 5280 sec. 4.1.2.7.
16
+ - pki.key.import(input, opts) imports a DER or PEM PKCS#8 private key, SPKI public key, or (with opts.password) an ENCRYPTED PRIVATE KEY into a CryptoKey, auto-detecting the structure. The WebCrypto algorithm is inferred for the algorithms that name exactly one (Ed25519, Ed448, X25519, X448, ML-DSA, ML-KEM, SLH-DSA); RSA and EC are ambiguous between signing and key agreement, so opts.algorithm is required for them -- import fails closed rather than guess a use.
17
+ - pki.key.generate(algorithm, opts) generates a key pair over the WebCrypto engine -- RSA, ECDSA/ECDH, Ed25519/Ed448, X25519/X448, and the FIPS post-quantum ML-DSA and ML-KEM -- with usages defaulting to the algorithm's natural set. pki.key.publicFromPrivate(privateKey) derives the SubjectPublicKeyInfo public key from a private key.
18
+ - pki.errors.KeyError is the typed error for the pki.key domain (key/bad-input, key/unsupported-algorithm, key/bad-algorithm-parameters, key/iteration-limit, key/bad-version, key/decrypt-failed).
19
+
7
20
  ## v0.3.9 — 2026-07-23
8
21
 
9
22
  pki.crl builds, signs, and verifies X.509 certificate revocation lists (RFC 5280 sec. 5) over any registry algorithm.
package/README.md CHANGED
@@ -228,6 +228,7 @@ is callable today; nothing below is a stub.
228
228
  | `pki.crmf` | RFC 4211 certificate-request-message issuance — `build(spec, key, opts)` assembles a `CertReqMessages`: a `spec` of `certReqId` (default 0; the RFC 9483 `-1` sentinel allowed), a `certTemplate` of the requested certificate fields (`subject`, `publicKey` — the SPKI DER of the key being certified — `validity`, requested `extensions`, an optional `version` 2), optional `controls` and `regInfo` (regToken / authenticator / utf8Pairs / oldCertID / protocolEncrKey, or pre-encoded `AttributeTypeAndValue` DER), and an optional `pop` selector. `key` (or `{ key }`) is the requester's private key — the message carries a `POPOSigningKey` proof of possession signed with the private half of `certTemplate.publicKey` (verified before the message is returned), exactly as a PKCS#10 CSR proves possession; a complete template signs the `CertRequest`, an incomplete one signs a `POPOSigningKeyInput`. The signature algorithm is resolved from the requested public key, so RSA (PKCS#1 v1.5 / PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. `key` is optional for a `raVerified` proof. Pass an array of specs for a batch; the CA-assigned template fields are never emitted. Returns DER, or a PEM block with `opts.pem`; malformed input throws a typed `CrmfError`. Parsing stays at `pki.schema.crmf.parse` — `build` |
229
229
  | `pki.cmp` | RFC 9810 Certificate Management Protocol message building — `build(message, opts)` assembles a protected `PKIMessage`. `message.header` carries the `sender` / `recipient` GeneralNames (including the anonymous NULL-DN) plus optional transaction metadata (`transactionID`, `senderNonce` / `recipNonce`, `messageTime` as a GeneralizedTime, `senderKID` / `recipKID`, `freeText`, `generalInfo`); `message.body` is a single-key object naming the arm. Request-side: `ir` / `cr` / `kur` (a `CertReqMessages` spec delegated to `pki.crmf.build`), `p10cr` (a PKCS#10 `CertificationRequest`), `certConf`, `pollReq`, `genm`, `rr`. CA/responder-side: `ip` / `cp` / `kup` / `ccp` (a `CertRepMessage` — `caPubs` and `response` entries carrying a `PKIStatusInfo` and, under a granting status, a `certifiedKeyPair`), `rp` (revocation response), `genp`, `error`, `pollRep`, `krp` (key-recovery response), `pkiconf`. Protection is exactly one of `opts.{ key, cert }` — a signature over the message under the sender key, the algorithm resolved from the signer certificate so RSA (PKCS#1 v1.5 / PSS), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch — or `opts.mac` — a PBMAC1 shared-secret HMAC (RFC 9481 / 9579, PBKDF2-derived). The protection covers the exact DER of the virtual `ProtectedPart` (the header and body) and is self-verified before the message is returned; the `protectionAlg` is derived, never caller-set, so the message the parser accepts is coherent by construction. Returns DER, or a PEM `CMP` block with `opts.pem`; malformed input throws a typed `CmpError`. Parsing stays at `pki.schema.cmp.parse` — `build` |
230
230
  | `pki.crl` | RFC 5280 §5 certificate revocation list issuance — `sign(spec, issuer, opts)` builds and signs a `CertificateList`: a `spec` of `thisUpdate` / `nextUpdate`, an optional `crlNumber`, a `revoked` array (each entry a `serialNumber` + `revocationDate` with an optional `reason` or `invalidityDate`), and an optional `extensions` object (authority key identifier, issuing distribution point, delta-CRL indicator, freshest CRL, authority information access) or an array of pre-encoded Extension DER; an `issuer` of `{ cert, key }` or `{ name, publicKey, key }`. The signature algorithm is resolved from the issuer key, so RSA (PKCS#1 v1.5 / PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. The version is derived from the extension set (v2 when any CRL or entry extension is present, else v1), the outer `signatureAlgorithm` matches `tbsCertList.signature`, an empty revocation list omits the field rather than emitting an empty SEQUENCE, `reasonCode` is an ENUMERATED and `invalidityDate` is always GeneralizedTime, per-extension criticality is fixed by the RFC, and the produced signature is verified under the issuer key before return. `verify(crl, issuer)` checks a CRL signature through the one path-validation signature engine (algorithm-confusion and EdDSA low-order gates included), and `isRevoked(crl, serialNumber)` looks a serial up in the revocation list. Returns DER, or a PEM `X509 CRL` with `opts.pem`; malformed input throws a typed `CrlError`. Parsing stays at `pki.schema.crl.parse` — `sign` / `verify` / `isRevoked` |
231
+ | `pki.key` | RFC 5958 / RFC 8018 key-material lifecycle — `encrypt(privateKey, password, opts)` wraps a PKCS#8 private key (DER, PEM, or an extractable `CryptoKey`) into an `EncryptedPrivateKeyInfo` under PBES2 (PBKDF2 + AES-CBC-Pad): `opts` selects the `cipher` (`aes-256-cbc` default, `aes-192-cbc`, `aes-128-cbc`), the `prf` (`hmacWithSHA256` default, SHA-384/512, SHA-1), the `iterations` (default 600000), and the `salt`; the plaintext is validated as PKCS#8 before encryption, a default `prf` and `keyLength` are omitted so the parameters are byte-exact with OpenSSL, and the output is re-parsed before return. `decrypt(encrypted, password, opts)` recovers the inner `PrivateKeyInfo` (re-validated through `pki.schema.pkcs8.parse`) — only PBES2/PBKDF2/AES-CBC is accepted (PBES1, PBMAC1, scrypt refused), the salt and iteration count are bounded before any derivation (`opts.maxIterations` lowers the cap), a malformed parameter set or wrong-length IV is a distinct typed error, and — because a MAC-less PBES2-CBC decrypt must not be a padding oracle (RFC 8018 §8) — a wrong password and a valid-pad-but-not-a-key both surface the one uniform `key/decrypt-failed`. `export(key, opts)` / `import(input, opts)` move a private key as PKCS#8 or a public key as SubjectPublicKeyInfo, delegating the encoding to WebCrypto so RSA carries an explicit NULL, EC a named curve, and Ed25519/Ed448/X25519/X448 omit parameters (an ambiguous RSA/EC import requires `opts.algorithm`). `generate(algorithm, opts)` produces a key pair over RSA, ECDSA/ECDH, the Edwards/Montgomery curves, and the FIPS post-quantum ML-DSA / ML-KEM, and `publicFromPrivate(privateKey)` derives the public key. Returns DER or PEM; fail-closed with typed `KeyError`. Parsing stays at `pki.schema.pkcs8.parse` — `encrypt` / `decrypt` / `export` / `import` / `generate` / `publicFromPrivate` |
231
232
  | `pki.cms` | RFC 5652 §5 CMS SignedData signing + signature verification — `sign(content, signers, opts)` produces a SignedData (attached or detached, one or many signers, RSA / RSASSA-PSS / ECDSA / EdDSA, the post-quantum ML-DSA-44/65/87 (RFC 9882) and SLH-DSA (all twelve FIPS 205 sets, RFC 9814), and composite ML-DSA (pairing ML-DSA with a traditional RSA / ECDSA / EdDSA — accepted only when **both** components verify — draft-ietf-lamps-cms-composite-sigs)); it builds the signed attributes (content-type, message-digest, signing-time) as canonical DER, signs the exact §5.4 preimage, and emits a DER `Buffer` or PEM. `verify(input, opts)` parses a SignedData over the strict `pki.schema.cms` codec, locates each SignerInfo's signer certificate by its issuerAndSerialNumber or subjectKeyIdentifier, and checks the signature over the exact §5.4 preimage: when signed attributes are present it confirms the message-digest attribute equals the content digest and verifies over the DER re-encoding of the SignedAttributes (the on-wire `[0]` tag replaced by a universal SET OF), otherwise directly over the content. It returns a per-signer verdict with the matched signer certificate; it does not chain that certificate to a trust anchor — that is the caller's step through `pki.path.validate`. **Content encryption** (RFC 5652/5083/5084/9629): `encrypt(content, recipients, opts)` produces an EnvelopedData, AuthEnvelopedData (AES-GCM, the authenticated default), or EncryptedData — recipients auto-dispatch off the certificate key to key-transport (RSAES-OAEP; v1.5 never emitted), key-agreement (ephemeral-static ECDH over P-256/384/521 with the X9.63 KDF, and X25519/X448 with HKDF), symmetric key-wrap, password (PBKDF2 + RFC 3211 PWRI-KEK), or the post-quantum ML-KEM KEMRecipientInfo (RFC 9629/9936) — one fresh content key wrapped for every recipient. `decrypt(input, keyMaterial, opts)` recovers the content through the matching arm and returns it with an `authenticated` flag; every secret-dependent failure collapses to one uniform `cms/decrypt-failed` verdict (Bleichenbacher / EFAIL / password-oracle freedom), and PKCS#1 v1.5 is decrypt-only under the RFC 3218 implicit-rejection countermeasure. **Compression** (RFC 3274): `compress(content, opts)` / `decompress(input, opts)` produce and consume a CompressedData (ZLIB, version 0, id-alg-zlibCompress); decompress bounds the uncompressed output at 16 MiB and stops before it is materialized, so a decompression bomb fails closed as `cms/decompress-too-large` — a size transform with no integrity/confidentiality (RFC 8551 §2.4.5). Fail-closed with typed `cms/*` errors — `sign`, `verify`, `encrypt`, `decrypt`, `compress`, `decompress` |
232
233
  | `pki.smime` | RFC 8551 S/MIME message assembly, verification, encryption, and compression over the CMS layer — `sign(content, signers, opts)` wraps a MIME entity as a signed S/MIME message in either form: `multipart/signed` (clear-signed — the content stays readable in any MUA, a detached CMS SignedData rides alongside as `application/pkcs7-signature` with a matching `micalg`) or `application/pkcs7-mime; smime-type=signed-data` (opaque — the whole entity is a base64 CMS SignedData). The signed bytes are the entity's RFC 8551 §3.1.1 canonical form (CRLF line endings); `verify(message, opts)` unwraps both forms and recomputes over the same canonicalizer, so a transport that re-wraps line endings still verifies and a tampered part fails. `encrypt(content, recipients, opts)` envelopes a MIME entity as an opaque `application/pkcs7-mime` message and `decrypt(message, keyMaterial, opts)` opens one — `smime-type=authEnveloped-data` (AES-GCM, confidentiality and integrity, the default) or `smime-type=enveloped-data` (AES-CBC, confidentiality only, so `decrypt` reports `authenticated: false`, the §3.3 no-integrity caveat); the `smime-type` is derived from the CMS body, not the header, and decryption is fail-closed and oracle-free. The crypto is entirely `pki.cms.sign` / `verify` / `encrypt` / `decrypt` — any RSA / RSASSA-PSS / ECDSA / EdDSA / ML-DSA / SLH-DSA signer and any RSA-OAEP / ECDH / X25519 / X448 / AES-KW / PBKDF2 / ML-KEM recipient carries through (algorithm-agnostic). Like `cms.verify`, `verify` returns the per-signer cryptographic verdict plus the recovered content; chaining a signer to a trust anchor is the caller's `pki.path.validate` step. `compress(content, opts)` / `decompress(message, opts)` add the opaque `application/pkcs7-mime; smime-type=compressed-data; name=smime.p7z` frame (RFC 8551 §3.6, RFC 3274) — a size transform with no integrity/confidentiality (§2.4.5), decompress bounded against a bomb; the recovered content, which may itself be signed or enveloped, is returned for the caller to re-verify. Bidirectionally interoperable with `openssl smime` / `openssl cms`. Fail-closed with typed `smime/*` errors — `sign`, `verify`, `encrypt`, `decrypt`, `compress`, `decompress` |
233
234
  | `pki.tsp` | RFC 3161 Time-Stamp Protocol — `sign(messageImprint, tsa, opts)` produces a TimeStampToken: a CMS SignedData (over `pki.cms.sign`) whose content is a `TSTInfo` carrying the timestamped message imprint, the TSA policy, a serial number, and `genTime` (with optional accuracy / nonce / ordering), plus the RFC 3161 §2.4.2 signing-certificate attribute binding the token to the TSA certificate (SHA-2 imprints, any `pki.cms.sign` TSA key). `request` / `parseRequest` build and parse the TimeStampReq a client sends (imprint, requested policy, nonce, certReq), `response` / `parseResponse` the TimeStampResp a TSA returns — a granted status wrapping a token, or a rejection with PKIStatus and failure info, the §2.4.2 status↔token coupling enforced in both directions. `verify(token, data, opts)` verifies a token fail-closed: the CMS signature over the exact signed bytes, the message imprint recomputed from the data, the TSTInfo content type, the ESSCertID(V2) binding to the TSA certificate, the §2.3 critical timeStamping-only extendedKeyUsage, the request nonce when used, and — with a trust anchor supplied — full certification-path validation of the TSA certificate at the token's `genTime`, returning `{ valid, genTime, serialNumber, tstInfo, … }` — `sign`, `request`, `parseRequest`, `response`, `parseResponse`, `verify` |
package/index.js CHANGED
@@ -46,6 +46,7 @@ var attrcert = require("./lib/attrcert-sign");
46
46
  var crmf = require("./lib/crmf-sign");
47
47
  var cmp = require("./lib/cmp-build");
48
48
  var crl = require("./lib/crl-sign");
49
+ var key = require("./lib/key");
49
50
  var merkle = require("./lib/merkle");
50
51
  var shbs = require("./lib/shbs");
51
52
  var hpke = require("./lib/hpke");
@@ -113,6 +114,10 @@ module.exports = {
113
114
  // over any registry algorithm, pki.crl.verify checks a CRL signature through the one path-validation
114
115
  // signature engine, and pki.crl.isRevoked looks a serial up. Parsing lives at pki.schema.crl.parse.
115
116
  crl: crl,
117
+ // `key` is the key-material lifecycle domain -- pki.key.encrypt / decrypt a private key under RFC 8018
118
+ // PBES2 (EncryptedPrivateKeyInfo), pki.key.export / import a PKCS#8 private or SPKI public key, and
119
+ // pki.key.generate / publicFromPrivate over every WebCrypto algorithm. Parsing lives at pki.schema.pkcs8.
120
+ key: key,
116
121
  // `merkle` is the RFC 6962 / RFC 9162 Merkle-tree proof-verification core --
117
122
  // pki.merkle.leafHash / nodeHash / emptyRootHash build the domain-separated
118
123
  // (0x00 leaf / 0x01 node) SHA-256 tree hashes; pki.merkle.verifyInclusion and
@@ -23,7 +23,7 @@ var schemaCms = require("./schema-cms");
23
23
  var webcrypto = require("./webcrypto");
24
24
  var frameworkError = require("./framework-error");
25
25
  var guard = require("./guard-all");
26
- var C = require("./constants");
26
+ var pbes2 = require("./pbes2");
27
27
  var b = asn1.build;
28
28
  var subtle = webcrypto.webcrypto.subtle;
29
29
  var CmsError = frameworkError.CmsError;
@@ -35,8 +35,7 @@ function _err(code, message, cause) { return new CmsError(code, message, cause);
35
35
  // The ONE uniform secret-dependent-failure verdict. No cause chaining that distinguishes the site.
36
36
  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)"); }
37
37
 
38
- var CONTENT_KEYBITS = {}; // content-encryption OID -> key bits
39
- [["aes128-CBC", 128], ["aes192-CBC", 192], ["aes256-CBC", 256], ["aes128-GCM", 128], ["aes192-GCM", 192], ["aes256-GCM", 256]].forEach(function (r) { CONTENT_KEYBITS[O(r[0])] = r[1]; });
38
+ var CONTENT_KEYBITS = pbes2.CONTENT_KEYBITS; // content-encryption OID -> key bits (the shared PBES2 table)
40
39
 
41
40
  // ---- entry -----------------------------------------------------------------
42
41
  async function decrypt(input, keyMaterial, opts) {
@@ -255,7 +254,7 @@ async function _pwriCek(ri, km, opts) {
255
254
  var kdf = ri.keyDerivationAlgorithm;
256
255
  if (!kdf) throw _err("cms/missing-key-derivation", "the pwri recipient has no keyDerivationAlgorithm (externally-supplied KEK is not supported)");
257
256
  if (kdf.oid !== O("pbkdf2")) throw _err("cms/unsupported-algorithm", "unsupported pwri key-derivation " + kdf.oid);
258
- var pb = _pbkdf2Params(kdf.parameters, opts);
257
+ var pb = pbes2.parsePbkdf2Params(kdf.parameters, opts, _err, "cms");
259
258
  var kea = ri.keyEncryptionAlgorithm;
260
259
  if (kea.oid !== O("id-alg-PWRI-KEK")) throw _err("cms/unsupported-algorithm", "unsupported pwri key-encryption " + kea.oid);
261
260
  var inner = asn1.decode(kea.parameters); // inner AES-CBC AlgorithmIdentifier
@@ -263,7 +262,7 @@ async function _pwriCek(ri, km, opts) {
263
262
  var innerBits = CONTENT_KEYBITS[innerOid];
264
263
  if (!innerBits || !/CBC/.test(oid.name(innerOid) || "")) throw _err("cms/unsupported-algorithm", "unsupported pwri inner cipher");
265
264
  var iv = asn1.read.octetString(inner.children[1]);
266
- var kek = nodeCrypto.pbkdf2Sync(_passwordBytes(km.password), pb.salt, pb.iterations, innerBits / 8, pb.prfNode);
265
+ var kek = nodeCrypto.pbkdf2Sync(pbes2.passwordBytes(km.password, _err, "cms"), pb.salt, pb.iterations, innerBits / 8, pb.prfNode);
267
266
  return _pwriUnwrap(kek, ri.encryptedKey, iv, innerBits);
268
267
  }
269
268
 
@@ -307,7 +306,7 @@ async function _openContent(parsed, eci, cek, ct) {
307
306
  }
308
307
  var iv = asn1.read.octetString(asn1.decode(alg.parameters));
309
308
  if (iv.length !== 16) throw _fail();
310
- return _cbcOpen(cek, iv, eci.encryptedContent, keyBits);
309
+ return pbes2.cbcDecrypt(cek, iv, eci.encryptedContent, keyBits);
311
310
  } catch (e) {
312
311
  if (e instanceof CmsError && e.code !== "cms/decrypt-failed") throw e;
313
312
  throw _fail();
@@ -322,10 +321,6 @@ function _gcmOpen(cek, nonce, ct, tag, aad, keyBits, icvLen) {
322
321
  if (aad && aad.length) d.setAAD(aad);
323
322
  return Buffer.concat([d.update(ct), d.final()]); // final() throws on tag mismatch -> uniform
324
323
  }
325
- function _cbcOpen(cek, iv, ct, keyBits) {
326
- var d = nodeCrypto.createDecipheriv("aes-" + keyBits + "-cbc", cek, iv);
327
- return Buffer.concat([d.update(ct), d.final()]); // final() throws on bad pad -> uniform
328
- }
329
324
 
330
325
  // ---- EncryptedData (sec. 8) ------------------------------------------------
331
326
  async function _decryptEncryptedData(parsed, km, opts) {
@@ -339,7 +334,7 @@ async function _decryptEncryptedData(parsed, km, opts) {
339
334
  var cek = guard.bytes.view(km.cek, CmsError, "cms/bad-input", "cek");
340
335
  if (cek.length !== keyBits / 8) throw _err("cms/bad-input", "the supplied cek length does not match the content algorithm");
341
336
  var iv = asn1.read.octetString(asn1.decode(alg.parameters));
342
- try { return { content: _cbcOpen(cek, iv, eci.encryptedContent, keyBits), contentType: eci.contentType, contentTypeName: oid.name(eci.contentType) || eci.contentType, recipientType: "cek", recipientIndex: -1, contentEncryptionAlgorithm: alg.name || alg.oid, authenticated: false }; }
337
+ try { return { content: pbes2.cbcDecrypt(cek, iv, eci.encryptedContent, keyBits), contentType: eci.contentType, contentTypeName: oid.name(eci.contentType) || eci.contentType, recipientType: "cek", recipientIndex: -1, contentEncryptionAlgorithm: alg.name || alg.oid, authenticated: false }; }
343
338
  catch (_e) { throw _fail(); }
344
339
  }
345
340
  async function _decryptPbes2(parsed, eci, km, opts) {
@@ -349,11 +344,11 @@ async function _decryptPbes2(parsed, eci, km, opts) {
349
344
  // cms/bad-input, never a raw dereference fault (the PBES2 structure is public -- not a decrypt oracle).
350
345
  var kdf, encOid, iv, pb;
351
346
  try {
352
- var params = _seqChildren(eci.contentEncryptionAlgorithm.parameters, 2, "PBES2 parameters");
353
- kdf = _requireChildren(params[0], 2, "PBES2 keyDerivationFunc");
354
- var encScheme = _requireChildren(params[1], 2, "PBES2 encryptionScheme");
347
+ var params = pbes2.seqChildren(eci.contentEncryptionAlgorithm.parameters, 2, "PBES2 parameters", _err, "cms");
348
+ kdf = pbes2.requireChildren(params[0], 2, "PBES2 keyDerivationFunc", _err, "cms");
349
+ var encScheme = pbes2.requireChildren(params[1], 2, "PBES2 encryptionScheme", _err, "cms");
355
350
  if (asn1.read.oid(kdf[0]) !== O("pbkdf2")) throw _err("cms/unsupported-algorithm", "PBES2 keyDerivationFunc must be PBKDF2");
356
- pb = _pbkdf2Params(kdf[1].bytes, opts);
351
+ pb = pbes2.parsePbkdf2Params(kdf[1].bytes, opts, _err, "cms");
357
352
  encOid = asn1.read.oid(encScheme[0]);
358
353
  iv = asn1.read.octetString(encScheme[1]);
359
354
  } catch (e) {
@@ -362,8 +357,8 @@ async function _decryptPbes2(parsed, eci, km, opts) {
362
357
  }
363
358
  var keyBits = CONTENT_KEYBITS[encOid];
364
359
  if (!keyBits) throw _err("cms/unsupported-algorithm", "unsupported PBES2 content cipher " + encOid);
365
- var key = nodeCrypto.pbkdf2Sync(_passwordBytes(km.password), pb.salt, pb.iterations, keyBits / 8, pb.prfNode);
366
- try { return { content: _cbcOpen(key, iv, eci.encryptedContent, keyBits), contentType: eci.contentType, contentTypeName: oid.name(eci.contentType) || eci.contentType, recipientType: "password", recipientIndex: -1, contentEncryptionAlgorithm: oid.name(encOid) || encOid, authenticated: false }; }
360
+ var key = nodeCrypto.pbkdf2Sync(pbes2.passwordBytes(km.password, _err, "cms"), pb.salt, pb.iterations, keyBits / 8, pb.prfNode);
361
+ try { return { content: pbes2.cbcDecrypt(key, iv, eci.encryptedContent, keyBits), contentType: eci.contentType, contentTypeName: oid.name(eci.contentType) || eci.contentType, recipientType: "password", recipientIndex: -1, contentEncryptionAlgorithm: oid.name(encOid) || encOid, authenticated: false }; }
367
362
  catch (_e) { throw _fail(); }
368
363
  }
369
364
 
@@ -410,44 +405,11 @@ function _pwriUnwrap(kek, wrapped, iv, keyBits) {
410
405
  if (bad !== 0) throw _fail();
411
406
  return Buffer.from(cek);
412
407
  }
413
- // SEQUENCE structural guards -- a malformed (primitive / too-short) attacker-controlled parameter
414
- // SEQUENCE is a typed cms/bad-input, never a raw `.children` dereference fault.
415
- function _requireChildren(node, minLen, what) {
416
- if (!node || !node.children || node.children.length < minLen) throw _err("cms/bad-input", "malformed " + what);
417
- return node.children;
418
- }
419
- function _seqChildren(paramsDer, minLen, what) {
420
- if (paramsDer == null) throw _err("cms/bad-input", "missing " + what);
421
- return _requireChildren(asn1.decode(paramsDer), minLen, what);
422
- }
423
- function _pbkdf2Params(paramsDer, opts) {
424
- var node = Buffer.isBuffer(paramsDer) ? asn1.decode(paramsDer) : paramsDer;
425
- var kids = _requireChildren(node, 2, "PBKDF2 parameters");
426
- var salt = asn1.read.octetString(kids[0]);
427
- if (salt.length > C.LIMITS.PBKDF2_MAX_SALT) throw _err("cms/bad-input", "PBKDF2 salt exceeds the " + C.LIMITS.PBKDF2_MAX_SALT + "-octet cap");
428
- var iterations = guard.range.positiveInt31(asn1.read.integer(kids[1]), _err, "cms/bad-input", "PBKDF2 iterationCount");
429
- // A caller-supplied maxIterations must be a positive integer -- a NaN / non-number would make
430
- // Math.min return NaN and silently disable the DoS cap (iterations > NaN is always false).
431
- var cap = C.LIMITS.PBKDF2_MAX_ITERATIONS;
432
- if (opts.maxIterations != null) {
433
- if (typeof opts.maxIterations !== "number" || !isFinite(opts.maxIterations) || opts.maxIterations < 1 || Math.floor(opts.maxIterations) !== opts.maxIterations) throw _err("cms/bad-input", "maxIterations must be a positive integer");
434
- cap = Math.min(opts.maxIterations, cap);
435
- }
436
- if (iterations > cap) throw _err("cms/iteration-limit", "PBKDF2 iterationCount " + iterations + " exceeds the cap " + cap);
437
- var prfNode = "sha1";
438
- for (var i = 2; i < node.children.length; i++) {
439
- var ch = node.children[i];
440
- if (ch.tagClass === "universal" && ch.tagNumber === asn1.TAGS.SEQUENCE) prfNode = _prfNode(asn1.read.oid(ch.children[0]));
441
- }
442
- return { salt: salt, iterations: iterations, prfNode: prfNode };
443
- }
444
- // Coverage residual (the unsupported-algorithm throw arm of each lookup below -- _prfNode, _hashW3c,
408
+ // Coverage residual (the unsupported-algorithm throw arm of each lookup below -- _hashW3c,
445
409
  // _x963Hash, and _originatorSpki that follow): reachable only from a fully well-formed recipient that
446
- // names an inner algorithm we do not implement (a non-registry prf / OAEP hash / kari scheme /
447
- // originator form). The producer never emits one; the fuzz harness (fuzz/cms-decrypt.fuzz.js) drives
448
- // these arms behaviorally under the PkiError-only contract.
449
- var PRF_NODE = {}; PRF_NODE[O("hmacWithSHA1")] = "sha1"; PRF_NODE[O("hmacWithSHA256")] = "sha256"; PRF_NODE[O("hmacWithSHA384")] = "sha384"; PRF_NODE[O("hmacWithSHA512")] = "sha512";
450
- function _prfNode(oidStr) { if (!PRF_NODE[oidStr]) throw _err("cms/unsupported-algorithm", "unsupported PBKDF2 prf " + oidStr); return PRF_NODE[oidStr]; }
410
+ // names an inner algorithm we do not implement (a non-registry OAEP hash / kari scheme / originator
411
+ // form). The producer never emits one; the fuzz harness (fuzz/cms-decrypt.fuzz.js) drives these arms
412
+ // behaviorally under the PkiError-only contract.
451
413
 
452
414
  function _oaepHashFromParams(paramsBytes) {
453
415
  if (paramsBytes == null) return "SHA-1"; // absent = the RFC 4055 defaults (accept floor)
@@ -529,11 +491,5 @@ function _normCertDer(cert) {
529
491
  if (typeof cert === "string") { try { return x509.pemDecode(cert); } catch (e) { throw _err("cms/bad-input", "the recipient certificate PEM could not be decoded", e); } }
530
492
  throw _err("cms/bad-input", "the recipient certificate must be a DER Buffer or PEM string");
531
493
  }
532
- function _passwordBytes(p) {
533
- if (Buffer.isBuffer(p)) return p;
534
- if (p instanceof Uint8Array) return Buffer.from(p);
535
- if (typeof p === "string") return Buffer.from(p, "utf8");
536
- throw _err("cms/bad-input", "a password must be a string, Buffer, or Uint8Array");
537
- }
538
494
 
539
495
  module.exports = { decrypt: decrypt };
@@ -23,7 +23,7 @@ var schemaCms = require("./schema-cms");
23
23
  var webcrypto = require("./webcrypto");
24
24
  var frameworkError = require("./framework-error");
25
25
  var guard = require("./guard-all");
26
- var C = require("./constants");
26
+ var pbes2 = require("./pbes2");
27
27
  var b = asn1.build;
28
28
  var subtle = webcrypto.webcrypto.subtle;
29
29
  var CmsError = frameworkError.CmsError;
@@ -230,25 +230,11 @@ async function _buildKekri(cek, desc) {
230
230
  }
231
231
 
232
232
  // A PBKDF2 iterationCount MUST be a positive integer within the same cap the decryptor enforces -- a
233
- // fractional / non-finite / <1 value would reach node's pbkdf2 (or BigInt() on encode) as a native
234
- // RangeError, and an over-cap value would produce a message our own decrypt refuses (cms/iteration-limit).
235
- function _assertIterations(n) {
236
- if (typeof n !== "number" || !isFinite(n) || n < 1 || Math.floor(n) !== n) throw _err("cms/bad-input", "iterations must be a positive integer");
237
- if (n > C.LIMITS.PBKDF2_MAX_ITERATIONS) throw _err("cms/bad-input", "iterations exceeds the " + C.LIMITS.PBKDF2_MAX_ITERATIONS + " cap");
238
- return n;
239
- }
240
- // Bound the PBKDF2 salt to the same cap the decryptor enforces, so a message we emit is always one we
241
- // can read back.
242
- function _assertSalt(salt) {
243
- guard.limits.byteCap(salt, C.LIMITS.PBKDF2_MAX_SALT, _err, "cms/bad-input", "salt");
244
- return salt;
245
- }
246
-
247
233
  // ---- pwri (password) : PBKDF2 + RFC 3211 double-CBC PWRI-KEK ---------------
248
234
  async function _buildPwri(cek, desc) {
249
- var password = _passwordBytes(desc.password);
250
- var iterations = _assertIterations(desc.iterations == null ? 600000 : desc.iterations);
251
- var salt = desc.salt ? _assertSalt(guard.bytes.view(desc.salt, CmsError, "cms/bad-input", "salt")) : nodeCrypto.randomBytes(16);
235
+ var password = pbes2.passwordBytes(desc.password, _err, "cms");
236
+ var iterations = pbes2.assertIterations(desc.iterations == null ? 600000 : desc.iterations, _err, "cms");
237
+ var salt = desc.salt ? pbes2.assertSalt(guard.bytes.view(desc.salt, CmsError, "cms/bad-input", "salt"), _err, "cms") : nodeCrypto.randomBytes(16);
252
238
  var prf = desc.prf || "hmacWithSHA256";
253
239
  var innerKeyBytes = 32; // AES-256-CBC inner
254
240
  var kekKey = await subtle.importKey("raw", password, { name: "PBKDF2" }, false, ["deriveBits"]);
@@ -258,7 +244,7 @@ async function _buildPwri(cek, desc) {
258
244
  var iv = nodeCrypto.randomBytes(16);
259
245
  var encryptedKey = _pwriWrapIv(kek, cek, iv);
260
246
  // PBKDF2-params as keyDerivationAlgorithm [0] IMPLICIT.
261
- var kdfParams = _pbkdf2ParamsSeq(salt, iterations, prf);
247
+ var kdfParams = pbes2.pbkdf2ParamsSeq(salt, iterations, prf);
262
248
  var kdfAlg = b.contextConstructed(0, Buffer.concat([b.oid(O("pbkdf2")), kdfParams]));
263
249
  var keyEncAlg = b.sequence([b.oid(O("id-alg-PWRI-KEK")), b.sequence([b.oid(O("aes256-CBC")), b.octetString(iv)])]);
264
250
  return { tag: 3, node: b.sequence([b.integer(0n), kdfAlg, keyEncAlg, b.octetString(encryptedKey)]) };
@@ -330,12 +316,6 @@ function _pwriWrapIv(kek, cek, iv) {
330
316
  return Buffer.concat([c2.update(pass1), c2.final()]);
331
317
  }
332
318
 
333
- function _passwordBytes(p) {
334
- if (Buffer.isBuffer(p)) return p;
335
- if (p instanceof Uint8Array) return Buffer.from(p);
336
- if (typeof p === "string") return Buffer.from(p, "utf8");
337
- throw _err("cms/bad-input", "a password must be a string, Buffer, or Uint8Array");
338
- }
339
319
  var PRF_HASH = { hmacWithSHA1: "SHA-1", hmacWithSHA256: "SHA-256", hmacWithSHA384: "SHA-384", hmacWithSHA512: "SHA-512" };
340
320
  function _prfHash(prf) { if (!PRF_HASH[prf]) throw _err("cms/bad-input", "unsupported pwri prf " + JSON.stringify(prf)); return PRF_HASH[prf]; }
341
321
 
@@ -401,7 +381,7 @@ function _emit(inner, ctName, opts) {
401
381
 
402
382
  function _envelopedData(contentBytes, cek, ca, contentType, riNodes, recips) {
403
383
  var iv = nodeCrypto.randomBytes(16);
404
- var enc = _cbcEncrypt(cek, iv, contentBytes, ca.keyBits);
384
+ var enc = pbes2.cbcEncrypt(cek, iv, contentBytes, ca.keyBits);
405
385
  var eci = b.sequence([b.oid(O(contentType)), b.sequence([b.oid(O(ca.oid)), b.octetString(iv)]), b.contextPrimitive(0, enc)]);
406
386
  return b.sequence([b.integer(BigInt(_envelopedVersion(recips, false))), b.setOf(riNodes), eci]);
407
387
  }
@@ -444,43 +424,26 @@ function _encryptedData(contentBytes, desc, ca, contentType, opts, cek) {
444
424
  } else {
445
425
  throw _err("cms/bad-input", "EncryptedData needs a single { cek } or { password } descriptor");
446
426
  }
447
- var enc = _cbcEncrypt(encKey, iv, contentBytes, ca.keyBits);
427
+ var enc = pbes2.cbcEncrypt(encKey, iv, contentBytes, ca.keyBits);
448
428
  var eci = b.sequence([b.oid(O(contentType)), contentAlgNode, b.contextPrimitive(0, enc)]);
449
429
  var inner = b.sequence([b.integer(0n), eci]);
450
430
  return _emit(inner, "encryptedData", opts);
451
431
  }
452
432
 
453
433
  function _encryptedDataPbes2(contentBytes, desc, ca, contentType, iv, opts) {
454
- var password = _passwordBytes(desc.password);
455
- var iterations = _assertIterations(desc.iterations == null ? 600000 : desc.iterations);
456
- var salt = desc.salt ? _assertSalt(guard.bytes.view(desc.salt, CmsError, "cms/bad-input", "salt")) : nodeCrypto.randomBytes(16);
434
+ var password = pbes2.passwordBytes(desc.password, _err, "cms");
435
+ var iterations = pbes2.assertIterations(desc.iterations == null ? 600000 : desc.iterations, _err, "cms");
436
+ var salt = desc.salt ? pbes2.assertSalt(guard.bytes.view(desc.salt, CmsError, "cms/bad-input", "salt"), _err, "cms") : nodeCrypto.randomBytes(16);
457
437
  var prf = desc.prf || "hmacWithSHA256";
458
- var key = nodeCrypto.pbkdf2Sync(password, salt, iterations, ca.keyBits / 8, _prfNode(prf));
459
- var enc = _cbcEncrypt(key, iv, contentBytes, ca.keyBits);
460
- var pbkdf2Params = _pbkdf2ParamsSeq(salt, iterations, prf);
461
- var kdf = b.sequence([b.oid(O("pbkdf2")), pbkdf2Params]);
462
- var encScheme = b.sequence([b.oid(O(ca.oid)), b.octetString(iv)]);
463
- var pbes2Params = b.sequence([kdf, encScheme]);
464
- var contentAlg = b.sequence([b.oid(O("pbes2")), pbes2Params]);
438
+ var key = nodeCrypto.pbkdf2Sync(password, salt, iterations, ca.keyBits / 8, pbes2.prfNodeByName(prf, _err, "cms"));
439
+ var enc = pbes2.cbcEncrypt(key, iv, contentBytes, ca.keyBits);
440
+ var contentAlg = pbes2.pbes2AlgId(salt, iterations, prf, ca.oid, iv);
465
441
  var eci = b.sequence([b.oid(O(contentType)), contentAlg, b.contextPrimitive(0, enc)]);
466
442
  var inner = b.sequence([b.integer(0n), eci]);
467
443
  return _emit(inner, "encryptedData", { pem: opts && opts.pem != null ? opts.pem : desc.pem });
468
444
  }
469
- var PRF_NODE = { hmacWithSHA1: "sha1", hmacWithSHA256: "sha256", hmacWithSHA384: "sha384", hmacWithSHA512: "sha512" };
470
- function _prfNode(prf) { if (!PRF_NODE[prf]) throw _err("cms/bad-input", "unsupported prf " + JSON.stringify(prf)); return PRF_NODE[prf]; }
471
- // PBKDF2-params { salt OCTET STRING, iterationCount, prf DEFAULT hmacWithSHA1 } -- one builder for
472
- // pwri + PBES2. prf equal to the DEFAULT (hmacWithSHA1) MUST be omitted (X.690 / RFC 8018 App. A.2).
473
- function _pbkdf2ParamsSeq(salt, iterations, prf) {
474
- var kids = [b.octetString(salt), b.integer(BigInt(iterations))];
475
- if (prf !== "hmacWithSHA1") kids.push(_algId(prf, "null"));
476
- return b.sequence(kids);
477
- }
478
445
 
479
446
  // ---- content-encryption primitives ----------------------------------------
480
- function _cbcEncrypt(key, iv, plaintext, keyBits) {
481
- var c = nodeCrypto.createCipheriv("aes-" + keyBits + "-cbc", key, iv);
482
- return Buffer.concat([c.update(plaintext), c.final()]);
483
- }
484
447
  function _gcmEncrypt(key, nonce, plaintext, aad, keyBits, tagLen) {
485
448
  var c = nodeCrypto.createCipheriv("aes-" + keyBits + "-gcm", key, nonce, { authTagLength: tagLen });
486
449
  if (aad && aad.length) c.setAAD(aad);
@@ -318,6 +318,15 @@ var WebauthnError = defineClass("WebauthnError", { withCause: true });
318
318
  // on malformed input -- a linter surveys a corpus that includes malformed members,
319
319
  // so hostile bytes surface a `fatal` finding (lint/unparseable) and return a report.
320
320
  var LintError = defineClass("LintError", { withCause: true });
321
+ // KeyError -- the pki.key key-material lifecycle domain: a bad export/import
322
+ // input (not a CryptoKey / DER / PEM), an unsupported key or PBES2 algorithm, a
323
+ // malformed EncryptedPrivateKeyInfo / PBES2 parameter set, an attacker-controlled
324
+ // PBKDF2 salt or iteration count over its cap, a version<=>publicKey mismatch on a
325
+ // produced OneAsymmetricKey, or a failed PBES2 decryption. A MAC-less PBES2-CBC
326
+ // decrypt collapses every post-derivation failure (wrong password, valid pad but
327
+ // not a PrivateKeyInfo) into ONE uniform key/decrypt-failed so it is not a padding
328
+ // oracle; the structural pre-derivation faults stay distinct and typed.
329
+ var KeyError = defineClass("KeyError", { withCause: true });
321
330
 
322
331
  module.exports = {
323
332
  PkiError: PkiError,
@@ -355,4 +364,5 @@ module.exports = {
355
364
  JoseError: JoseError,
356
365
  AcmeError: AcmeError,
357
366
  TrustError: TrustError,
367
+ KeyError: KeyError,
358
368
  };
package/lib/key.js ADDED
@@ -0,0 +1,420 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.key
6
+ * @nav Signing
7
+ * @title Keys
8
+ * @intro The key-material lifecycle: export / import a private key as PKCS#8 (`OneAsymmetricKey`,
9
+ * RFC 5958) or a public key as SPKI (RFC 5280 sec. 4.1.2.7), encrypt / decrypt a private key under
10
+ * RFC 8018 PBES2 (`EncryptedPrivateKeyInfo`, PBKDF2 + AES-CBC-Pad), and `generate` /
11
+ * `publicFromPrivate` over every algorithm the WebCrypto engine drives -- RSA, EC, Ed25519/Ed448,
12
+ * X25519/X448, and the FIPS post-quantum ML-DSA / ML-KEM. Unencrypted export / import DELEGATES to the
13
+ * WebCrypto `exportKey` / `importKey` PKCS#8 / SPKI encoders (which already emit each algorithm's
14
+ * `AlgorithmIdentifier.parameters` correctly -- RSA NULL, EC namedCurve, Ed/X ABSENT), so the wrapper
15
+ * never re-encodes an AlgorithmIdentifier. PBES2 encrypt / decrypt composes the one shared `lib/pbes2.js`
16
+ * home (the same PBKDF2 + AES-CBC primitives `pki.cms` uses). Parsing lives at `pki.schema.pkcs8.parse`.
17
+ * @spec RFC 5958, RFC 8018, RFC 5280
18
+ * @card Export / import PKCS#8 and SPKI keys and encrypt a private key under RFC 8018 PBES2.
19
+ */
20
+ //
21
+ // PBES2 is composed from lib/pbes2.js (the ONE PBKDF2 + AES-CBC home shared with pki.cms), bound to the key
22
+ // namespace through the `_err` error FACTORY + the "key" domain prefix so every reject keeps a key/* code.
23
+ // A MAC-less PBES2-CBC decrypt is not a padding oracle (RFC 8018 sec. 8): every post-derivation failure --
24
+ // a bad PKCS#7 pad OR a valid pad that does not re-parse as a PrivateKeyInfo -- collapses into ONE uniform
25
+ // key/decrypt-failed; the structural pre-derivation faults (a non-PBKDF2 KDF, a non-AES-CBC scheme, an
26
+ // over-cap salt / iteration count, a wrong-length IV, a malformed parameter SEQUENCE) stay distinct and are
27
+ // thrown BEFORE any key derivation, so they leak nothing password-dependent. Unencrypted export / import
28
+ // and publicFromPrivate DELEGATE to the WebCrypto / node key engines rather than re-serialize an
29
+ // AlgorithmIdentifier -- a re-encode is exactly where a params-absent-vs-NULL divergence sneaks back in.
30
+
31
+ var nodeCrypto = require("crypto");
32
+ var asn1 = require("./asn1-der");
33
+ var oid = require("./oid");
34
+ var pbes2 = require("./pbes2");
35
+ var pkcs8 = require("./schema-pkcs8");
36
+ var pkix = require("./schema-pkix");
37
+ var webcrypto = require("./webcrypto");
38
+ var frameworkError = require("./framework-error");
39
+ var guard = require("./guard-all");
40
+
41
+ var b = asn1.build;
42
+ var subtle = webcrypto.webcrypto.subtle;
43
+ var CryptoKey = webcrypto.CryptoKey;
44
+ var KeyError = frameworkError.KeyError;
45
+ var PemError = frameworkError.PemError;
46
+ function O(n) { return oid.byName(n); }
47
+ // The guard / pbes2 error convention: a (code, msg, cause) FACTORY, never the KeyError class -- pbes2 and
48
+ // the guards invoke it as E(code, msg) with no `new` (a class there crashes on the error path).
49
+ function _err(code, msg, cause) { return new KeyError(code, msg, cause); }
50
+
51
+ // The operator-facing node cipher name <-> the AES-CBC registry name the PBES2 home speaks.
52
+ var CIPHER_NAME = { "aes-128-cbc": "aes128-CBC", "aes-192-cbc": "aes192-CBC", "aes-256-cbc": "aes256-CBC" };
53
+
54
+ // import inference: the algorithm OIDs that name exactly ONE WebCrypto algorithm and need no parameters --
55
+ // the Edwards/Montgomery curves and the FIPS post-quantum ML-DSA, ML-KEM, and SLH-DSA (all signing-only or
56
+ // agreement-only, so unambiguous). RSA (sign vs OAEP) and EC (ECDSA vs ECDH) are deliberately ABSENT -- they
57
+ // are ambiguous, so import fails closed and asks for opts.algorithm rather than guess a plausible use.
58
+ var INFER_ALG = {};
59
+ ["Ed25519", "Ed448", "X25519", "X448"].forEach(function (n) { INFER_ALG[O(n)] = { name: n }; });
60
+ [["id-ml-dsa-44", "ML-DSA-44"], ["id-ml-dsa-65", "ML-DSA-65"], ["id-ml-dsa-87", "ML-DSA-87"],
61
+ ["id-ml-kem-512", "ML-KEM-512"], ["id-ml-kem-768", "ML-KEM-768"], ["id-ml-kem-1024", "ML-KEM-1024"]
62
+ ].forEach(function (r) { INFER_ALG[O(r[0])] = { name: r[1] }; });
63
+ // SLH-DSA (FIPS 205) -- all twelve parameter sets; the WebCrypto name is the OID name minus "id-", uppercased.
64
+ ["sha2-128s", "sha2-128f", "sha2-192s", "sha2-192f", "sha2-256s", "sha2-256f",
65
+ "shake-128s", "shake-128f", "shake-192s", "shake-192f", "shake-256s", "shake-256f"
66
+ ].forEach(function (s) { INFER_ALG[O("id-slh-dsa-" + s)] = { name: ("SLH-DSA-" + s).toUpperCase() }; });
67
+
68
+ function _isCryptoKey(x) { return x instanceof CryptoKey; }
69
+ function _algName(a) { return typeof a === "string" ? a : (a && a.name); }
70
+
71
+ // A private-key input (CryptoKey | DER Buffer | 'PRIVATE KEY' PEM) -> the raw PKCS#8 PrivateKeyInfo DER.
72
+ async function _toPrivateKeyDer(input) {
73
+ if (_isCryptoKey(input)) {
74
+ if (input.type !== "private") throw _err("key/bad-input", "a private CryptoKey is required (got a " + input.type + " key)");
75
+ return Buffer.from(await subtle.exportKey("pkcs8", input));
76
+ }
77
+ return pkix.coerceToDer(input, { pemLabel: "PRIVATE KEY", PemError: PemError, ErrorClass: KeyError, prefix: "key" });
78
+ }
79
+
80
+ /**
81
+ * @primitive pki.key.encrypt
82
+ * @signature pki.key.encrypt(privateKey, password, opts?) -> Promise<Buffer|string>
83
+ * @since 0.3.10
84
+ * @status experimental
85
+ * @spec RFC 5958 sec. 3, RFC 8018
86
+ * @related pki.key.decrypt, pki.schema.pkcs8.parseEncrypted, pki.cms.encrypt
87
+ *
88
+ * Encrypt a PKCS#8 private key into an RFC 5958 `EncryptedPrivateKeyInfo` under RFC 8018 PBES2 (PBKDF2 +
89
+ * AES-CBC-Pad). `privateKey` is a DER `Buffer`, a `PRIVATE KEY` PEM string, or an extractable private
90
+ * `CryptoKey`; `password` is a string (UTF-8-encoded, byte-identical to OpenSSL), `Buffer`, or `Uint8Array`.
91
+ * The plaintext is the DER `PrivateKeyInfo`, validated as a well-formed PKCS#8 structure before encryption
92
+ * (never encrypt opaque bytes), and the produced `EncryptedPrivateKeyInfo` is re-parsed before return.
93
+ *
94
+ * The PBKDF2 `prf` equal to the default (`hmacWithSHA1`) is omitted from the parameters (X.690 sec. 11.5),
95
+ * `keyLength` is omitted (the AES cipher OID fixes the key size), and the salt / iteration count are bounded
96
+ * -- so the output is byte-exact with OpenSSL's `pkcs8 -topk8 -v2`. A bad input throws a typed `KeyError`.
97
+ *
98
+ * @opts
99
+ * - `cipher` (string) -- `aes-256-cbc` (default), `aes-192-cbc`, or `aes-128-cbc`.
100
+ * - `prf` (string) -- `hmacWithSHA256` (default), `hmacWithSHA384`, `hmacWithSHA512`, or `hmacWithSHA1`.
101
+ * - `iterations` (number) -- PBKDF2 iteration count, default 600000 (bounded by the decryptor's cap).
102
+ * - `salt` (Buffer) -- an explicit PBKDF2 salt (default 16 random octets).
103
+ * - `pem` (boolean) -- return an `ENCRYPTED PRIVATE KEY` PEM string instead of DER.
104
+ * @example
105
+ * var pair = await pki.key.generate("Ed25519");
106
+ * var der = await pki.key.export(pair.privateKey);
107
+ * var enc = await pki.key.encrypt(der, "s3cr3t", { pem: true });
108
+ * var back = await pki.key.decrypt(enc, "s3cr3t");
109
+ */
110
+ async function encrypt(privateKey, password, opts) {
111
+ opts = opts || {};
112
+ var der = await _toPrivateKeyDer(privateKey);
113
+ // RFC 5958 sec. 3: the plaintext is the DER PrivateKeyInfo -- validate it IS one (the strict parser
114
+ // rejects BER / non-canonical / trailing bytes) before encrypting, so we never wrap opaque input.
115
+ try { pkcs8.parse(der); } catch (e) { throw _err("key/bad-input", "the private key is not a well-formed PKCS#8 PrivateKeyInfo", e); }
116
+ var cipherName = CIPHER_NAME[opts.cipher || "aes-256-cbc"];
117
+ if (!cipherName) throw _err("key/bad-input", "unsupported cipher " + JSON.stringify(opts.cipher) + " (aes-128-cbc / aes-192-cbc / aes-256-cbc)");
118
+ var keyBits = pbes2.CONTENT_KEYBITS[O(cipherName)];
119
+ var prf = opts.prf || "hmacWithSHA256";
120
+ var prfNode = pbes2.prfNodeByName(prf, _err, "key");
121
+ var iterations = pbes2.assertIterations(opts.iterations == null ? 600000 : opts.iterations, _err, "key");
122
+ var salt = opts.salt != null ? pbes2.assertSalt(guard.bytes.view(opts.salt, KeyError, "key/bad-input", "salt"), _err, "key") : nodeCrypto.randomBytes(16);
123
+ var iv = nodeCrypto.randomBytes(16);
124
+ var dk = nodeCrypto.pbkdf2Sync(pbes2.passwordBytes(password, _err, "key"), salt, iterations, keyBits / 8, prfNode);
125
+ var ciphertext = pbes2.cbcEncrypt(dk, iv, der, keyBits);
126
+ var epki = b.sequence([pbes2.pbes2AlgId(salt, iterations, prf, cipherName, iv), b.octetString(ciphertext)]);
127
+ pkcs8.parseEncrypted(epki); // self-check: the produced EncryptedPrivateKeyInfo re-parses
128
+ return opts.pem ? pkcs8.pemEncode(epki, "ENCRYPTED PRIVATE KEY") : epki;
129
+ }
130
+
131
+ /**
132
+ * @primitive pki.key.decrypt
133
+ * @signature pki.key.decrypt(encrypted, password, opts?) -> Promise<Buffer|string>
134
+ * @since 0.3.10
135
+ * @status experimental
136
+ * @spec RFC 5958 sec. 3, RFC 8018 sec. 6.2, RFC 8018 sec. 8
137
+ * @defends pbes2-padding-oracle (CWE-208), pbkdf2-work-dos (CWE-400)
138
+ * @related pki.key.encrypt, pki.schema.pkcs8.parse
139
+ *
140
+ * Decrypt an RFC 5958 `EncryptedPrivateKeyInfo` (DER `Buffer` or `ENCRYPTED PRIVATE KEY` PEM) under RFC 8018
141
+ * PBES2, returning the inner `PrivateKeyInfo` (re-validated via `pki.schema.pkcs8.parse`). Only PBES2 with a
142
+ * PBKDF2 key-derivation function and an AES-CBC encryption scheme is accepted; PBES1, PBMAC1, scrypt, and any
143
+ * other `encryptionAlgorithm` fail closed with `key/unsupported-algorithm`.
144
+ *
145
+ * The salt and iteration count are attacker-controlled work: both caps are enforced BEFORE any derivation
146
+ * (`opts.maxIterations` may lower the cap, never raise it), and a wrong-length IV or malformed parameter set
147
+ * is a typed `key/bad-algorithm-parameters`. A MAC-less PBES2-CBC decrypt is not a padding oracle -- a wrong
148
+ * password and a valid-pad-but-not-a-PrivateKeyInfo both surface the single uniform `key/decrypt-failed`.
149
+ *
150
+ * @opts
151
+ * - `maxIterations` (number) -- lower the PBKDF2 iteration cap for this decrypt (downward-only).
152
+ * - `pem` (boolean) -- return a `PRIVATE KEY` PEM string instead of DER.
153
+ * @example
154
+ * var pair = await pki.key.generate("Ed25519");
155
+ * var enc = await pki.key.encrypt(await pki.key.export(pair.privateKey), "s3cr3t", { pem: true });
156
+ * var der = await pki.key.decrypt(enc, "s3cr3t");
157
+ */
158
+ async function decrypt(encrypted, password, opts) {
159
+ opts = opts || {};
160
+ // opts.maxIterations is a CALLER authoring bound (a downward-only cap): validate it config-time so an
161
+ // invalid value is a key/bad-input, never a NaN that silently disables the DoS cap and never mis-coded as
162
+ // a fault in the attacker-controlled input parameters.
163
+ if (opts.maxIterations != null && (typeof opts.maxIterations !== "number" || !isFinite(opts.maxIterations) || opts.maxIterations < 1 || Math.floor(opts.maxIterations) !== opts.maxIterations)) {
164
+ throw _err("key/bad-input", "maxIterations must be a positive integer");
165
+ }
166
+ var input = pkix.coerceToDer(encrypted, { pemLabel: "ENCRYPTED PRIVATE KEY", PemError: PemError, ErrorClass: KeyError, prefix: "key" });
167
+ var epki;
168
+ try { epki = pkcs8.parseEncrypted(input); }
169
+ catch (e) { throw _err("key/bad-input", "the input is not a well-formed EncryptedPrivateKeyInfo", e); }
170
+ var encAlg = epki.encryptionAlgorithm;
171
+ if (encAlg.oid !== O("pbes2")) throw _err("key/unsupported-algorithm", "unsupported key encryption algorithm " + (encAlg.name || encAlg.oid) + " (only RFC 8018 PBES2 is supported)");
172
+ var plaintext = _decryptPbes2(encAlg, epki.encryptedData, password, opts);
173
+ return opts.pem ? pkcs8.pemEncode(plaintext, "PRIVATE KEY") : plaintext;
174
+ }
175
+
176
+ function _decryptPbes2(encAlg, ciphertext, password, opts) {
177
+ var pb, keyBits, iv;
178
+ // Structural parse (typed, pre-derivation). These faults are not password-dependent, so a distinct code
179
+ // per fault leaks no oracle; a raw asn1 fault is normalized to key/bad-algorithm-parameters.
180
+ try {
181
+ var params = pbes2.seqChildren(encAlg.parameters, 2, "PBES2 parameters", _err, "key");
182
+ var kdf = pbes2.requireChildren(params[0], 2, "PBES2 keyDerivationFunc", _err, "key");
183
+ var encScheme = pbes2.requireChildren(params[1], 2, "PBES2 encryptionScheme", _err, "key");
184
+ if (asn1.read.oid(kdf[0]) !== O("pbkdf2")) throw _err("key/unsupported-algorithm", "the PBES2 keyDerivationFunc must be PBKDF2 (RFC 8018 sec. 6.2)");
185
+ pb = pbes2.parsePbkdf2Params(kdf[1].bytes, opts, _err, "key", true); // strictPrf: reject a non-canonical explicit default prf
186
+ var encOid = asn1.read.oid(encScheme[0]);
187
+ keyBits = pbes2.CONTENT_KEYBITS[encOid];
188
+ if (!keyBits || !/CBC/.test(oid.name(encOid) || "")) throw _err("key/unsupported-algorithm", "unsupported PBES2 encryptionScheme " + (oid.name(encOid) || encOid) + " (AES-CBC only)");
189
+ iv = asn1.read.octetString(encScheme[1]);
190
+ if (iv.length !== 16) throw _err("key/bad-algorithm-parameters", "the AES-CBC IV must be 16 octets");
191
+ } catch (e) {
192
+ // The shared PBES2 home rejects a malformed / truncated / over-cap parameter with the generic
193
+ // key/bad-input; in the PBES2 parameter-structure context that IS a bad-algorithm-parameters. The
194
+ // semantic verdicts it raises (unsupported-algorithm, iteration-limit) keep their precise codes.
195
+ if (e instanceof KeyError) {
196
+ if (e.code === "key/bad-input") throw _err("key/bad-algorithm-parameters", e.message, e.cause);
197
+ throw e;
198
+ }
199
+ throw _err("key/bad-algorithm-parameters", "malformed PBES2 parameters", e);
200
+ }
201
+ var dk = nodeCrypto.pbkdf2Sync(pbes2.passwordBytes(password, _err, "key"), pb.salt, pb.iterations, keyBits / 8, pb.prfNode);
202
+ // Post-derivation failures collapse to ONE uniform verdict (RFC 8018 sec. 8): a bad PKCS#7 pad and a
203
+ // valid pad whose plaintext is not a PrivateKeyInfo are indistinguishable -- the re-parse is the integrity
204
+ // check for MAC-less PBES2-CBC, and it MUST run (never skipped).
205
+ var plaintext;
206
+ try { plaintext = pbes2.cbcDecrypt(dk, iv, ciphertext, keyBits); }
207
+ catch (_e) { throw _err("key/decrypt-failed", "decryption failed"); }
208
+ try { pkcs8.parse(plaintext); }
209
+ catch (_e) { throw _err("key/decrypt-failed", "decryption failed"); }
210
+ return plaintext;
211
+ }
212
+
213
+ /**
214
+ * @primitive pki.key.export
215
+ * @signature pki.key.export(key, opts?) -> Promise<Buffer|string>
216
+ * @since 0.3.10
217
+ * @status experimental
218
+ * @spec RFC 5958, RFC 5280 sec. 4.1.2.7, RFC 8410 sec. 3
219
+ * @related pki.key.import, pki.key.publicFromPrivate, pki.schema.pkcs8.parse
220
+ *
221
+ * Export an extractable `CryptoKey` to DER (or PEM): a private key as PKCS#8 `OneAsymmetricKey`, a public
222
+ * key as SubjectPublicKeyInfo. The encoding is delegated to the WebCrypto `exportKey` PKCS#8 / SPKI encoder,
223
+ * so the algorithm-specific `AlgorithmIdentifier.parameters` are byte-correct -- RSA carries an explicit
224
+ * NULL, EC a namedCurve OID, and Ed25519 / Ed448 / X25519 / X448 omit parameters (RFC 8410 sec. 3); the
225
+ * wrapper never re-encodes the AlgorithmIdentifier.
226
+ *
227
+ * @opts
228
+ * - `format` (string) -- `der` (default) or `pem`.
229
+ * - `label` (string) -- the PEM label (defaults `PRIVATE KEY` / `PUBLIC KEY` by key type).
230
+ * @example
231
+ * var pair = await pki.key.generate("Ed25519");
232
+ * var spkiPem = await pki.key.export(pair.publicKey, { format: "pem" });
233
+ */
234
+ async function export_(key, opts) {
235
+ opts = opts || {};
236
+ if (!_isCryptoKey(key)) throw _err("key/bad-input", "export expects a WebCrypto CryptoKey");
237
+ var format, defaultLabel;
238
+ if (key.type === "private") { format = "pkcs8"; defaultLabel = "PRIVATE KEY"; }
239
+ else if (key.type === "public") { format = "spki"; defaultLabel = "PUBLIC KEY"; }
240
+ else throw _err("key/bad-input", "export supports asymmetric (private / public) CryptoKeys only");
241
+ var der = Buffer.from(await subtle.exportKey(format, key));
242
+ var fmt = opts.format || "der";
243
+ if (fmt === "der") return der;
244
+ if (fmt === "pem") return pkix.pemEncode(der, opts.label || defaultLabel, PemError);
245
+ throw _err("key/bad-input", "unsupported format " + JSON.stringify(opts.format) + " (der / pem)");
246
+ }
247
+
248
+ /**
249
+ * @primitive pki.key.import
250
+ * @signature pki.key.import(input, opts?) -> Promise<CryptoKey>
251
+ * @since 0.3.10
252
+ * @status experimental
253
+ * @spec RFC 5958, RFC 5280 sec. 4.1.2.7, RFC 8018
254
+ * @related pki.key.export, pki.key.decrypt
255
+ *
256
+ * Import a DER / PEM PKCS#8 private key, SPKI public key, or (with `opts.password`) an `ENCRYPTED PRIVATE
257
+ * KEY` -- auto-detecting the structure -- into a `CryptoKey`. The WebCrypto algorithm is inferred from the
258
+ * key's OID for the algorithms that name exactly one (Ed25519 / Ed448 / X25519 / X448 / ML-DSA / ML-KEM /
259
+ * SLH-DSA); RSA and EC are ambiguous between signing and key agreement, so `opts.algorithm` must be supplied
260
+ * for them (import fails closed rather than guess a use). Default key usages follow the algorithm and key type.
261
+ *
262
+ * @opts
263
+ * - `algorithm` (string | object) -- the WebCrypto algorithm (required for RSA / EC; overrides inference).
264
+ * - `usages` (string[]) -- key usages (default derived from the algorithm and public/private type).
265
+ * - `extractable` (boolean) -- default false.
266
+ * - `password` (string | Buffer) -- decrypt an `ENCRYPTED PRIVATE KEY` first.
267
+ * @example
268
+ * var pair = await pki.key.generate("Ed25519");
269
+ * var spki = await pki.key.export(pair.publicKey);
270
+ * var pub = await pki.key.import(spki); // Ed25519 -- algorithm inferred
271
+ */
272
+ async function import_(input, opts) {
273
+ opts = opts || {};
274
+ var detected = _detectKeyInput(input);
275
+ if (detected.format === "encrypted") {
276
+ if (opts.password == null) throw _err("key/bad-input", "an ENCRYPTED PRIVATE KEY requires opts.password to import");
277
+ detected = { format: "pkcs8", der: await decrypt(detected.der, opts.password) };
278
+ }
279
+ var algorithm = opts.algorithm != null ? opts.algorithm : _inferAlgorithm(detected);
280
+ var isPublic = detected.format === "spki";
281
+ var usages = opts.usages || _importUsages(_algName(algorithm), isPublic);
282
+ var extractable = opts.extractable != null ? opts.extractable : false;
283
+ try {
284
+ return await subtle.importKey(detected.format, detected.der, algorithm, extractable, usages);
285
+ } catch (e) {
286
+ if (e && e.isPkiError) throw e;
287
+ throw _err("key/bad-input", "importKey failed", e);
288
+ }
289
+ }
290
+
291
+ /**
292
+ * @primitive pki.key.generate
293
+ * @signature pki.key.generate(algorithm, opts?) -> Promise<{ privateKey, publicKey }>
294
+ * @since 0.3.10
295
+ * @status experimental
296
+ * @spec W3C WebCrypto, FIPS 203, FIPS 204
297
+ * @related pki.key.export, pki.key.publicFromPrivate
298
+ *
299
+ * Generate an asymmetric key pair over the WebCrypto engine: RSA, ECDSA / ECDH, Ed25519 / Ed448, X25519 /
300
+ * X448, and the FIPS post-quantum ML-DSA / ML-KEM. `algorithm` is a WebCrypto algorithm string or object;
301
+ * usages default to the algorithm's natural set (sign/verify, deriveBits/deriveKey, or encapsulate/
302
+ * decapsulate) and keys are extractable by default. Returns the `{ privateKey, publicKey }` `CryptoKey` pair.
303
+ *
304
+ * @opts
305
+ * - `extractable` (boolean) -- default true.
306
+ * - `usages` (string[]) -- override the default key usages.
307
+ * @example
308
+ * var pair = await pki.key.generate("Ed25519");
309
+ * var pkcs8 = await pki.key.export(pair.privateKey);
310
+ */
311
+ async function generate(algorithm, opts) {
312
+ opts = opts || {};
313
+ var extractable = opts.extractable != null ? opts.extractable : true;
314
+ var usages = opts.usages || _generateUsages(_algName(algorithm));
315
+ var pair;
316
+ try { pair = await subtle.generateKey(algorithm, extractable, usages); }
317
+ catch (e) { if (e && e.isPkiError) throw e; throw _err("key/bad-input", "generateKey failed", e); }
318
+ if (!pair || !pair.privateKey || !pair.publicKey) throw _err("key/bad-input", "the algorithm does not generate an asymmetric key pair");
319
+ return { privateKey: pair.privateKey, publicKey: pair.publicKey };
320
+ }
321
+
322
+ /**
323
+ * @primitive pki.key.publicFromPrivate
324
+ * @signature pki.key.publicFromPrivate(privateKey, opts?) -> Promise<Buffer|string>
325
+ * @since 0.3.10
326
+ * @status experimental
327
+ * @spec RFC 5280 sec. 4.1.2.7, RFC 8410 sec. 3
328
+ * @related pki.key.export, pki.key.import
329
+ *
330
+ * Derive the SubjectPublicKeyInfo (SPKI) public key from a PKCS#8 private key (DER `Buffer`, `PRIVATE KEY`
331
+ * PEM, or extractable private `CryptoKey`). The derivation is delegated to the node key engine, which infers
332
+ * the algorithm from the key structure, so no `AlgorithmIdentifier` is re-encoded -- Ed25519 stays
333
+ * parameters-absent, RSA keeps its NULL, EC keeps its namedCurve.
334
+ *
335
+ * @opts
336
+ * - `pem` (boolean) -- return a `PUBLIC KEY` PEM string instead of DER.
337
+ * @example
338
+ * var pair = await pki.key.generate("Ed25519");
339
+ * var spki = await pki.key.publicFromPrivate(await pki.key.export(pair.privateKey));
340
+ */
341
+ async function publicFromPrivate(privateKey, opts) {
342
+ opts = opts || {};
343
+ var der = await _toPrivateKeyDer(privateKey);
344
+ var spki;
345
+ try {
346
+ var priv = nodeCrypto.createPrivateKey({ key: der, format: "der", type: "pkcs8" });
347
+ spki = nodeCrypto.createPublicKey(priv).export({ format: "der", type: "spki" });
348
+ } catch (e) { throw _err("key/bad-input", "could not derive the public key from the private key", e); }
349
+ return opts.pem ? pkix.pemEncode(spki, "PUBLIC KEY", PemError) : spki;
350
+ }
351
+
352
+ // ---- import helpers --------------------------------------------------------
353
+
354
+ // Auto-detect a key input by structure (works for a raw DER Buffer or a label-agnostic PEM): a PrivateKeyInfo
355
+ // leads with an INTEGER version; an SPKI is SEQ{ algId, BIT STRING }; an EncryptedPrivateKeyInfo is
356
+ // SEQ{ algId, OCTET STRING }. Returns { format, der }.
357
+ function _detectKeyInput(input) {
358
+ var der = pkix.coerceToDer(input, { pemLabel: null, PemError: PemError, ErrorClass: KeyError, prefix: "key" });
359
+ var root;
360
+ try { root = asn1.decode(der); } catch (e) { throw _err("key/bad-input", "the input is not DER or a PEM key", e); }
361
+ var kids = root.children || [];
362
+ if (kids.length >= 1 && kids[0].tagClass === "universal" && kids[0].tagNumber === asn1.TAGS.INTEGER) return { format: "pkcs8", der: der };
363
+ if (kids.length === 2 && kids[1].tagClass === "universal" && kids[1].tagNumber === asn1.TAGS.BIT_STRING) return { format: "spki", der: der };
364
+ if (kids.length === 2 && kids[1].tagClass === "universal" && kids[1].tagNumber === asn1.TAGS.OCTET_STRING) return { format: "encrypted", der: der };
365
+ throw _err("key/bad-input", "the input is not a recognized PKCS#8, SPKI, or EncryptedPrivateKeyInfo");
366
+ }
367
+
368
+ // The AlgorithmIdentifier OID from a detected key: privateKeyAlgorithm (PKCS#8) or algorithm (SPKI).
369
+ function _readAlgOid(detected) {
370
+ try {
371
+ if (detected.format === "pkcs8") return pkcs8.parse(detected.der).privateKeyAlgorithm.oid;
372
+ // SPKI: SEQUENCE { algorithm AlgorithmIdentifier SEQUENCE { OID, ... }, subjectPublicKey BIT STRING }.
373
+ // Validate the algorithm SEQUENCE shape before dereferencing its OID -- a malformed SPKI must be a typed
374
+ // reject, never a raw children[] fault.
375
+ var alg = asn1.decode(detected.der).children[0];
376
+ if (!alg || alg.tagClass !== "universal" || alg.tagNumber !== asn1.TAGS.SEQUENCE || !alg.children || !alg.children.length) {
377
+ throw _err("key/bad-input", "malformed SubjectPublicKeyInfo algorithm identifier");
378
+ }
379
+ return asn1.read.oid(alg.children[0]);
380
+ } catch (e) {
381
+ if (e instanceof KeyError) throw e;
382
+ throw _err("key/bad-input", "the key algorithm could not be read for inference", e);
383
+ }
384
+ }
385
+
386
+ function _inferAlgorithm(detected) {
387
+ var algOid = _readAlgOid(detected);
388
+ var a = INFER_ALG[algOid];
389
+ if (a) return a;
390
+ throw _err("key/unsupported-algorithm", "cannot infer the WebCrypto algorithm for " + (oid.name(algOid) || algOid) +
391
+ " (RSA and EC are ambiguous between signing and key agreement -- pass opts.algorithm)");
392
+ }
393
+
394
+ // Default key usages by algorithm CLASS. The name is normalized to upper case first: WebCrypto matches
395
+ // algorithm names case-insensitively, so a caller passing { name: "x25519" } / "ml-kem-768" must land on the
396
+ // same usage class as the canonical spelling rather than fall through to the signing default.
397
+ function _importUsages(name, isPublic) {
398
+ var n = String(name == null ? "" : name).toUpperCase();
399
+ if (n === "X25519" || n === "X448" || n === "ECDH") return isPublic ? [] : ["deriveBits", "deriveKey"];
400
+ if (/^ML-KEM/.test(n)) return isPublic ? ["encapsulateBits"] : ["decapsulateBits"];
401
+ if (n === "RSA-OAEP") return isPublic ? ["encrypt"] : ["decrypt"];
402
+ return isPublic ? ["verify"] : ["sign"]; // signing default (Ed / ECDSA / RSA-* sign / ML-DSA / SLH-DSA)
403
+ }
404
+
405
+ function _generateUsages(name) {
406
+ var n = String(name == null ? "" : name).toUpperCase();
407
+ if (n === "X25519" || n === "X448" || n === "ECDH") return ["deriveBits", "deriveKey"];
408
+ if (/^ML-KEM/.test(n)) return ["encapsulateBits", "decapsulateBits"];
409
+ if (n === "RSA-OAEP") return ["encrypt", "decrypt"];
410
+ return ["sign", "verify"];
411
+ }
412
+
413
+ module.exports = {
414
+ encrypt: encrypt,
415
+ decrypt: decrypt,
416
+ export: export_,
417
+ import: import_,
418
+ generate: generate,
419
+ publicFromPrivate: publicFromPrivate,
420
+ };
package/lib/pbes2.js ADDED
@@ -0,0 +1,136 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- the RFC 8018 PBES2 / PBKDF2 primitives, the ONE home shared by pki.cms EncryptedData / PWRI
6
+ // password encryption and pki.key EncryptedPrivateKeyInfo (RFC 5958). Error-factory-parameterized like the
7
+ // guard family: every reject goes through the CALLER's `E(code, msg, cause)` full-code factory plus a domain
8
+ // `prefix`, so pki.cms keeps cms/* codes and pki.key keeps key/* codes off ONE implementation. Extracted
9
+ // behavior-preservingly from cms-encrypt.js + cms-decrypt.js -- the shipped CMS PBES2 tests are the guard.
10
+ // A shape here that CANNOT route through a guard stays with its caller; the reusable PBES2 shape lives here
11
+ // so a fourth copy (the drift a reviewer flags) cannot re-accrete.
12
+
13
+ var nodeCrypto = require("crypto");
14
+ var asn1 = require("./asn1-der");
15
+ var oid = require("./oid");
16
+ var guard = require("./guard-all");
17
+ var C = require("./constants");
18
+
19
+ var b = asn1.build;
20
+ function O(n) { return oid.byName(n); }
21
+
22
+ // PBKDF2 prf tables: the OID name -> node digest (encrypt names the prf) and the OID -> node digest (decrypt
23
+ // reads the OID). One source, both directions.
24
+ var PRF_NODE_BY_NAME = { hmacWithSHA1: "sha1", hmacWithSHA256: "sha256", hmacWithSHA384: "sha384", hmacWithSHA512: "sha512" };
25
+ var PRF_NODE_BY_OID = {}; Object.keys(PRF_NODE_BY_NAME).forEach(function (n) { PRF_NODE_BY_OID[O(n)] = PRF_NODE_BY_NAME[n]; });
26
+
27
+ // content-encryption OID -> AES key bits (CBC + GCM). The PBES2 encryptionScheme + CMS content cipher table.
28
+ var CONTENT_KEYBITS = {};
29
+ [["aes128-CBC", 128], ["aes192-CBC", 192], ["aes256-CBC", 256], ["aes128-GCM", 128], ["aes192-GCM", 192], ["aes256-GCM", 256]].forEach(function (r) { CONTENT_KEYBITS[O(r[0])] = r[1]; });
30
+
31
+ // A password is an octet string (RFC 8018 sec. 2): a string is UTF-8-encoded deterministically (correct for
32
+ // non-ASCII, and byte-identical to OpenSSL), a Buffer/Uint8Array used verbatim.
33
+ function passwordBytes(p, E, prefix) {
34
+ if (Buffer.isBuffer(p)) return p;
35
+ if (p instanceof Uint8Array) return Buffer.from(p);
36
+ if (typeof p === "string") return Buffer.from(p, "utf8");
37
+ throw E(prefix + "/bad-input", "a password must be a string, Buffer, or Uint8Array");
38
+ }
39
+
40
+ // iterationCount for a PRODUCED PBES2/PWRI structure: a positive integer at or below the cap the decryptor
41
+ // enforces, so a structure we emit is always one we can read back (config-time throw on a bad value).
42
+ function assertIterations(n, E, prefix) {
43
+ if (typeof n !== "number" || !isFinite(n) || n < 1 || Math.floor(n) !== n) throw E(prefix + "/bad-input", "iterations must be a positive integer");
44
+ if (n > C.LIMITS.PBKDF2_MAX_ITERATIONS) throw E(prefix + "/bad-input", "iterations exceeds the " + C.LIMITS.PBKDF2_MAX_ITERATIONS + " cap");
45
+ return n;
46
+ }
47
+
48
+ // Bound a PRODUCED salt to the same cap the decryptor enforces.
49
+ function assertSalt(salt, E, prefix) {
50
+ guard.limits.byteCap(salt, C.LIMITS.PBKDF2_MAX_SALT, E, prefix + "/bad-input", "salt");
51
+ return salt;
52
+ }
53
+
54
+ function prfNodeByName(prf, E, prefix) { if (!PRF_NODE_BY_NAME[prf]) throw E(prefix + "/bad-input", "unsupported prf " + JSON.stringify(prf)); return PRF_NODE_BY_NAME[prf]; }
55
+ function prfNodeByOid(oidStr, E, prefix) { if (!PRF_NODE_BY_OID[oidStr]) throw E(prefix + "/unsupported-algorithm", "unsupported PBKDF2 prf " + oidStr); return PRF_NODE_BY_OID[oidStr]; }
56
+
57
+ // PBKDF2-params { salt OCTET STRING, iterationCount INTEGER, prf DEFAULT hmacWithSHA1 }. prf equal to the
58
+ // DEFAULT (hmacWithSHA1) MUST be omitted on encode (X.690 sec. 11.5 / RFC 8018 App. A.2); every other prf is
59
+ // an hmacWithSHA_n AlgorithmIdentifier carrying NULL parameters.
60
+ function pbkdf2ParamsSeq(salt, iterations, prf) {
61
+ var kids = [b.octetString(salt), b.integer(BigInt(iterations))];
62
+ if (prf !== "hmacWithSHA1") kids.push(b.sequence([b.oid(O(prf)), b.nullValue()]));
63
+ return b.sequence(kids);
64
+ }
65
+
66
+ // The PBES2 AlgorithmIdentifier: SEQUENCE { id-PBES2, SEQUENCE { keyDerivationFunc PBKDF2, encryptionScheme } }.
67
+ // The ONE home for the CMS EncryptedData contentEncryptionAlgorithm AND the RFC 5958 EncryptedPrivateKeyInfo
68
+ // encryptionAlgorithm -- both wrap the same PBES2 structure around a PBKDF2 KDF plus an AES-CBC scheme whose
69
+ // IV rides in the encryptionScheme parameter. `cipherName` is a registry NAME (an AES-CBC OID name).
70
+ function pbes2AlgId(salt, iterations, prf, cipherName, iv) {
71
+ var kdf = b.sequence([b.oid(O("pbkdf2")), pbkdf2ParamsSeq(salt, iterations, prf)]);
72
+ var encScheme = b.sequence([b.oid(O(cipherName)), b.octetString(iv)]);
73
+ return b.sequence([b.oid(O("pbes2")), b.sequence([kdf, encScheme])]);
74
+ }
75
+
76
+ // SEQUENCE structural guards -- a malformed (primitive / too-short) attacker-controlled parameter SEQUENCE is
77
+ // a typed E(prefix + "/bad-input"), never a raw `.children` dereference fault (a PBES2 structure is public,
78
+ // not a decrypt oracle).
79
+ function requireChildren(node, minLen, what, E, prefix) {
80
+ if (!node || !node.children || node.children.length < minLen) throw E(prefix + "/bad-input", "malformed " + what);
81
+ return node.children;
82
+ }
83
+ function seqChildren(paramsDer, minLen, what, E, prefix) {
84
+ if (paramsDer == null) throw E(prefix + "/bad-input", "missing " + what);
85
+ return requireChildren(asn1.decode(paramsDer), minLen, what, E, prefix);
86
+ }
87
+
88
+ // Strict PBKDF2-params decode: salt (capped BEFORE derivation), iterationCount (positiveInt31 + cap, downward-
89
+ // overridable via a validated-positive-int opts.maxIterations so Math.min(NaN, cap) cannot silently disable
90
+ // the DoS cap), prf. Returns { salt, iterations, prfNode }.
91
+ function parsePbkdf2Params(paramsDer, opts, E, prefix, strictPrf) {
92
+ var node = Buffer.isBuffer(paramsDer) ? asn1.decode(paramsDer) : paramsDer;
93
+ var kids = requireChildren(node, 2, "PBKDF2 parameters", E, prefix);
94
+ var salt = asn1.read.octetString(kids[0]);
95
+ if (salt.length > C.LIMITS.PBKDF2_MAX_SALT) throw E(prefix + "/bad-input", "PBKDF2 salt exceeds the " + C.LIMITS.PBKDF2_MAX_SALT + "-octet cap");
96
+ var iterations = guard.range.positiveInt31(asn1.read.integer(kids[1]), E, prefix + "/bad-input", "PBKDF2 iterationCount");
97
+ var cap = C.LIMITS.PBKDF2_MAX_ITERATIONS;
98
+ if (opts && opts.maxIterations != null) {
99
+ if (typeof opts.maxIterations !== "number" || !isFinite(opts.maxIterations) || opts.maxIterations < 1 || Math.floor(opts.maxIterations) !== opts.maxIterations) throw E(prefix + "/bad-input", "maxIterations must be a positive integer");
100
+ cap = Math.min(opts.maxIterations, cap);
101
+ }
102
+ if (iterations > cap) throw E(prefix + "/iteration-limit", "PBKDF2 iterationCount " + iterations + " exceeds the cap " + cap);
103
+ var prfNode = "sha1";
104
+ for (var i = 2; i < node.children.length; i++) {
105
+ var ch = node.children[i];
106
+ if (ch.tagClass === "universal" && ch.tagNumber === asn1.TAGS.SEQUENCE) {
107
+ var prfOid = asn1.read.oid(ch.children[0]);
108
+ // X.690 sec. 11.5 / RFC 8018 App. A.2: a prf equal to the DEFAULT (hmacWithSHA1 with NULL parameters)
109
+ // MUST be omitted -- an explicit encoding of it is non-canonical. A strictPrf caller rejects it.
110
+ if (strictPrf && prfOid === O("hmacWithSHA1") && ch.children.length > 1 && ch.children[1].tagClass === "universal" && ch.children[1].tagNumber === asn1.TAGS.NULL) {
111
+ throw E(prefix + "/bad-input", "a PBKDF2 prf equal to the default hmacWithSHA1 must be omitted (X.690 sec. 11.5)");
112
+ }
113
+ prfNode = prfNodeByOid(prfOid, E, prefix);
114
+ }
115
+ }
116
+ return { salt: salt, iterations: iterations, prfNode: prfNode };
117
+ }
118
+
119
+ // AES-CBC content encryption / decryption (PKCS#7 pad). decrypt's final() throws on bad pad -- the caller
120
+ // collapses that into its uniform decrypt-failed verdict.
121
+ function cbcEncrypt(key, iv, plaintext, keyBits) {
122
+ var c = nodeCrypto.createCipheriv("aes-" + keyBits + "-cbc", key, iv);
123
+ return Buffer.concat([c.update(plaintext), c.final()]);
124
+ }
125
+ function cbcDecrypt(key, iv, ct, keyBits) {
126
+ var d = nodeCrypto.createDecipheriv("aes-" + keyBits + "-cbc", key, iv);
127
+ return Buffer.concat([d.update(ct), d.final()]);
128
+ }
129
+
130
+ module.exports = {
131
+ passwordBytes: passwordBytes, assertIterations: assertIterations, assertSalt: assertSalt,
132
+ prfNodeByName: prfNodeByName, prfNodeByOid: prfNodeByOid,
133
+ pbkdf2ParamsSeq: pbkdf2ParamsSeq, pbes2AlgId: pbes2AlgId, parsePbkdf2Params: parsePbkdf2Params,
134
+ requireChildren: requireChildren, seqChildren: seqChildren,
135
+ cbcEncrypt: cbcEncrypt, cbcDecrypt: cbcDecrypt, CONTENT_KEYBITS: CONTENT_KEYBITS,
136
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:c2eb240a-1337-4a5a-862e-43dad1701426",
5
+ "serialNumber": "urn:uuid:0bc607cc-142c-4a49-a525-90a6f0216e38",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-23T16:52:13.594Z",
8
+ "timestamp": "2026-07-23T18:57:16.468Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/pki@0.3.9",
22
+ "bom-ref": "@blamejs/pki@0.3.10",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.3.9",
25
+ "version": "0.3.10",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
29
- "purl": "pkg:npm/%40blamejs/pki@0.3.9",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.3.10",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/pki@0.3.9",
57
+ "ref": "@blamejs/pki@0.3.10",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]