@blamejs/pki 0.2.5 → 0.2.7

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/lib/webcrypto.js CHANGED
@@ -353,6 +353,16 @@ SubtleCrypto.prototype.verify = async function verify(algorithm, key, signature,
353
353
  throw new WebCryptoError("webcrypto/not-supported", "verify: unsupported algorithm " + JSON.stringify(name));
354
354
  };
355
355
 
356
+ // Run a node:crypto AES cipher sequence, converting any raw node fault -- a bad IV length,
357
+ // a failed GCM authentication tag, bad CBC padding -- into a typed OperationError. A
358
+ // decrypt of tampered ciphertext (or a malformed cipher parameter) is a typed webcrypto
359
+ // verdict, never a raw Error crossing the public API (input prep that already throws a
360
+ // typed error runs OUTSIDE this, so a typed fault is never double-wrapped).
361
+ function _runCipher(fn, who) {
362
+ try { return fn(); }
363
+ catch (e) { throw new WebCryptoError("webcrypto/operation", who + ": AES cipher operation failed (bad key / iv / tag / padding)", e); }
364
+ }
365
+
356
366
  SubtleCrypto.prototype.encrypt = async function encrypt(algorithm, key, data) {
357
367
  var alg = _normalizeAlg(algorithm, "encrypt");
358
368
  _requireUsage(key, "encrypt");
@@ -369,19 +379,28 @@ SubtleCrypto.prototype.encrypt = async function encrypt(algorithm, key, data) {
369
379
  }
370
380
  if (name === "AES-GCM") {
371
381
  var iv = _toBuf(alg.iv, "AES-GCM iv");
372
- var cipher = nodeCrypto.createCipheriv("aes-" + key.algorithm.length + "-gcm", _secretBytes(key), iv, { authTagLength: (alg.tagLength || 128) / 8 });
373
- if (alg.additionalData) cipher.setAAD(_toBuf(alg.additionalData, "AES-GCM aad"));
374
- var ct = Buffer.concat([cipher.update(buf), cipher.final()]);
375
- return _toArrayBuffer(Buffer.concat([ct, cipher.getAuthTag()]));
382
+ var aad = alg.additionalData ? _toBuf(alg.additionalData, "AES-GCM aad") : null;
383
+ return _toArrayBuffer(_runCipher(function () {
384
+ var cipher = nodeCrypto.createCipheriv("aes-" + key.algorithm.length + "-gcm", _secretBytes(key), iv, { authTagLength: (alg.tagLength || 128) / 8 });
385
+ if (aad) cipher.setAAD(aad);
386
+ var ct = Buffer.concat([cipher.update(buf), cipher.final()]);
387
+ return Buffer.concat([ct, cipher.getAuthTag()]);
388
+ }, "encrypt"));
376
389
  }
377
390
  if (name === "AES-CBC") {
378
- var c2 = nodeCrypto.createCipheriv("aes-" + key.algorithm.length + "-cbc", _secretBytes(key), _toBuf(alg.iv, "AES-CBC iv"));
379
- return _toArrayBuffer(Buffer.concat([c2.update(buf), c2.final()]));
391
+ var cbcIv = _toBuf(alg.iv, "AES-CBC iv");
392
+ return _toArrayBuffer(_runCipher(function () {
393
+ var c2 = nodeCrypto.createCipheriv("aes-" + key.algorithm.length + "-cbc", _secretBytes(key), cbcIv);
394
+ return Buffer.concat([c2.update(buf), c2.final()]);
395
+ }, "encrypt"));
380
396
  }
381
397
  if (name === "AES-CTR") {
382
398
  _requireCtrLength128(alg);
383
- var c3 = nodeCrypto.createCipheriv("aes-" + key.algorithm.length + "-ctr", _secretBytes(key), _toBuf(alg.counter, "AES-CTR counter"));
384
- return _toArrayBuffer(Buffer.concat([c3.update(buf), c3.final()]));
399
+ var ctrCounter = _toBuf(alg.counter, "AES-CTR counter");
400
+ return _toArrayBuffer(_runCipher(function () {
401
+ var c3 = nodeCrypto.createCipheriv("aes-" + key.algorithm.length + "-ctr", _secretBytes(key), ctrCounter);
402
+ return Buffer.concat([c3.update(buf), c3.final()]);
403
+ }, "encrypt"));
385
404
  }
386
405
  throw new WebCryptoError("webcrypto/not-supported", "encrypt: unsupported algorithm " + JSON.stringify(name));
387
406
  };
