@blamejs/core 0.9.49 → 0.10.1

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.
Files changed (77) hide show
  1. package/CHANGELOG.md +951 -908
  2. package/index.js +25 -0
  3. package/lib/_test/crypto-fixtures.js +67 -0
  4. package/lib/agent-event-bus.js +52 -6
  5. package/lib/agent-idempotency.js +169 -16
  6. package/lib/agent-orchestrator.js +263 -9
  7. package/lib/agent-posture-chain.js +163 -5
  8. package/lib/agent-saga.js +146 -16
  9. package/lib/agent-snapshot.js +349 -19
  10. package/lib/agent-stream.js +34 -2
  11. package/lib/agent-tenant.js +179 -23
  12. package/lib/agent-trace.js +84 -21
  13. package/lib/auth/aal.js +8 -1
  14. package/lib/auth/ciba.js +6 -1
  15. package/lib/auth/dpop.js +7 -2
  16. package/lib/auth/fal.js +17 -8
  17. package/lib/auth/jwt-external.js +128 -4
  18. package/lib/auth/oauth.js +232 -10
  19. package/lib/auth/oid4vci.js +67 -7
  20. package/lib/auth/openid-federation.js +71 -25
  21. package/lib/auth/passkey.js +140 -6
  22. package/lib/auth/sd-jwt-vc.js +67 -5
  23. package/lib/circuit-breaker.js +10 -2
  24. package/lib/compliance.js +176 -8
  25. package/lib/crypto-field.js +114 -14
  26. package/lib/crypto.js +216 -20
  27. package/lib/db.js +1 -0
  28. package/lib/guard-jmap.js +321 -0
  29. package/lib/guard-managesieve-command.js +566 -0
  30. package/lib/guard-pop3-command.js +317 -0
  31. package/lib/guard-smtp-command.js +58 -3
  32. package/lib/mail-agent.js +20 -7
  33. package/lib/mail-arc-sign.js +12 -8
  34. package/lib/mail-auth.js +323 -34
  35. package/lib/mail-crypto-pgp.js +934 -0
  36. package/lib/mail-crypto-smime.js +340 -0
  37. package/lib/mail-crypto.js +108 -0
  38. package/lib/mail-dav.js +1224 -0
  39. package/lib/mail-deploy.js +492 -0
  40. package/lib/mail-dkim.js +431 -26
  41. package/lib/mail-journal.js +435 -0
  42. package/lib/mail-scan.js +502 -0
  43. package/lib/mail-server-imap.js +64 -26
  44. package/lib/mail-server-jmap.js +488 -0
  45. package/lib/mail-server-managesieve.js +853 -0
  46. package/lib/mail-server-mx.js +40 -30
  47. package/lib/mail-server-pop3.js +836 -0
  48. package/lib/mail-server-rate-limit.js +13 -0
  49. package/lib/mail-server-submission.js +70 -24
  50. package/lib/mail-server-tls.js +445 -0
  51. package/lib/mail-sieve.js +557 -0
  52. package/lib/mail-spam-score.js +284 -0
  53. package/lib/mail.js +99 -0
  54. package/lib/metrics.js +80 -3
  55. package/lib/middleware/dpop.js +58 -3
  56. package/lib/middleware/idempotency-key.js +255 -42
  57. package/lib/middleware/protected-resource-metadata.js +114 -2
  58. package/lib/network-dns-resolver.js +33 -0
  59. package/lib/network-tls.js +46 -0
  60. package/lib/outbox.js +62 -12
  61. package/lib/pqc-agent.js +13 -5
  62. package/lib/retry.js +23 -9
  63. package/lib/router.js +23 -1
  64. package/lib/safe-ical.js +634 -0
  65. package/lib/safe-icap.js +502 -0
  66. package/lib/safe-mime.js +15 -0
  67. package/lib/safe-sieve.js +684 -0
  68. package/lib/safe-smtp.js +57 -0
  69. package/lib/safe-url.js +37 -0
  70. package/lib/safe-vcard.js +473 -0
  71. package/lib/self-update-standalone-verifier.js +32 -3
  72. package/lib/self-update.js +153 -33
  73. package/lib/vendor/MANIFEST.json +161 -156
  74. package/lib/vendor-data.js +127 -9
  75. package/lib/vex.js +324 -59
  76. package/package.json +1 -1
  77. package/sbom.cdx.json +6 -6
