@blamejs/pki 0.3.9 → 0.3.11
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 +23 -0
- package/README.md +2 -0
- package/index.js +11 -0
- package/lib/asn1-der.js +14 -0
- package/lib/cmp-build.js +3 -28
- package/lib/cms-decrypt.js +16 -60
- package/lib/cms-encrypt.js +13 -50
- package/lib/framework-error.js +10 -0
- package/lib/key.js +420 -0
- package/lib/pbes2.js +184 -0
- package/lib/pkcs12-build.js +434 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ 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.11 — 2026-07-23
|
|
8
|
+
|
|
9
|
+
pki.pkcs12 builds and MAC-verifies password-integrity PKCS#12 (.p12/.pfx) stores (RFC 7292, RFC 9579).
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.pkcs12.build(spec, opts) builds a password-integrity PKCS#12 store. spec is { key, cert, ca?, friendlyName?, localKeyId? } (one PBES2-encrypted cert safe plus one shrouded-key safe) or { safeContents: [...] }, where each element is a plaintext or PBES2-encrypted SafeContents of key / shroudedKey / cert / crl / secret / nested safeContents bags; keys and certs are validated before wrapping. opts.mac selects a classic Appendix B HMAC (default) or an RFC 9579 PBMAC1 over SHA-256/384/512, or false for a MAC-less store; opts.password is the shared privacy + integrity password; opts.pem returns a PEM PKCS12 string. Shrouded keys and cert safes are encrypted under RFC 8018 PBES2 (AES-128/192/256-CBC). Passwords are BMPString+NULL encoded for the classic MAC and UTF-8 for the PBES2 bags and PBMAC1, so the output opens in OpenSSL and NSS; the MAC covers the exact AuthenticatedSafe byte range, a DEFAULT-1 MacData iterations and a <=160-bit PBMAC1 digest are rejected, and the store is re-parsed before return. RFC 7292, RFC 9579, RFC 8018.
|
|
14
|
+
- pki.pkcs12.verifyMac(pfx, password, opts) verifies a password-integrity store's MAC. pfx is a pki.schema.pkcs12.parse result, a DER Buffer, or a PEM string; the password is BMPString+NULL (classic) or UTF-8 (PBMAC1) encoded, the MAC is recomputed over the store's exact AuthenticatedSafe byte range with the store's own MAC parameters, and constant-time-compared to the stored value. Returns true / false for the password match; throws Pkcs12Error on a MAC-less or public-key-integrity store, or an unsupported MAC algorithm. RFC 7292 sec. 5.1, RFC 9579.
|
|
15
|
+
- pki.asn1.build.bmpString(str) encodes a JS string as a universal BMPString (UTF-16BE) TLV, rejecting an unpaired surrogate code point.
|
|
16
|
+
|
|
17
|
+
## v0.3.10 — 2026-07-23
|
|
18
|
+
|
|
19
|
+
pki.key exports, imports, and PBES2-encrypts private keys (RFC 5958, RFC 8018) over every WebCrypto algorithm.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- 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.
|
|
24
|
+
- 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.
|
|
25
|
+
- 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.
|
|
26
|
+
- 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.
|
|
27
|
+
- 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.
|
|
28
|
+
- 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).
|
|
29
|
+
|
|
7
30
|
## v0.3.9 — 2026-07-23
|
|
8
31
|
|
|
9
32
|
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,8 @@ 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` |
|
|
232
|
+
| `pki.pkcs12` | RFC 7292 / RFC 9579 PKCS#12 (.p12/.pfx) issuance — `build(spec, opts)` assembles a password-integrity store. `spec` is the OpenSSL-style `{ key, cert, ca?, friendlyName?, localKeyId? }` or the full `{ safeContents: [...] }`, where each element is a plaintext or PBES2-encrypted `SafeContents` of key / shroudedKey / cert / crl / secret / nested `safeContents` bags. Keys and certs are validated before wrapping; `friendlyName` (BMPString) and `localKeyId` attributes are single-value. The store is protected by a classic Appendix B HMAC (default, max interop) or an RFC 9579 PBMAC1 (`opts.mac.algorithm`), over SHA-256/384/512, with the shrouded keys and cert safes encrypted under RFC 8018 PBES2 (AES-128/192/256-CBC). Every password is encoded the PKCS#12 way — BMPString+NULL for the classic MAC, UTF-8 for the PBES2 bags and PBMAC1 (what OpenSSL and NSS consume) — so a file it emits opens in OpenSSL and NSS, cross-checked bidirectionally. The MAC is computed over the exact AuthenticatedSafe byte range, a DEFAULT-1 `MacData.iterations` is rejected up front, and the store is re-parsed before return. `verifyMac(pfx, password, opts)` recomputes a store's classic or PBMAC1 MAC over `macedBytes` and constant-time-compares it, throwing on a MAC-less or public-key-integrity store. Public-key integrity and bag decryption are not yet built. Returns DER or a PEM `PKCS12`; fail-closed with typed `Pkcs12Error`. Parsing stays at `pki.schema.pkcs12.parse` — `build` / `verifyMac` |
|
|
231
233
|
| `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
234
|
| `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
235
|
| `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,8 @@ 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");
|
|
50
|
+
var pkcs12 = require("./lib/pkcs12-build");
|
|
49
51
|
var merkle = require("./lib/merkle");
|
|
50
52
|
var shbs = require("./lib/shbs");
|
|
51
53
|
var hpke = require("./lib/hpke");
|
|
@@ -113,6 +115,15 @@ module.exports = {
|
|
|
113
115
|
// over any registry algorithm, pki.crl.verify checks a CRL signature through the one path-validation
|
|
114
116
|
// signature engine, and pki.crl.isRevoked looks a serial up. Parsing lives at pki.schema.crl.parse.
|
|
115
117
|
crl: crl,
|
|
118
|
+
// `key` is the key-material lifecycle domain -- pki.key.encrypt / decrypt a private key under RFC 8018
|
|
119
|
+
// PBES2 (EncryptedPrivateKeyInfo), pki.key.export / import a PKCS#8 private or SPKI public key, and
|
|
120
|
+
// pki.key.generate / publicFromPrivate over every WebCrypto algorithm. Parsing lives at pki.schema.pkcs8.
|
|
121
|
+
key: key,
|
|
122
|
+
// `pkcs12` is the RFC 7292 / RFC 9579 PKCS#12 (.p12/.pfx) producing side -- pki.pkcs12.build assembles a
|
|
123
|
+
// password-integrity store (key/cert/crl/secret bags in an AuthenticatedSafe, shrouded keys + cert safes
|
|
124
|
+
// under PBES2, a classic HMAC or PBMAC1 MAC), and pki.pkcs12.verifyMac checks a store's MAC. Parsing lives
|
|
125
|
+
// at pki.schema.pkcs12.parse.
|
|
126
|
+
pkcs12: pkcs12,
|
|
116
127
|
// `merkle` is the RFC 6962 / RFC 9162 Merkle-tree proof-verification core --
|
|
117
128
|
// pki.merkle.leafHash / nodeHash / emptyRootHash build the domain-separated
|
|
118
129
|
// (0x00 leaf / 0x01 node) SHA-256 tree hashes; pki.merkle.verifyInclusion and
|
package/lib/asn1-der.js
CHANGED
|
@@ -971,6 +971,20 @@ var build = {
|
|
|
971
971
|
if (!PRINTABLE_RE.test(s)) throw new Asn1Error("asn1/bad-printable-string", "value has characters outside the PrintableString set");
|
|
972
972
|
return _universal(TAGS.PRINTABLE_STRING, false, Buffer.from(s, "latin1"));
|
|
973
973
|
},
|
|
974
|
+
bmpString: function (s) {
|
|
975
|
+
// BMPString (UCS-2, X.680) encodes each character as a 2-byte big-endian BMP scalar value -- the exact
|
|
976
|
+
// inverse of _decodeUtf16be. A surrogate code unit (U+D800..U+DFFF), whether lone or half of a >U+FFFF
|
|
977
|
+
// pair, cannot be a BMPString character, so it is rejected rather than silently emitted.
|
|
978
|
+
s = String(s);
|
|
979
|
+
var out = Buffer.alloc(s.length * 2);
|
|
980
|
+
for (var i = 0; i < s.length; i++) {
|
|
981
|
+
var u = s.charCodeAt(i);
|
|
982
|
+
if (u >= 0xD800 && u <= 0xDFFF) throw new Asn1Error("asn1/bad-bmp-string", "BMPString cannot encode a surrogate code point (non-BMP characters are unsupported)");
|
|
983
|
+
out[i * 2] = (u >> 8) & 0xFF;
|
|
984
|
+
out[i * 2 + 1] = u & 0xFF;
|
|
985
|
+
}
|
|
986
|
+
return _universal(TAGS.BMP_STRING, false, out);
|
|
987
|
+
},
|
|
974
988
|
utcTime: function (date) { return _universal(TAGS.UTC_TIME, false, Buffer.from(_utcTimeString(date), "latin1")); },
|
|
975
989
|
generalizedTime: function (date) { return _universal(TAGS.GENERALIZED_TIME, false, Buffer.from(_generalizedTimeString(date), "latin1")); },
|
|
976
990
|
sequence: function (children) { return _universal(TAGS.SEQUENCE, true, Buffer.concat(_asBufferArray(children, "build.sequence"))); },
|
package/lib/cmp-build.js
CHANGED
|
@@ -38,6 +38,7 @@ var signScheme = require("./sign-scheme");
|
|
|
38
38
|
var pkix = require("./schema-pkix");
|
|
39
39
|
var pkiBuild = require("./pki-build");
|
|
40
40
|
var webcrypto = require("./webcrypto");
|
|
41
|
+
var pbes2 = require("./pbes2");
|
|
41
42
|
var constants = require("./constants");
|
|
42
43
|
var guard = require("./guard-all");
|
|
43
44
|
var frameworkError = require("./framework-error");
|
|
@@ -148,9 +149,6 @@ function _encodeGeneralInfo(itavs, code, what) {
|
|
|
148
149
|
|
|
149
150
|
// An AlgorithmIdentifier { algorithm OID, parameters ANY OPTIONAL } for a bare/absent-params digest.
|
|
150
151
|
function _algIdNoParams(name) { return b.sequence([b.oid(O(name))]); }
|
|
151
|
-
// An HMAC AlgorithmIdentifier carries NULL parameters (RFC 8018 App. B.1.1 / RFC 4231), unlike a bare SHA-2
|
|
152
|
-
// digest algId (ABSENT parameters, RFC 5754 sec. 2). Used for the PBMAC1 prf + messageAuthScheme.
|
|
153
|
-
function _hmacAlgId(name) { return b.sequence([b.oid(O(name)), b.nullValue()]); }
|
|
154
152
|
|
|
155
153
|
// PKIHeader ::= SEQUENCE { pvno INTEGER, sender GeneralName, recipient GeneralName, EXPLICIT [0..8] }.
|
|
156
154
|
// protectionAlgDer is the DERIVED AlgorithmIdentifier ([1]), present iff the message is protected. Returns
|
|
@@ -473,17 +471,6 @@ function _encodeBody(bodySpec, key, opts) {
|
|
|
473
471
|
|
|
474
472
|
// ---- protection ----
|
|
475
473
|
|
|
476
|
-
// PBMAC1 protectionAlg: id-PBMAC1 with PBMAC1-params { keyDerivationFunc PBKDF2{salt,iter,keyLength,prf},
|
|
477
|
-
// messageAuthScheme HMAC } -- the byte-exact inverse of schema-pkcs12.js PBMAC1_PARAMS / PBKDF2_PARAMS.
|
|
478
|
-
function _encodePbmac1AlgId(pd) {
|
|
479
|
-
var pbkdf2Params = [b.octetString(pd.salt), b.integer(BigInt(pd.iterationCount)), b.integer(BigInt(pd.keyLength))];
|
|
480
|
-
// prf: omit iff the DEFAULT algid-hmacWithSHA1; else the prf AlgorithmIdentifier with ABSENT params.
|
|
481
|
-
// nosemgrep: pki-non-constant-time-secret-compare -- prfName is a PRF algorithm name, not a secret
|
|
482
|
-
if (pd.prfName !== "hmacWithSHA1") pbkdf2Params.push(_hmacAlgId(pd.prfName));
|
|
483
|
-
var pbkdf2AlgId = b.sequence([b.oid(O("pbkdf2")), b.sequence(pbkdf2Params)]);
|
|
484
|
-
var pbmac1Params = b.sequence([pbkdf2AlgId, _hmacAlgId(pd.macName)]);
|
|
485
|
-
return b.sequence([b.oid(O("pbmac1")), pbmac1Params]);
|
|
486
|
-
}
|
|
487
474
|
|
|
488
475
|
// Resolve the protection selector to { protectionAlgDer, computeBits(protectedPartDer)->Promise<Buffer> },
|
|
489
476
|
// senderSpki, senderScheme } BEFORE the header is built (protectionAlg is opts-derived, not header-derived).
|
|
@@ -538,28 +525,16 @@ function _resolveProtection(opts) {
|
|
|
538
525
|
var macDesc = { salt: salt, iterationCount: iterationCount, keyLength: keyLength, prfName: PBMAC1_PRF[prf], macName: PBMAC1_MAC_OID[prf] };
|
|
539
526
|
|
|
540
527
|
return {
|
|
541
|
-
protectionAlgDer:
|
|
528
|
+
protectionAlgDer: pbes2.pbmac1AlgId(macDesc),
|
|
542
529
|
certDer: null,
|
|
543
530
|
computeBits: function (protectedPartDer) {
|
|
544
|
-
return
|
|
531
|
+
return pbes2.pbmac1(secretBuf, salt, iterationCount, keyLength, prf, prf, protectedPartDer).then(function (mac) {
|
|
545
532
|
return b.bitString(mac, 0);
|
|
546
533
|
});
|
|
547
534
|
},
|
|
548
535
|
};
|
|
549
536
|
}
|
|
550
537
|
|
|
551
|
-
// PBKDF2-derive an HMAC key from the shared secret, then HMAC the ProtectedPart (both via the engine).
|
|
552
|
-
function _pbmac1(secretBuf, salt, iterationCount, keyLength, prf, message) {
|
|
553
|
-
var subtle = webcrypto.webcrypto.subtle;
|
|
554
|
-
return subtle.importKey("raw", secretBuf, { name: "PBKDF2" }, false, ["deriveBits"]).then(function (baseKey) {
|
|
555
|
-
return subtle.deriveBits({ name: "PBKDF2", salt: salt, iterations: iterationCount, hash: prf }, baseKey, keyLength * 8);
|
|
556
|
-
}).then(function (bits) {
|
|
557
|
-
return subtle.importKey("raw", Buffer.from(bits), { name: "HMAC", hash: prf }, false, ["sign"]).then(function (hmacKey) {
|
|
558
|
-
return subtle.sign({ name: "HMAC" }, hmacKey, message);
|
|
559
|
-
});
|
|
560
|
-
}).then(function (sig) { return Buffer.from(sig); });
|
|
561
|
-
}
|
|
562
|
-
|
|
563
538
|
// ---- orchestrator ----
|
|
564
539
|
|
|
565
540
|
function build(message, opts) {
|
package/lib/cms-decrypt.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
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:
|
|
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 =
|
|
353
|
-
kdf =
|
|
354
|
-
var encScheme =
|
|
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 =
|
|
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(
|
|
366
|
-
try { return { content:
|
|
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
|
-
//
|
|
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
|
|
447
|
-
//
|
|
448
|
-
//
|
|
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 };
|
package/lib/cms-encrypt.js
CHANGED
|
@@ -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
|
|
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 =
|
|
250
|
-
var iterations =
|
|
251
|
-
var salt = desc.salt ?
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
455
|
-
var iterations =
|
|
456
|
-
var salt = desc.salt ?
|
|
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,
|
|
459
|
-
var enc =
|
|
460
|
-
var
|
|
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);
|
package/lib/framework-error.js
CHANGED
|
@@ -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
|
};
|