@@ -403,21 +422,31 @@ SubtleCrypto.prototype.decrypt = async function decrypt(algorithm, key, data) {
403
422
  if (name === "AES-GCM") {
404
423
  var tagLen = (alg.tagLength || 128) / 8;
405
424
  var iv = _toBuf(alg.iv, "AES-GCM iv");
425
+ if (buf.length < tagLen) throw new WebCryptoError("webcrypto/operation", "decrypt: AES-GCM ciphertext is shorter than its authentication tag");
406
426
  var ct = buf.subarray(0, buf.length - tagLen);
407
427
  var tag = buf.subarray(buf.length - tagLen);
408
- var d = nodeCrypto.createDecipheriv("aes-" + key.algorithm.length + "-gcm", _secretBytes(key), iv, { authTagLength: tagLen });
409
- if (alg.additionalData) d.setAAD(_toBuf(alg.additionalData, "AES-GCM aad"));
410
- d.setAuthTag(tag);
411
- return _toArrayBuffer(Buffer.concat([d.update(ct), d.final()]));
428
+ var gcmAad = alg.additionalData ? _toBuf(alg.additionalData, "AES-GCM aad") : null;
429
+ return _toArrayBuffer(_runCipher(function () {
430
+ var d = nodeCrypto.createDecipheriv("aes-" + key.algorithm.length + "-gcm", _secretBytes(key), iv, { authTagLength: tagLen });
431
+ if (gcmAad) d.setAAD(gcmAad);
432
+ d.setAuthTag(tag);
433
+ return Buffer.concat([d.update(ct), d.final()]);
434
+ }, "decrypt"));
412
435
  }
413
436
  if (name === "AES-CBC") {
414
- var d2 = nodeCrypto.createDecipheriv("aes-" + key.algorithm.length + "-cbc", _secretBytes(key), _toBuf(alg.iv, "AES-CBC iv"));
415
- return _toArrayBuffer(Buffer.concat([d2.update(buf), d2.final()]));
437
+ var cbcIv2 = _toBuf(alg.iv, "AES-CBC iv");
438
+ return _toArrayBuffer(_runCipher(function () {
439
+ var d2 = nodeCrypto.createDecipheriv("aes-" + key.algorithm.length + "-cbc", _secretBytes(key), cbcIv2);
440
+ return Buffer.concat([d2.update(buf), d2.final()]);
441
+ }, "decrypt"));
416
442
  }
417
443
  if (name === "AES-CTR") {
418
444
  _requireCtrLength128(alg);
419
- var d3 = nodeCrypto.createDecipheriv("aes-" + key.algorithm.length + "-ctr", _secretBytes(key), _toBuf(alg.counter, "AES-CTR counter"));
420
- return _toArrayBuffer(Buffer.concat([d3.update(buf), d3.final()]));
445
+ var ctrCounter2 = _toBuf(alg.counter, "AES-CTR counter");
446
+ return _toArrayBuffer(_runCipher(function () {
447
+ var d3 = nodeCrypto.createDecipheriv("aes-" + key.algorithm.length + "-ctr", _secretBytes(key), ctrCounter2);
448
+ return Buffer.concat([d3.update(buf), d3.final()]);
449
+ }, "decrypt"));
421
450
  }
422
451
  throw new WebCryptoError("webcrypto/not-supported", "decrypt: unsupported algorithm " + JSON.stringify(name));
423
452
  };