package/lib/crypto.js CHANGED
@@ -49,6 +49,12 @@ var nodeFs = require("node:fs");
49
49
  var { pipeline } = require("node:stream/promises");
50
50
  var { xchacha20poly1305 } = require("./vendor/noble-ciphers.cjs");
51
51
  var C = require("./constants");
52
+ // Circular: audit imports b.crypto for sha3Hash + envelope sign. Lazy-
53
+ // load the audit module so the legacy-envelope decrypt path can emit
54
+ // `system.crypto.decrypt.allow_legacy` events without an inline
55
+ // require() inside setImmediate (top-of-file requires per rule §3).
56
+ var lazyRequire = require("./lazy-require");
57
+ var audit = lazyRequire(function () { return require("./audit"); });
52
58
 
53
59
  // Streaming-hash algorithm allowlist. Mirrors the framework's PQC-
54
60
  // first crypto policy: SHA3 / SHAKE family is the default surface;
@@ -151,8 +157,50 @@ function hashFile(filePath, algorithm) {
151
157
  // parallel. Returns { path, byteLength, <alg>: hex } for every
152
158
  // `algorithms` entry. Used by hashFilesParallel below; not exported
153
159
  // directly because the common case is the parallel-many shape.
154
- function _hashFileMulti(filePath, algorithms) {
160
+ //
161
+ // CRYPTO-5 hardening (v0.9.58):
162
+ // - lstat-then-stat so symlinks are detected before open; refused
163
+ // unless opts.followSymlinks === true (default false — a symlink-
164
+ // in-input-list attack lets a write-restricted caller hash files
165
+ // they can't otherwise reach).
166
+ // - Refuses non-regular files (FIFOs / sockets / block / char
167
+ // devices) which read indefinitely or return platform-undefined
168
+ // bytes. The same path also defeats /dev/zero-as-input DoS.
169
+ // - opts.maxBytesPerFile (default C.BYTES.gib(1)) caps the bytes
170
+ // read per file; oversized inputs reject before exhausting heap.
171
+ function _hashFileMulti(filePath, algorithms, opts) {
172
+ var maxBytes = opts && opts.maxBytesPerFile;
173
+ var followSymlink = opts && opts.followSymlinks === true;
155
174
  return new Promise(function (resolve, reject) {
175
+ // Pre-open lstat: detect symlinks + special files before opening the
176
+ // stream. Open-then-stat is racy under symlink-swap; lstat the path
177
+ // itself ahead of createReadStream.
178
+ var st;
179
+ try { st = nodeFs.lstatSync(filePath); }
180
+ catch (statErr) {
181
+ reject(new Error("crypto.hashFilesParallel: stat failed for '" +
182
+ filePath + "': " + (statErr && statErr.message ? statErr.message : String(statErr))));
183
+ return;
184
+ }
185
+ if (st.isSymbolicLink() && !followSymlink) {
186
+ reject(new Error("crypto.hashFilesParallel: refusing symlink '" +
187
+ filePath + "' — pass {followSymlinks: true} to opt in (an attacker " +
188
+ "with write access to the input list can otherwise direct the hasher " +
189
+ "to files the caller cannot read directly)"));
190
+ return;
191
+ }
192
+ if (!st.isFile() && !st.isSymbolicLink()) {
193
+ reject(new Error("crypto.hashFilesParallel: refusing non-regular file '" +
194
+ filePath + "' (FIFOs / sockets / character / block devices read indefinitely " +
195
+ "or return platform-undefined bytes; hashing them is meaningless and " +
196
+ "DoS-prone). Type: " +
197
+ (st.isFIFO() ? "FIFO" :
198
+ st.isSocket() ? "socket" :
199
+ st.isBlockDevice() ? "block-device" :
200
+ st.isCharacterDevice() ? "char-device" :
201
+ st.isDirectory() ? "directory" : "unknown")));
202
+ return;
203
+ }
156
204
  var hashers = new Array(algorithms.length);
157
205
  for (var i = 0; i < algorithms.length; i += 1) {
158
206
  try { hashers[i] = nodeCrypto.createHash(algorithms[i]); }
@@ -163,13 +211,24 @@ function _hashFileMulti(filePath, algorithms) {
163
211
  }
164
212
  }
165
213
  var byteLength = 0;
214
+ var aborted = false;
166
215
  var stream = nodeFs.createReadStream(filePath);
167
216
  stream.on("error", reject);
168
217
  stream.on("data", function (chunk) {
218
+ if (aborted) return;
169
219
  byteLength += chunk.length;
220
+ if (maxBytes && byteLength > maxBytes) {
221
+ aborted = true;
222
+ try { stream.destroy(); } catch (_e) { /* best-effort */ }
223
+ reject(new Error("crypto.hashFilesParallel: file '" + filePath +
224
+ "' exceeded opts.maxBytesPerFile (" + maxBytes +
225
+ " bytes); refusing to continue hashing"));
226
+ return;
227
+ }
170
228
  for (var j = 0; j < hashers.length; j += 1) hashers[j].update(chunk);
171
229
  });
172
230
  stream.on("end", function () {
231
+ if (aborted) return;
173
232
  var out = { path: filePath, byteLength: byteLength };
174
233
  for (var k = 0; k < hashers.length; k += 1) {
175
234
  // Field name = algorithm with `-` → `_` so "sha3-512" surfaces
@@ -205,9 +264,11 @@ function _hashFileMulti(filePath, algorithms) {
205
264
  * every release.
206
265
  *
207
266
  * @opts
208
- * algorithms?: string[], // default ["sha256", "sha3-512"]; any node:crypto-known digest
209
- * concurrency?: number, // default min(8, filePaths.length); 1..256
210
- * onProgress?: function (completed, total) // best-effort; thrown errors swallowed
267
+ * algorithms?: string[], // default ["sha256", "sha3-512"]; any node:crypto-known digest
268
+ * concurrency?: number, // default min(8, filePaths.length); 1..256
269
+ * onProgress?: function (completed, total) // best-effort; thrown errors swallowed
270
+ * maxBytesPerFile?: number, // default C.BYTES.gib(1) — DoS cap; oversized inputs reject
271
+ * followSymlinks?: boolean, // default false — refuse symlinks unless explicitly opted in
211
272
  *
212
273
  * @example
213
274
  * var rows = await b.crypto.hashFilesParallel(
@@ -262,8 +323,24 @@ function hashFilesParallel(filePaths, opts) {
262
323
  "crypto.hashFilesParallel: opts.onProgress must be a function when supplied"
263
324
  ));
264
325
  }
326
+ // CRYPTO-5 — DoS cap. Default 1 GiB per file; operators with larger
327
+ // legitimate hashing workloads (firmware images, vendor packs)
328
+ // override per-call.
329
+ var maxBytesPerFile = opts.maxBytesPerFile !== undefined
330
+ ? opts.maxBytesPerFile : C.BYTES.gib(1);
331
+ if (typeof maxBytesPerFile !== "number" || !isFinite(maxBytesPerFile) ||
332
+ maxBytesPerFile <= 0 || Math.floor(maxBytesPerFile) !== maxBytesPerFile) {
333
+ return Promise.reject(new TypeError(
334
+ "crypto.hashFilesParallel: opts.maxBytesPerFile must be a positive integer, got " + maxBytesPerFile
335
+ ));
336
+ }
337
+ var followSymlinks = opts.followSymlinks === true;
265
338
  if (filePaths.length === 0) return Promise.resolve([]);
266
339
 
340
+ var hashOpts = {
341
+ maxBytesPerFile: maxBytesPerFile,
342
+ followSymlinks: followSymlinks,
343
+ };
267
344
  var results = new Array(filePaths.length);
268
345
  var nextIdx = 0;
269
346
  var completed = 0;
@@ -273,7 +350,7 @@ function hashFilesParallel(filePaths, opts) {
273
350
  var idx = nextIdx;
274
351
  nextIdx += 1;
275
352
  if (idx >= total) return Promise.resolve();
276
- return _hashFileMulti(filePaths[idx], algorithms).then(function (rec) {
353
+ return _hashFileMulti(filePaths[idx], algorithms, hashOpts).then(function (rec) {
277
354
  results[idx] = rec;
278
355
  completed += 1;
279
356
  if (onProgress) {
@@ -612,7 +689,7 @@ function toBase64Url(buf) {
612
689
 
613
690
  /**
614
691
  * @primitive b.crypto.fromBase64Url
615
- * @signature b.crypto.fromBase64Url(s)
692
+ * @signature b.crypto.fromBase64Url(s, opts?)
616
693
  * @since 0.9.45
617
694
  * @status stable
618
695
  * @related b.crypto.toBase64Url
@@ -623,15 +700,61 @@ function toBase64Url(buf) {
623
700
  * string + provides a single grep-able call site for the round-trip
624
701
  * pair.
625
702
  *
703
+ * Strict mode (default) refuses non-canonical input — chars outside
704
+ * the RFC 4648 §5 alphabet, length-mod-4-of-1, mixed `+/` from
705
+ * standard base64, trailing garbage. Defends a CVE-2022-0235-class
706
+ * footgun where Node's permissive decoder silently tolerated
707
+ * tampered JWT signatures. Operators with a documented lossy legacy
708
+ * payload opt out per call via `{ strict: false }`.
709
+ *
710
+ * @opts
711
+ * strict: boolean // default: true — refuse non-canonical input
712
+ *
626
713
  * @example
627
714
  * var buf = b.crypto.fromBase64Url("aGVsbG8");
628
715
  * buf.toString("utf8");
629
716
  * // → "hello"
630
717
  */
631
- function fromBase64Url(s) {
718
+ // RFC 4648 §5 alphabet for base64url, with optional padding. The
719
+ // canonical form has no padding, the URL-safe alphabet (`-_`), and a
720
+ // length consistent with the byte count (length % 4 ∈ {0, 2, 3}; the
721
+ // `length % 4 === 1` shape is impossible to produce by any conforming
722
+ // encoder and signals truncated / forged input).
723
+ var _BASE64URL_STRICT_RE = /^[A-Za-z0-9_-]*={0,2}$/;
724
+
725
+ function fromBase64Url(s, opts) {
632
726
  if (typeof s !== "string") {
633
727
  throw new TypeError("crypto.fromBase64Url: input must be a string");
634
728
  }
729
+ // Crypto callers (JWT signature payloads, JWS / COSE encoded values,
730
+ // OAuth `state` round-tripping) MUST reject non-canonical / malformed
731
+ // input. The Node base64url decoder silently tolerates trailing
732
+ // garbage, mixed `+/` from standard base64, missing padding errors,
733
+ // and length-mod-4 shapes — CVE-2022-0235-class footgun. Strict mode
734
+ // (the default) refuses anything outside the RFC 4648 §5 alphabet +
735
+ // length rules. Operators with a known-lossy legacy payload pass
736
+ // `{ strict: false }` to opt out per call.
737
+ var strict = !opts || opts.strict !== false;
738
+ if (strict) {
739
+ // Manual trailing-`=` strip — avoids the polynomial-regex shape
740
+ // `/=+$/` CodeQL flags, where `=+` can backtrack on long input
741
+ // ending in many `=`. Walking from end is O(n) worst-case.
742
+ var trimEnd = s.length;
743
+ while (trimEnd > 0 && s.charCodeAt(trimEnd - 1) === 0x3D) trimEnd -= 1; // allow:raw-byte-literal — '=' codepoint
744
+ var unpadded = s.slice(0, trimEnd);
745
+ if (!_BASE64URL_STRICT_RE.test(s)) {
746
+ throw new TypeError(
747
+ "crypto.fromBase64Url: input contains characters outside RFC 4648 §5 " +
748
+ "base64url alphabet (A-Z a-z 0-9 - _ =) — pass {strict:false} to allow non-canonical input"
749
+ );
750
+ }
751
+ if (unpadded.length % 4 === 1) { // allow:raw-byte-literal — base64 group length, not bytes
752
+ throw new TypeError(
753
+ "crypto.fromBase64Url: input length %% 4 === 1 is not a valid base64url encoding " +
754
+ "(every conforming encoder produces 0 / 2 / 3 remainder; got " + unpadded.length + " chars)"
755
+ );
756
+ }
757
+ }
635
758
  return Buffer.from(s, "base64url");
636
759
  }
637
760
 
@@ -867,8 +990,7 @@ function encrypt(plaintext, publicKeys) {
867
990
  _hybridDisabledAuditEmitted = true;
868
991
  setImmediate(function () {
869
992
  try {
870
- var auditMod = require("./audit"); // allow:inline-require — circular-load defense (audit imports crypto)
871
- auditMod.safeEmit({
993
+ audit().safeEmit({
872
994
  action: "system.crypto.hybrid_disabled",
873
995
  outcome: "success",
874
996
  metadata: { reason: "no-ec-public-key", note: "encrypt() received only mlkem; ecPublicKey absent — call encryptMlkemOnly explicitly to silence (audited once per process)" },
@@ -929,7 +1051,7 @@ function encryptMlkemOnly(plaintext, publicKeyPem) {
929
1051
  // ---- Envelope decrypt (dispatches on envelope IDs, supports both KEM IDs) ----
930
1052
  /**
931
1053
  * @primitive b.crypto.decrypt
932
- * @signature b.crypto.decrypt(ciphertext, privateKeys)
1054
+ * @signature b.crypto.decrypt(ciphertext, privateKeys, opts?)
933
1055
  * @since 0.1.0
934
1056
  * @related b.crypto.encrypt, b.crypto.generateEncryptionKeyPair, b.crypto.decryptMlkem768X25519
935
1057
  *
@@ -942,6 +1064,28 @@ function encryptMlkemOnly(plaintext, publicKeyPem) {
942
1064
  * Pass `{ privateKey, ecPrivateKey }` for the default hybrid; the
943
1065
  * ML-KEM-768 + X25519 KEM ID also requires `x25519PrivateKey`.
944
1066
  *
1067
+ * ## Legacy 0xE1 envelopes (`opts.allowLegacy: true`)
1068
+ *
1069
+ * The framework's envelope magic byte was bumped from 0xE1 to 0xE2
1070
+ * pre-v1 to enforce a NIST SP 800-56C r2 §4.1 FixedInfo / RFC 9180
1071
+ * §5.1 suite-binding KDF input — SHAKE256 absorbs the suite-id triple
1072
+ * (kemId / cipherId / kdfId) plus the literal "blamejs/v1" label
1073
+ * alongside the shared secret(s), so the same key cannot be reused
1074
+ * across suites without distinct derived material. 0xE1 envelopes
1075
+ * lack this binding.
1076
+ *
1077
+ * By default 0xE1 envelopes are refused with a hard error directing
1078
+ * the operator to re-seal under 0xE2. Operators with at-rest data
1079
+ * sealed pre-bump (rare; the bump landed before any operator started
1080
+ * depending on the framework) pass `opts.allowLegacy: true` to read
1081
+ * the old envelope, then immediately re-seal via `b.crypto.encrypt`
1082
+ * to migrate. Each legacy decrypt emits a `crypto.decrypt.allow_legacy`
1083
+ * audit event so the migration window is visible in the audit log.
1084
+ *
1085
+ * @opts
1086
+ * allowLegacy: boolean // default false — when true, 0xE1 envelopes
1087
+ * // decrypt via the pre-FixedInfo KDF path
1088
+ *
945
1089
  * @example
946
1090
  * var pair = b.crypto.generateEncryptionKeyPair();
947
1091
  * var sealed = b.crypto.encrypt("session-token=abc123", {
@@ -953,12 +1097,48 @@ function encryptMlkemOnly(plaintext, publicKeyPem) {
953
1097
  * ecPrivateKey: pair.ecPrivateKey,
954
1098
  * });
955
1099
  * // → "session-token=abc123"
1100
+ *
1101
+ * // Legacy 0xE1 migration:
1102
+ * var plaintext = b.crypto.decrypt(legacyBlob, oldKeys, { allowLegacy: true });
1103
+ * var resealed = b.crypto.encrypt(plaintext, newKeys); // now 0xE2
956
1104
  */
957
- function decrypt(ciphertext, privateKeys) {
1105
+ function decrypt(ciphertext, privateKeys, opts) {
958
1106
  var packed = Buffer.from(ciphertext, "base64");
959
1107
  if (packed[0] === 0xE1) { // allow:raw-byte-literal — legacy envelope magic
960
- throw new Error("Invalid envelope: legacy 0xE1 format predates the FixedInfo " +
961
- "KDF binding (NIST SP 800-56C r2 §4.1) re-seal data under the current envelope");
1108
+ if (!opts || !opts.allowLegacy) {
1109
+ throw new Error("Invalid envelope: legacy 0xE1 format predates the FixedInfo " +
1110
+ "KDF binding (NIST SP 800-56C r2 §4.1) — re-seal data under the current envelope, " +
1111
+ "or pass { allowLegacy: true } to opt in to one-shot read for migration");
1112
+ }
1113
+ // Audit-emit every legacy decrypt so the migration window is
1114
+ // visible. Emit success ONLY on actual decrypt success; emit
1115
+ // failure on throw. Codex P2 PR #74 — pre-fix the audit fired
1116
+ // before decryptEnvelope() ran, so corrupted 0xE1 blobs / wrong
1117
+ // private keys / unsupported KEMs got logged as successful legacy
1118
+ // decrypts when the call actually threw, inflating real success
1119
+ // rates during migration windows. Audit module is top-of-file
1120
+ // lazy-loaded (var audit above) so this hot-path emit doesn't
1121
+ // re-resolve the require() cache on every legacy decrypt.
1122
+ function _emitLegacyAudit(outcome, extra) {
1123
+ setImmediate(function () {
1124
+ try {
1125
+ audit().safeEmit({
1126
+ action: "system.crypto.decrypt.allow_legacy",
1127
+ outcome: outcome,
1128
+ metadata: Object.assign({ magic: "0xE1", kemId: packed[1] }, extra || {}),
1129
+ });
1130
+ } catch (_e) { /* drop-silent — audit best-effort */ }
1131
+ });
1132
+ }
1133
+ var plaintext;
1134
+ try {
1135
+ plaintext = decryptEnvelope(packed, privateKeys, { omitFixedInfo: true });
1136
+ } catch (e) {
1137
+ _emitLegacyAudit("failure", { reason: (e && e.message) || "decrypt threw" });
1138
+ throw e;
1139
+ }
1140
+ _emitLegacyAudit("success", {});
1141
+ return plaintext;
962
1142
  }
963
1143
  if (packed[0] !== C.ENVELOPE_MAGIC) {
964
1144
  throw new Error("Invalid envelope: unsupported format");
@@ -966,9 +1146,15 @@ function decrypt(ciphertext, privateKeys) {
966
1146
  return decryptEnvelope(packed, privateKeys);
967
1147
  }
968
1148
 
969
- function decryptEnvelope(packed, privateKeys) {
1149
+ function decryptEnvelope(packed, privateKeys, internalOpts) {
970
1150
  var kemId = packed[1], cipherId = packed[2], kdfId = packed[3], pos = 4;
971
1151
 
1152
+ // The legacy 0xE1 envelope predates the FixedInfo / suite-binding
1153
+ // KDF input; the same wire format otherwise. The dispatcher passes
1154
+ // omitFixedInfo: true for the 0xE1 path and the KDF input below
1155
+ // skips the _suiteFixedInfo concat.
1156
+ var omitFixedInfo = !!(internalOpts && internalOpts.omitFixedInfo);
1157
+
972
1158
  if (cipherId !== C.CIPHER_IDS.XCHACHA20_POLY1305) {
973
1159
  throw new Error("Invalid envelope: unsupported cipher (only XChaCha20-Poly1305 supported)");
974
1160
  }
@@ -984,6 +1170,7 @@ function decryptEnvelope(packed, privateKeys) {
984
1170
  );
985
1171
  var mlkemSs = nodeCrypto.decapsulate(mlkemPriv, kemCt);
986
1172
  var symmetricKey;
1173
+ var fixedInfo = omitFixedInfo ? Buffer.alloc(0) : _suiteFixedInfo(kemId, cipherId, kdfId);
987
1174
 
988
1175
  if (kemId === C.KEM_IDS.ML_KEM_1024_P384) {
989
1176
  var ecEphLen = packed.readUInt16BE(pos); pos += 2;
@@ -994,11 +1181,9 @@ function decryptEnvelope(packed, privateKeys) {
994
1181
  privateKey: nodeCrypto.createPrivateKey(ecPrivPem),
995
1182
  publicKey: nodeCrypto.createPublicKey({ key: ecEphDer, type: "spki", format: "der" }),
996
1183
  });
997
- symmetricKey = kdf(Buffer.concat([mlkemSs, ecSs,
998
- _suiteFixedInfo(kemId, cipherId, kdfId)]), C.BYTES.bytes(32));
1184
+ symmetricKey = kdf(Buffer.concat([mlkemSs, ecSs, fixedInfo]), C.BYTES.bytes(32));
999
1185
  } else if (kemId === C.KEM_IDS.ML_KEM_1024) {
1000
- symmetricKey = kdf(Buffer.concat([mlkemSs,
1001
- _suiteFixedInfo(kemId, cipherId, kdfId)]), C.BYTES.bytes(32));
1186
+ symmetricKey = kdf(Buffer.concat([mlkemSs, fixedInfo]), C.BYTES.bytes(32));
1002
1187
  } else if (kemId === C.KEM_IDS.ML_KEM_768_X25519) {
1003
1188
  // ML-KEM-768 + X25519 hybrid envelope. The mlkemPriv must be an
1004
1189
  // ML-KEM-768 key (not 1024); operators are responsible for passing
@@ -1013,8 +1198,7 @@ function decryptEnvelope(packed, privateKeys) {
1013
1198
  privateKey: nodeCrypto.createPrivateKey(x25519PrivPem),
1014
1199
  publicKey: nodeCrypto.createPublicKey({ key: x25519EphDer, type: "spki", format: "der" }),
1015
1200
  });
1016
- symmetricKey = kdf(Buffer.concat([mlkemSs, x25519Ss,
1017
- _suiteFixedInfo(kemId, cipherId, kdfId)]), C.BYTES.bytes(32));
1201
+ symmetricKey = kdf(Buffer.concat([mlkemSs, x25519Ss, fixedInfo]), C.BYTES.bytes(32));
1018
1202
  } else {
1019
1203
  throw new Error("Invalid envelope: unsupported KEM ID " + kemId);
1020
1204
  }
@@ -1571,6 +1755,18 @@ var SUPPORTED_KEM_ALGORITHMS = Object.freeze([
1571
1755
  { id: "ml-kem-768-x25519", envelopeId: C.KEM_IDS.ML_KEM_768_X25519, description: "ML-KEM-768 + X25519 hybrid (IETF / Cloudflare / Chrome TLS 1.3 codepoint 0x11EC)" },
1572
1756
  ]);
1573
1757
 
1758
+ // Note: legacy 0xE1 envelope minting (used only by test round-trip
1759
+ // coverage) lives at `lib/_test/crypto-fixtures.js` —
1760
+ // `mintLegacyEnvelope0xE1`. The function is NOT auto-exported on
1761
+ // `b.crypto.*` because (a) operator code has no production use for
1762
+ // it (the framework's only contract is READING pre-bump 0xE1 data
1763
+ // via `decrypt(..., { allowLegacy: true })`), and (b) exposing a
1764
+ // mint helper widens the attack surface of the `allowLegacy: true`
1765
+ // decrypt path. Tests require it directly:
1766
+ //
1767
+ // var fixtures = require("blamejs/lib/_test/crypto-fixtures");
1768
+ // var blob = fixtures.mintLegacyEnvelope0xE1(plaintext, recipient);
1769
+
1574
1770
  module.exports = {
1575
1771
  sri: sri,
1576
1772
  // Hashing
package/lib/db.js CHANGED
@@ -662,6 +662,7 @@ function loadOrCreateDbKey(dataDirPath, keyPathOverride) {
662
662
  }
663
663
  // First run — generate, seal, persist (atomic)
664
664
  var raw = generateBytes(C.BYTES.bytes(32));
665
+ // allow:seal-without-aad — whole-file DB encryption key, not a row column
665
666
  var sealedKey = vault.seal(raw.toString("base64"));
666
667
  atomicFile.writeSync(keyPath, sealedKey, { fileMode: 0o600 });
667
668
  log("generated DB encryption key at " + keyPath);