@@ -526,8 +555,16 @@ SubtleCrypto.prototype.wrapKey = async function wrapKey(format, key, wrappingKey
526
555
  if (alg.name !== "AES-KW" && !ENCRYPT_DECRYPT_NAMES[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "wrapKey: unsupported algorithm " + JSON.stringify(alg.name));
527
556
  _requireAlgMatch(alg, wrappingKey, "wrapKey");
528
557
  if (alg.name === "AES-KW") {
529
- var c = nodeCrypto.createCipheriv("aes" + wrappingKey.algorithm.length + "-wrap", _secretBytes(wrappingKey), Buffer.from("A6A6A6A6A6A6A6A6", "hex"));
530
- return _toArrayBuffer(Buffer.concat([c.update(bytes), c.final()]));
558
+ // AES-KW wraps 64-bit blocks (RFC 3394): the serialized key MUST be a multiple of 8
559
+ // bytes and at least 16. A "jwk" serialization (arbitrary-length JSON) or any other
560
+ // non-conforming length is an OperationError -- a typed verdict, never a raw node fault.
561
+ if (bytes.length < 16 || bytes.length % 8 !== 0) {
562
+ throw new WebCryptoError("webcrypto/operation", "wrapKey: AES-KW requires the serialized key be a multiple of 8 bytes (>= 16); got " + bytes.length + " -- format " + JSON.stringify(format) + " is not AES-KW-wrappable");
563
+ }
564
+ try {
565
+ var c = nodeCrypto.createCipheriv("aes" + wrappingKey.algorithm.length + "-wrap", _secretBytes(wrappingKey), Buffer.from("A6A6A6A6A6A6A6A6", "hex"));
566
+ return _toArrayBuffer(Buffer.concat([c.update(bytes), c.final()]));
567
+ } catch (e) { throw new WebCryptoError("webcrypto/operation", "wrapKey: AES-KW key wrap failed", e); }
531
568
  }
532
569
  // Delegate to a content-encryption algorithm (RSA-OAEP / AES-GCM).
533
570
  var wrapKeyClone = _cloneWithUsage(wrappingKey, "encrypt");
@@ -546,8 +583,17 @@ SubtleCrypto.prototype.unwrapKey = async function unwrapKey(format, wrappedKey,
546
583
  _requireAlgMatch(alg, unwrappingKey, "unwrapKey");
547
584
  var bytes;
548
585
  if (alg.name === "AES-KW") {
549
- var d = nodeCrypto.createDecipheriv("aes" + unwrappingKey.algorithm.length + "-wrap", _secretBytes(unwrappingKey), Buffer.from("A6A6A6A6A6A6A6A6", "hex"));
550
- bytes = Buffer.concat([d.update(_toBuf(wrappedKey, "unwrapKey")), d.final()]);
586
+ var wrapped = _toBuf(wrappedKey, "unwrapKey");
587
+ // The wrapped input is the plaintext plus one 64-bit integrity block: a multiple of 8,
588
+ // at least 24. A wrong length, or a failed integrity check inside final(), is an
589
+ // OperationError -- a typed verdict, never a raw node cipher fault.
590
+ if (wrapped.length < 24 || wrapped.length % 8 !== 0) {
591
+ throw new WebCryptoError("webcrypto/operation", "unwrapKey: AES-KW wrapped key must be a multiple of 8 bytes (>= 24); got " + wrapped.length);
592
+ }
593
+ try {
594
+ var d = nodeCrypto.createDecipheriv("aes" + unwrappingKey.algorithm.length + "-wrap", _secretBytes(unwrappingKey), Buffer.from("A6A6A6A6A6A6A6A6", "hex"));
595
+ bytes = Buffer.concat([d.update(wrapped), d.final()]);
596
+ } catch (e) { throw new WebCryptoError("webcrypto/operation", "unwrapKey: AES-KW key unwrap failed (integrity or length)", e); }
551
597
  } else {
552
598
  var unwrapKeyClone = _cloneWithUsage(unwrappingKey, "decrypt");
553
599
  bytes = Buffer.from(await this.decrypt(unwrapAlgorithm, unwrapKeyClone, wrappedKey));
@@ -633,7 +679,28 @@ function _importRawPublic(name, alg, raw, extractable, usages) {
633
679
  throw new WebCryptoError("webcrypto/not-supported", "importKey raw: unsupported public-key algorithm " + JSON.stringify(name));
634
680
  }
635
681
 
682
+ // The node asymmetricKeyType(s) each imported asymmetric algorithm requires. The key TYPE
683
+ // is a property of the key material, not the caller's claim: an RSA key imported under
684
+ // {name:"Ed25519"} must be a DataError, not a CryptoKey mislabeled Ed25519 that then signs
685
+ // under the wrong scheme (algorithm confusion). EC is additionally curve-validated below.
686
+ var IMPORT_KEY_TYPE = {
687
+ "RSASSA-PKCS1-V1_5": { rsa: 1 }, "RSA-PSS": { rsa: 1, "rsa-pss": 1 }, "RSA-OAEP": { rsa: 1 },
688
+ "ECDSA": { ec: 1 }, "ECDH": { ec: 1 },
689
+ "ED25519": { ed25519: 1 }, "ED448": { ed448: 1 },
690
+ "X25519": { x25519: 1 }, "X448": { x448: 1 },
691
+ };
692
+ // The FIPS 203/204/205 PQC families import the same key-is-authority rule: each WebCrypto
693
+ // algorithm name maps to exactly its node asymmetricKeyType, so an RSA (or any) key imported
694
+ // under an ML-DSA / ML-KEM / SLH-DSA name is a DataError, not a mislabeled PQC CryptoKey.
695
+ [ML_DSA_NODE, ML_KEM_NODE, SLH_DSA_NODE].forEach(function (m) {
696
+ Object.keys(m).forEach(function (webName) { var t = {}; t[m[webName]] = 1; IMPORT_KEY_TYPE[webName] = t; });
697
+ });
636
698
  function _algFromImport(name, alg, keyObject) {
699
+ var wantType = IMPORT_KEY_TYPE[name];
700
+ if (wantType && keyObject && keyObject.asymmetricKeyType && !wantType[keyObject.asymmetricKeyType]) {
701
+ throw new WebCryptoError("webcrypto/data", name + ": the imported key is a " + keyObject.asymmetricKeyType +
702
+ " key, which is not compatible with algorithm " + name);
703
+ }
637
704
  if (name === "ECDSA" || name === "ECDH") {
638
705
  // W3C WebCrypto EC import -- the curve is a property of the KEY, not the
639
706
  // caller's claim. Derive it from the imported key material; reject an
@@ -658,7 +725,11 @@ function _algFromImport(name, alg, keyObject) {
658
725
  }
659
726
 
660
727
  function _curveFromKey(ko) {
661
- try { var jwk = ko.export({ format: "jwk" }); for (var k in CURVE_NODE) { if (jwk.crv === k) return k; } } catch (_e) { /* best-effort */ }
728
+ try {
729
+ var jwk = ko.export({ format: "jwk" });
730
+ for (var k in CURVE_NODE) { if (jwk.crv === k) return k; }
731
+ }
732
+ catch (_e) { /* best-effort */ }
662
733
  return undefined;
663
734
  }
664
735
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
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",
@@ -66,6 +66,8 @@
66
66
  "fuzz": "npm ci --prefix fuzz && npx --prefix fuzz jazzer fuzz/asn1-der.fuzz.js -- -max_total_time=60",
67
67
  "gates": "node test/layer-0-primitives/codebase-patterns.test.js && node scripts/validate-source-comment-blocks.js && node scripts/check-api-snapshot.js",
68
68
  "coverage": "c8 --include=lib/** --include=index.js --reporter=text-summary --reporter=lcov node test/smoke.js",
69
+ "check:swallows": "node scripts/check-swallow-coverage.js",
70
+ "coverage:gated": "npm run coverage && npm run check:swallows",
69
71
  "prepack": "node scripts/check-pack-against-gitignore.js",
70
72
  "check:vendor-currency": "node scripts/check-vendor-currency.js"
71
73
  },
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:0c805130-1ce6-499f-b389-67252a2e8b93",
5
+ "serialNumber": "urn:uuid:ea60e88f-9354-49c4-aa35-ad6712102de1",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-13T00:36:23.753Z",
8
+ "timestamp": "2026-07-13T07:29:25.087Z",
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.2.5",
22
+ "bom-ref": "@blamejs/pki@0.2.7",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.2.5",
25
+ "version": "0.2.7",
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.2.5",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.2.7",
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.2.5",
57
+ "ref": "@blamejs/pki@0.2.7",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]