@blamejs/core 0.7.4 → 0.7.18
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 +423 -395
- package/README.md +150 -149
- package/bin/blamejs.js +0 -0
- package/index.js +308 -284
- package/lib/api-key.js +660 -663
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt.js +311 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +594 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -256
- package/lib/cache.js +1206 -1206
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/codepoint-class.js +196 -0
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +315 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -286
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -421
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/file-upload.js +213 -10
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -215
- package/lib/framework-schema.js +717 -717
- package/lib/gate-contract.js +971 -0
- package/lib/guard-all.js +405 -0
- package/lib/guard-archive.js +739 -0
- package/lib/guard-csv.js +816 -0
- package/lib/guard-email.js +744 -0
- package/lib/guard-filename.js +724 -0
- package/lib/guard-html.js +976 -0
- package/lib/guard-json.js +729 -0
- package/lib/guard-markdown.js +586 -0
- package/lib/guard-svg.js +976 -0
- package/lib/guard-xml.js +405 -0
- package/lib/guard-yaml.js +529 -0
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -362
- package/lib/mail.js +981 -962
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -936
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/body-parser.js +1170 -1091
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +316 -316
- package/lib/middleware/db-role-for.js +264 -264
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +79 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +116 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -612
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -91
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -488
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -519
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -903
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -167
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -745
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -519
- package/lib/request-helpers.js +340 -340
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -618
- package/lib/session.js +478 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/static.js +184 -4
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -319
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -595
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
package/lib/crypto.js
CHANGED
|
@@ -1,266 +1,266 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Centralized crypto module — envelope-versioned PQC primitives.
|
|
4
|
-
*
|
|
5
|
-
* Algorithm suite (modernity bar, per blamejs principle #8):
|
|
6
|
-
* KEM: ML-KEM-1024 + P-384 ECDH hybrid (FIPS 203 + classical defense in depth)
|
|
7
|
-
* Symmetric: XChaCha20-Poly1305 (24-byte nonce — no nonce-reuse risk under volume)
|
|
8
|
-
* KDF: SHAKE256 (FIPS 202)
|
|
9
|
-
* Hash: SHA3-512
|
|
10
|
-
* HMAC: HMAC-SHA3-512
|
|
11
|
-
* Signatures: ML-DSA-87 / SLH-DSA-SHAKE-256f (auto-detected from key PEM)
|
|
12
|
-
*
|
|
13
|
-
* Argon2id lives in lib/vault-wrap.js (used to derive vault and
|
|
14
|
-
* audit-signing key passphrases), not here.
|
|
15
|
-
*
|
|
16
|
-
* Envelope versioning (lib/constants.js → ENVELOPE_MAGIC, KEM_IDS, etc.):
|
|
17
|
-
* byte 0: ENVELOPE_MAGIC (0xE1)
|
|
18
|
-
* byte 1: KEM ID
|
|
19
|
-
* byte 2: CIPHER ID
|
|
20
|
-
* byte 3: KDF ID
|
|
21
|
-
*
|
|
22
|
-
* Old data decrypts under whichever IDs were written into its envelope; new
|
|
23
|
-
* writes use ACTIVE.{KEM, CIPHER, KDF}. Algorithm rotation is forward-only —
|
|
24
|
-
* see roadmap "Modernity posture" for the rotation policy.
|
|
25
|
-
*/
|
|
26
|
-
var nodeCrypto = require("crypto");
|
|
27
|
-
var { xchacha20poly1305 } = require("./vendor/noble-ciphers.cjs");
|
|
28
|
-
var C = require("./constants");
|
|
29
|
-
|
|
30
|
-
// ===========================================================
|
|
31
|
-
// Core primitives — everything else is built from these
|
|
32
|
-
// ===========================================================
|
|
33
|
-
|
|
34
|
-
function hash(data, algorithm, outputLength) {
|
|
35
|
-
var opts = outputLength ? { outputLength: outputLength } : undefined;
|
|
36
|
-
return nodeCrypto.createHash(algorithm, opts).update(data).digest();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function hmac(key, data, algorithm) {
|
|
40
|
-
return nodeCrypto.createHmac(algorithm, key).update(data).digest("hex");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function random(byteLength) {
|
|
44
|
-
var n = byteLength || 32;
|
|
45
|
-
// SHAKE256 over OS-RNG bytes. The OS RNG (nodeCrypto.randomBytes) is
|
|
46
|
-
// already cryptographically secure on modern platforms; passing
|
|
47
|
-
// through a hash adds defense-in-depth (stops a hypothetical
|
|
48
|
-
// randomBytes weakness from being directly observable downstream)
|
|
49
|
-
// without measurable cost. SHAKE256 is the right XOF here because it
|
|
50
|
-
// supports arbitrary output length — the previous implementation
|
|
51
|
-
// used SHA3-512 + subarray, which silently truncated to 64 bytes
|
|
52
|
-
// when callers requested more. SHAKE256 is also already the
|
|
53
|
-
// framework's KDF / browser-side derivation primitive, so the same
|
|
54
|
-
// hash family does double duty.
|
|
55
|
-
return nodeCrypto.createHash("shake256", { outputLength: n })
|
|
56
|
-
.update(nodeCrypto.randomBytes(n))
|
|
57
|
-
.digest();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function generateKeyPair(algorithm, options) {
|
|
61
|
-
var pair = nodeCrypto.generateKeyPairSync(algorithm, Object.assign({
|
|
62
|
-
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
63
|
-
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
64
|
-
}, options || {}));
|
|
65
|
-
return { publicKey: pair.publicKey, privateKey: pair.privateKey };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function timingSafeEqual(a, b) {
|
|
69
|
-
var bufA = Buffer.isBuffer(a) ? a : Buffer.from(String(a));
|
|
70
|
-
var bufB = Buffer.isBuffer(b) ? b : Buffer.from(String(b));
|
|
71
|
-
if (bufA.length !== bufB.length) return false;
|
|
72
|
-
return nodeCrypto.timingSafeEqual(bufA, bufB);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// ===========================================================
|
|
76
|
-
// Public API — built on core primitives
|
|
77
|
-
// ===========================================================
|
|
78
|
-
|
|
79
|
-
// ---- Hashing ----
|
|
80
|
-
function sha3Hash(data) { return hash(data, "sha3-512").toString("hex"); }
|
|
81
|
-
function hmacSha3(key, data) { return hmac(key, data, "sha3-512"); }
|
|
82
|
-
|
|
83
|
-
// (SHA-1 is intentionally NOT exported from b.crypto. The framework's
|
|
84
|
-
// only legitimate SHA-1 use is the HaveIBeenPwned k-anonymity API in
|
|
85
|
-
// lib/auth/password.js, which imports lib/framework-sha1-hibp.js
|
|
86
|
-
// directly. Public b.crypto.sha1* is permanently off the table — a
|
|
87
|
-
// future caller wanting SHA-1 for storage / signing / fingerprinting
|
|
88
|
-
// would re-introduce a broken primitive into the crypto surface this
|
|
89
|
-
// framework spent every other line keeping out.)
|
|
90
|
-
|
|
91
|
-
// ---- KDF ----
|
|
92
|
-
function kdf(input, outputLength) { return hash(input, "shake256", outputLength); }
|
|
93
|
-
|
|
94
|
-
// ---- Random ----
|
|
95
|
-
function generateBytes(byteLength) { return Buffer.from(random(byteLength)); }
|
|
96
|
-
function generateToken(byteLength) { return random(byteLength || 32).toString("hex"); }
|
|
97
|
-
|
|
98
|
-
// ---- Key generation ----
|
|
99
|
-
function generateEncryptionKeyPair() {
|
|
100
|
-
var mlkem = generateKeyPair("ml-kem-1024");
|
|
101
|
-
var ec = generateKeyPair("ec", { namedCurve: "P-384" });
|
|
102
|
-
return {
|
|
103
|
-
publicKey: mlkem.publicKey,
|
|
104
|
-
privateKey: mlkem.privateKey,
|
|
105
|
-
ecPublicKey: ec.publicKey,
|
|
106
|
-
ecPrivateKey: ec.privateKey,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function generateSigningKeyPair(algorithm) {
|
|
111
|
-
return generateKeyPair(algorithm || "ml-dsa-87");
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// ---- Signatures (auto-detect algorithm from key PEM) ----
|
|
115
|
-
function sign(data, privateKeyPem) {
|
|
116
|
-
return nodeCrypto.sign(null, Buffer.from(data), privateKeyPem);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function verify(data, signature, publicKeyPem) {
|
|
120
|
-
return nodeCrypto.verify(null, Buffer.from(data), publicKeyPem, signature);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ---- Envelope encrypt (ML-KEM-1024 + P-384 ECDH hybrid + SHAKE256 + XChaCha20) ----
|
|
124
|
-
function encrypt(plaintext, publicKeys) {
|
|
125
|
-
var mlkemPubPem = typeof publicKeys === "string" ? publicKeys : publicKeys.publicKey;
|
|
126
|
-
var ecPubPem = typeof publicKeys === "string" ? null : publicKeys.ecPublicKey;
|
|
127
|
-
if (!ecPubPem) return encryptMlkemOnly(plaintext, mlkemPubPem);
|
|
128
|
-
|
|
129
|
-
var mlkemPub = nodeCrypto.createPublicKey(mlkemPubPem);
|
|
130
|
-
var kem = nodeCrypto.encapsulate(mlkemPub);
|
|
131
|
-
var ephEc = generateKeyPair("ec", {
|
|
132
|
-
namedCurve: "P-384",
|
|
133
|
-
publicKeyEncoding: { type: "spki", format: "der" },
|
|
134
|
-
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
135
|
-
});
|
|
136
|
-
var ecSs = nodeCrypto.diffieHellman({
|
|
137
|
-
privateKey: nodeCrypto.createPrivateKey(ephEc.privateKey),
|
|
138
|
-
publicKey: nodeCrypto.createPublicKey(ecPubPem),
|
|
139
|
-
});
|
|
140
|
-
var key = kdf(Buffer.concat([kem.sharedKey, ecSs]), C.BYTES.bytes(32));
|
|
141
|
-
var nonce = generateBytes(C.BYTES.bytes(24));
|
|
142
|
-
var ct = xchacha20poly1305(key, nonce).encrypt(Buffer.from(plaintext, "utf8"));
|
|
143
|
-
|
|
144
|
-
var kemCtLen = Buffer.alloc(2); kemCtLen.writeUInt16BE(kem.ciphertext.length);
|
|
145
|
-
var ecEphDer = ephEc.publicKey;
|
|
146
|
-
var ecEphLen = Buffer.alloc(2); ecEphLen.writeUInt16BE(ecEphDer.length);
|
|
147
|
-
|
|
148
|
-
return Buffer.concat([
|
|
149
|
-
Buffer.from([C.ENVELOPE_MAGIC, C.ACTIVE.KEM, C.ACTIVE.CIPHER, C.ACTIVE.KDF]),
|
|
150
|
-
kemCtLen, kem.ciphertext, ecEphLen, ecEphDer, nonce, Buffer.from(ct),
|
|
151
|
-
]).toString("base64");
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function encryptMlkemOnly(plaintext, publicKeyPem) {
|
|
155
|
-
var kem = nodeCrypto.encapsulate(nodeCrypto.createPublicKey(publicKeyPem));
|
|
156
|
-
var key = kdf(kem.sharedKey, C.BYTES.bytes(32));
|
|
157
|
-
var nonce = generateBytes(C.BYTES.bytes(24));
|
|
158
|
-
var ct = xchacha20poly1305(key, nonce).encrypt(Buffer.from(plaintext, "utf8"));
|
|
159
|
-
var kemCtLen = Buffer.alloc(2); kemCtLen.writeUInt16BE(kem.ciphertext.length);
|
|
160
|
-
return Buffer.concat([
|
|
161
|
-
Buffer.from([C.ENVELOPE_MAGIC, C.KEM_IDS.ML_KEM_1024, C.ACTIVE.CIPHER, C.ACTIVE.KDF]),
|
|
162
|
-
kemCtLen, kem.ciphertext, nonce, Buffer.from(ct),
|
|
163
|
-
]).toString("base64");
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// ---- Envelope decrypt (dispatches on envelope IDs, supports both KEM IDs) ----
|
|
167
|
-
function decrypt(ciphertext, privateKeys) {
|
|
168
|
-
var packed = Buffer.from(ciphertext, "base64");
|
|
169
|
-
if (packed[0] !== C.ENVELOPE_MAGIC) {
|
|
170
|
-
throw new Error("Invalid envelope: unsupported format");
|
|
171
|
-
}
|
|
172
|
-
return decryptEnvelope(packed, privateKeys);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function decryptEnvelope(packed, privateKeys) {
|
|
176
|
-
var kemId = packed[1], cipherId = packed[2], kdfId = packed[3], pos = 4;
|
|
177
|
-
|
|
178
|
-
if (cipherId !== C.CIPHER_IDS.XCHACHA20_POLY1305) {
|
|
179
|
-
throw new Error("Invalid envelope: unsupported cipher (only XChaCha20-Poly1305 supported)");
|
|
180
|
-
}
|
|
181
|
-
if (kdfId !== C.KDF_IDS.SHAKE256) {
|
|
182
|
-
throw new Error("Invalid envelope: unsupported KDF (only SHAKE256 supported)");
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
var kemCtLen = packed.readUInt16BE(pos); pos += 2;
|
|
186
|
-
var kemCt = packed.subarray(pos, pos + kemCtLen); pos += kemCtLen;
|
|
187
|
-
|
|
188
|
-
var mlkemPriv = nodeCrypto.createPrivateKey(
|
|
189
|
-
typeof privateKeys === "string" ? privateKeys : privateKeys.privateKey
|
|
190
|
-
);
|
|
191
|
-
var mlkemSs = nodeCrypto.decapsulate(mlkemPriv, kemCt);
|
|
192
|
-
var symmetricKey;
|
|
193
|
-
|
|
194
|
-
if (kemId === C.KEM_IDS.ML_KEM_1024_P384) {
|
|
195
|
-
var ecEphLen = packed.readUInt16BE(pos); pos += 2;
|
|
196
|
-
var ecEphDer = packed.subarray(pos, pos + ecEphLen); pos += ecEphLen;
|
|
197
|
-
var ecPrivPem = typeof privateKeys === "string" ? null : privateKeys.ecPrivateKey;
|
|
198
|
-
if (!ecPrivPem) throw new Error("Hybrid KEM requires EC private key");
|
|
199
|
-
var ecSs = nodeCrypto.diffieHellman({
|
|
200
|
-
privateKey: nodeCrypto.createPrivateKey(ecPrivPem),
|
|
201
|
-
publicKey: nodeCrypto.createPublicKey({ key: ecEphDer, type: "spki", format: "der" }),
|
|
202
|
-
});
|
|
203
|
-
symmetricKey = kdf(Buffer.concat([mlkemSs, ecSs]), C.BYTES.bytes(32));
|
|
204
|
-
} else if (kemId === C.KEM_IDS.ML_KEM_1024) {
|
|
205
|
-
symmetricKey = kdf(mlkemSs, C.BYTES.bytes(32));
|
|
206
|
-
} else {
|
|
207
|
-
throw new Error("Invalid envelope: unsupported KEM ID " + kemId);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
var nonce = packed.subarray(pos, pos + C.BYTES.bytes(24)); pos += C.BYTES.bytes(24);
|
|
211
|
-
return Buffer.from(
|
|
212
|
-
xchacha20poly1305(symmetricKey, nonce).decrypt(packed.subarray(pos))
|
|
213
|
-
).toString("utf8");
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// ---- Symmetric buffer encrypt/decrypt (for storage) ----
|
|
217
|
-
//
|
|
218
|
-
// Optional `aad` (additional authenticated data) is mixed into the
|
|
219
|
-
// Poly1305 tag — encrypt-time and decrypt-time AAD must match exactly
|
|
220
|
-
// or decrypt fails. Used by primitives that want encryption-context
|
|
221
|
-
// binding (b.breakGlass.encryptCell binds (table, rowId, column) so a
|
|
222
|
-
// ciphertext from row A literally cannot decrypt as row B even with
|
|
223
|
-
// the same key).
|
|
224
|
-
function encryptPacked(buffer, key, aad) {
|
|
225
|
-
var nonce = random(C.BYTES.bytes(24));
|
|
226
|
-
var ct = xchacha20poly1305(key, nonce, aad ? Buffer.from(aad) : undefined).encrypt(buffer);
|
|
227
|
-
return Buffer.concat([
|
|
228
|
-
Buffer.from([C.FORMAT.XCHACHA20_POLY1305]),
|
|
229
|
-
Buffer.from(nonce),
|
|
230
|
-
Buffer.from(ct),
|
|
231
|
-
]);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function decryptPacked(packed, key, aad) {
|
|
235
|
-
if (packed[0] !== C.FORMAT.XCHACHA20_POLY1305) {
|
|
236
|
-
throw new Error("Invalid packed format: unsupported version");
|
|
237
|
-
}
|
|
238
|
-
return Buffer.from(
|
|
239
|
-
xchacha20poly1305(key, packed.subarray(1, 25), aad ? Buffer.from(aad) : undefined)
|
|
240
|
-
.decrypt(packed.subarray(25))
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
module.exports = {
|
|
245
|
-
// Hashing
|
|
246
|
-
sha3Hash: sha3Hash,
|
|
247
|
-
hmacSha3: hmacSha3,
|
|
248
|
-
kdf: kdf,
|
|
249
|
-
// Comparison
|
|
250
|
-
timingSafeEqual: timingSafeEqual,
|
|
251
|
-
// Random
|
|
252
|
-
generateBytes: generateBytes,
|
|
253
|
-
generateToken: generateToken,
|
|
254
|
-
// Keys
|
|
255
|
-
generateEncryptionKeyPair: generateEncryptionKeyPair,
|
|
256
|
-
generateSigningKeyPair: generateSigningKeyPair,
|
|
257
|
-
// Signatures
|
|
258
|
-
sign: sign,
|
|
259
|
-
verify: verify,
|
|
260
|
-
// Envelope encrypt/decrypt
|
|
261
|
-
encrypt: encrypt,
|
|
262
|
-
decrypt: decrypt,
|
|
263
|
-
// Symmetric buffer encrypt/decrypt
|
|
264
|
-
encryptPacked: encryptPacked,
|
|
265
|
-
decryptPacked: decryptPacked,
|
|
266
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Centralized crypto module — envelope-versioned PQC primitives.
|
|
4
|
+
*
|
|
5
|
+
* Algorithm suite (modernity bar, per blamejs principle #8):
|
|
6
|
+
* KEM: ML-KEM-1024 + P-384 ECDH hybrid (FIPS 203 + classical defense in depth)
|
|
7
|
+
* Symmetric: XChaCha20-Poly1305 (24-byte nonce — no nonce-reuse risk under volume)
|
|
8
|
+
* KDF: SHAKE256 (FIPS 202)
|
|
9
|
+
* Hash: SHA3-512
|
|
10
|
+
* HMAC: HMAC-SHA3-512
|
|
11
|
+
* Signatures: ML-DSA-87 / SLH-DSA-SHAKE-256f (auto-detected from key PEM)
|
|
12
|
+
*
|
|
13
|
+
* Argon2id lives in lib/vault-wrap.js (used to derive vault and
|
|
14
|
+
* audit-signing key passphrases), not here.
|
|
15
|
+
*
|
|
16
|
+
* Envelope versioning (lib/constants.js → ENVELOPE_MAGIC, KEM_IDS, etc.):
|
|
17
|
+
* byte 0: ENVELOPE_MAGIC (0xE1)
|
|
18
|
+
* byte 1: KEM ID
|
|
19
|
+
* byte 2: CIPHER ID
|
|
20
|
+
* byte 3: KDF ID
|
|
21
|
+
*
|
|
22
|
+
* Old data decrypts under whichever IDs were written into its envelope; new
|
|
23
|
+
* writes use ACTIVE.{KEM, CIPHER, KDF}. Algorithm rotation is forward-only —
|
|
24
|
+
* see roadmap "Modernity posture" for the rotation policy.
|
|
25
|
+
*/
|
|
26
|
+
var nodeCrypto = require("crypto");
|
|
27
|
+
var { xchacha20poly1305 } = require("./vendor/noble-ciphers.cjs");
|
|
28
|
+
var C = require("./constants");
|
|
29
|
+
|
|
30
|
+
// ===========================================================
|
|
31
|
+
// Core primitives — everything else is built from these
|
|
32
|
+
// ===========================================================
|
|
33
|
+
|
|
34
|
+
function hash(data, algorithm, outputLength) {
|
|
35
|
+
var opts = outputLength ? { outputLength: outputLength } : undefined;
|
|
36
|
+
return nodeCrypto.createHash(algorithm, opts).update(data).digest();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function hmac(key, data, algorithm) {
|
|
40
|
+
return nodeCrypto.createHmac(algorithm, key).update(data).digest("hex");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function random(byteLength) {
|
|
44
|
+
var n = byteLength || 32;
|
|
45
|
+
// SHAKE256 over OS-RNG bytes. The OS RNG (nodeCrypto.randomBytes) is
|
|
46
|
+
// already cryptographically secure on modern platforms; passing
|
|
47
|
+
// through a hash adds defense-in-depth (stops a hypothetical
|
|
48
|
+
// randomBytes weakness from being directly observable downstream)
|
|
49
|
+
// without measurable cost. SHAKE256 is the right XOF here because it
|
|
50
|
+
// supports arbitrary output length — the previous implementation
|
|
51
|
+
// used SHA3-512 + subarray, which silently truncated to 64 bytes
|
|
52
|
+
// when callers requested more. SHAKE256 is also already the
|
|
53
|
+
// framework's KDF / browser-side derivation primitive, so the same
|
|
54
|
+
// hash family does double duty.
|
|
55
|
+
return nodeCrypto.createHash("shake256", { outputLength: n })
|
|
56
|
+
.update(nodeCrypto.randomBytes(n))
|
|
57
|
+
.digest();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function generateKeyPair(algorithm, options) {
|
|
61
|
+
var pair = nodeCrypto.generateKeyPairSync(algorithm, Object.assign({
|
|
62
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
63
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
64
|
+
}, options || {}));
|
|
65
|
+
return { publicKey: pair.publicKey, privateKey: pair.privateKey };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function timingSafeEqual(a, b) {
|
|
69
|
+
var bufA = Buffer.isBuffer(a) ? a : Buffer.from(String(a));
|
|
70
|
+
var bufB = Buffer.isBuffer(b) ? b : Buffer.from(String(b));
|
|
71
|
+
if (bufA.length !== bufB.length) return false;
|
|
72
|
+
return nodeCrypto.timingSafeEqual(bufA, bufB);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ===========================================================
|
|
76
|
+
// Public API — built on core primitives
|
|
77
|
+
// ===========================================================
|
|
78
|
+
|
|
79
|
+
// ---- Hashing ----
|
|
80
|
+
function sha3Hash(data) { return hash(data, "sha3-512").toString("hex"); }
|
|
81
|
+
function hmacSha3(key, data) { return hmac(key, data, "sha3-512"); }
|
|
82
|
+
|
|
83
|
+
// (SHA-1 is intentionally NOT exported from b.crypto. The framework's
|
|
84
|
+
// only legitimate SHA-1 use is the HaveIBeenPwned k-anonymity API in
|
|
85
|
+
// lib/auth/password.js, which imports lib/framework-sha1-hibp.js
|
|
86
|
+
// directly. Public b.crypto.sha1* is permanently off the table — a
|
|
87
|
+
// future caller wanting SHA-1 for storage / signing / fingerprinting
|
|
88
|
+
// would re-introduce a broken primitive into the crypto surface this
|
|
89
|
+
// framework spent every other line keeping out.)
|
|
90
|
+
|
|
91
|
+
// ---- KDF ----
|
|
92
|
+
function kdf(input, outputLength) { return hash(input, "shake256", outputLength); }
|
|
93
|
+
|
|
94
|
+
// ---- Random ----
|
|
95
|
+
function generateBytes(byteLength) { return Buffer.from(random(byteLength)); }
|
|
96
|
+
function generateToken(byteLength) { return random(byteLength || 32).toString("hex"); }
|
|
97
|
+
|
|
98
|
+
// ---- Key generation ----
|
|
99
|
+
function generateEncryptionKeyPair() {
|
|
100
|
+
var mlkem = generateKeyPair("ml-kem-1024");
|
|
101
|
+
var ec = generateKeyPair("ec", { namedCurve: "P-384" });
|
|
102
|
+
return {
|
|
103
|
+
publicKey: mlkem.publicKey,
|
|
104
|
+
privateKey: mlkem.privateKey,
|
|
105
|
+
ecPublicKey: ec.publicKey,
|
|
106
|
+
ecPrivateKey: ec.privateKey,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function generateSigningKeyPair(algorithm) {
|
|
111
|
+
return generateKeyPair(algorithm || "ml-dsa-87");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---- Signatures (auto-detect algorithm from key PEM) ----
|
|
115
|
+
function sign(data, privateKeyPem) {
|
|
116
|
+
return nodeCrypto.sign(null, Buffer.from(data), privateKeyPem);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function verify(data, signature, publicKeyPem) {
|
|
120
|
+
return nodeCrypto.verify(null, Buffer.from(data), publicKeyPem, signature);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---- Envelope encrypt (ML-KEM-1024 + P-384 ECDH hybrid + SHAKE256 + XChaCha20) ----
|
|
124
|
+
function encrypt(plaintext, publicKeys) {
|
|
125
|
+
var mlkemPubPem = typeof publicKeys === "string" ? publicKeys : publicKeys.publicKey;
|
|
126
|
+
var ecPubPem = typeof publicKeys === "string" ? null : publicKeys.ecPublicKey;
|
|
127
|
+
if (!ecPubPem) return encryptMlkemOnly(plaintext, mlkemPubPem);
|
|
128
|
+
|
|
129
|
+
var mlkemPub = nodeCrypto.createPublicKey(mlkemPubPem);
|
|
130
|
+
var kem = nodeCrypto.encapsulate(mlkemPub);
|
|
131
|
+
var ephEc = generateKeyPair("ec", {
|
|
132
|
+
namedCurve: "P-384",
|
|
133
|
+
publicKeyEncoding: { type: "spki", format: "der" },
|
|
134
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
135
|
+
});
|
|
136
|
+
var ecSs = nodeCrypto.diffieHellman({
|
|
137
|
+
privateKey: nodeCrypto.createPrivateKey(ephEc.privateKey),
|
|
138
|
+
publicKey: nodeCrypto.createPublicKey(ecPubPem),
|
|
139
|
+
});
|
|
140
|
+
var key = kdf(Buffer.concat([kem.sharedKey, ecSs]), C.BYTES.bytes(32));
|
|
141
|
+
var nonce = generateBytes(C.BYTES.bytes(24));
|
|
142
|
+
var ct = xchacha20poly1305(key, nonce).encrypt(Buffer.from(plaintext, "utf8"));
|
|
143
|
+
|
|
144
|
+
var kemCtLen = Buffer.alloc(2); kemCtLen.writeUInt16BE(kem.ciphertext.length);
|
|
145
|
+
var ecEphDer = ephEc.publicKey;
|
|
146
|
+
var ecEphLen = Buffer.alloc(2); ecEphLen.writeUInt16BE(ecEphDer.length);
|
|
147
|
+
|
|
148
|
+
return Buffer.concat([
|
|
149
|
+
Buffer.from([C.ENVELOPE_MAGIC, C.ACTIVE.KEM, C.ACTIVE.CIPHER, C.ACTIVE.KDF]),
|
|
150
|
+
kemCtLen, kem.ciphertext, ecEphLen, ecEphDer, nonce, Buffer.from(ct),
|
|
151
|
+
]).toString("base64");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function encryptMlkemOnly(plaintext, publicKeyPem) {
|
|
155
|
+
var kem = nodeCrypto.encapsulate(nodeCrypto.createPublicKey(publicKeyPem));
|
|
156
|
+
var key = kdf(kem.sharedKey, C.BYTES.bytes(32));
|
|
157
|
+
var nonce = generateBytes(C.BYTES.bytes(24));
|
|
158
|
+
var ct = xchacha20poly1305(key, nonce).encrypt(Buffer.from(plaintext, "utf8"));
|
|
159
|
+
var kemCtLen = Buffer.alloc(2); kemCtLen.writeUInt16BE(kem.ciphertext.length);
|
|
160
|
+
return Buffer.concat([
|
|
161
|
+
Buffer.from([C.ENVELOPE_MAGIC, C.KEM_IDS.ML_KEM_1024, C.ACTIVE.CIPHER, C.ACTIVE.KDF]),
|
|
162
|
+
kemCtLen, kem.ciphertext, nonce, Buffer.from(ct),
|
|
163
|
+
]).toString("base64");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---- Envelope decrypt (dispatches on envelope IDs, supports both KEM IDs) ----
|
|
167
|
+
function decrypt(ciphertext, privateKeys) {
|
|
168
|
+
var packed = Buffer.from(ciphertext, "base64");
|
|
169
|
+
if (packed[0] !== C.ENVELOPE_MAGIC) {
|
|
170
|
+
throw new Error("Invalid envelope: unsupported format");
|
|
171
|
+
}
|
|
172
|
+
return decryptEnvelope(packed, privateKeys);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function decryptEnvelope(packed, privateKeys) {
|
|
176
|
+
var kemId = packed[1], cipherId = packed[2], kdfId = packed[3], pos = 4;
|
|
177
|
+
|
|
178
|
+
if (cipherId !== C.CIPHER_IDS.XCHACHA20_POLY1305) {
|
|
179
|
+
throw new Error("Invalid envelope: unsupported cipher (only XChaCha20-Poly1305 supported)");
|
|
180
|
+
}
|
|
181
|
+
if (kdfId !== C.KDF_IDS.SHAKE256) {
|
|
182
|
+
throw new Error("Invalid envelope: unsupported KDF (only SHAKE256 supported)");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
var kemCtLen = packed.readUInt16BE(pos); pos += 2;
|
|
186
|
+
var kemCt = packed.subarray(pos, pos + kemCtLen); pos += kemCtLen;
|
|
187
|
+
|
|
188
|
+
var mlkemPriv = nodeCrypto.createPrivateKey(
|
|
189
|
+
typeof privateKeys === "string" ? privateKeys : privateKeys.privateKey
|
|
190
|
+
);
|
|
191
|
+
var mlkemSs = nodeCrypto.decapsulate(mlkemPriv, kemCt);
|
|
192
|
+
var symmetricKey;
|
|
193
|
+
|
|
194
|
+
if (kemId === C.KEM_IDS.ML_KEM_1024_P384) {
|
|
195
|
+
var ecEphLen = packed.readUInt16BE(pos); pos += 2;
|
|
196
|
+
var ecEphDer = packed.subarray(pos, pos + ecEphLen); pos += ecEphLen;
|
|
197
|
+
var ecPrivPem = typeof privateKeys === "string" ? null : privateKeys.ecPrivateKey;
|
|
198
|
+
if (!ecPrivPem) throw new Error("Hybrid KEM requires EC private key");
|
|
199
|
+
var ecSs = nodeCrypto.diffieHellman({
|
|
200
|
+
privateKey: nodeCrypto.createPrivateKey(ecPrivPem),
|
|
201
|
+
publicKey: nodeCrypto.createPublicKey({ key: ecEphDer, type: "spki", format: "der" }),
|
|
202
|
+
});
|
|
203
|
+
symmetricKey = kdf(Buffer.concat([mlkemSs, ecSs]), C.BYTES.bytes(32));
|
|
204
|
+
} else if (kemId === C.KEM_IDS.ML_KEM_1024) {
|
|
205
|
+
symmetricKey = kdf(mlkemSs, C.BYTES.bytes(32));
|
|
206
|
+
} else {
|
|
207
|
+
throw new Error("Invalid envelope: unsupported KEM ID " + kemId);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
var nonce = packed.subarray(pos, pos + C.BYTES.bytes(24)); pos += C.BYTES.bytes(24);
|
|
211
|
+
return Buffer.from(
|
|
212
|
+
xchacha20poly1305(symmetricKey, nonce).decrypt(packed.subarray(pos))
|
|
213
|
+
).toString("utf8");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ---- Symmetric buffer encrypt/decrypt (for storage) ----
|
|
217
|
+
//
|
|
218
|
+
// Optional `aad` (additional authenticated data) is mixed into the
|
|
219
|
+
// Poly1305 tag — encrypt-time and decrypt-time AAD must match exactly
|
|
220
|
+
// or decrypt fails. Used by primitives that want encryption-context
|
|
221
|
+
// binding (b.breakGlass.encryptCell binds (table, rowId, column) so a
|
|
222
|
+
// ciphertext from row A literally cannot decrypt as row B even with
|
|
223
|
+
// the same key).
|
|
224
|
+
function encryptPacked(buffer, key, aad) {
|
|
225
|
+
var nonce = random(C.BYTES.bytes(24));
|
|
226
|
+
var ct = xchacha20poly1305(key, nonce, aad ? Buffer.from(aad) : undefined).encrypt(buffer);
|
|
227
|
+
return Buffer.concat([
|
|
228
|
+
Buffer.from([C.FORMAT.XCHACHA20_POLY1305]),
|
|
229
|
+
Buffer.from(nonce),
|
|
230
|
+
Buffer.from(ct),
|
|
231
|
+
]);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function decryptPacked(packed, key, aad) {
|
|
235
|
+
if (packed[0] !== C.FORMAT.XCHACHA20_POLY1305) {
|
|
236
|
+
throw new Error("Invalid packed format: unsupported version");
|
|
237
|
+
}
|
|
238
|
+
return Buffer.from(
|
|
239
|
+
xchacha20poly1305(key, packed.subarray(1, 25), aad ? Buffer.from(aad) : undefined)
|
|
240
|
+
.decrypt(packed.subarray(25))
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
module.exports = {
|
|
245
|
+
// Hashing
|
|
246
|
+
sha3Hash: sha3Hash,
|
|
247
|
+
hmacSha3: hmacSha3,
|
|
248
|
+
kdf: kdf,
|
|
249
|
+
// Comparison
|
|
250
|
+
timingSafeEqual: timingSafeEqual,
|
|
251
|
+
// Random
|
|
252
|
+
generateBytes: generateBytes,
|
|
253
|
+
generateToken: generateToken,
|
|
254
|
+
// Keys
|
|
255
|
+
generateEncryptionKeyPair: generateEncryptionKeyPair,
|
|
256
|
+
generateSigningKeyPair: generateSigningKeyPair,
|
|
257
|
+
// Signatures
|
|
258
|
+
sign: sign,
|
|
259
|
+
verify: verify,
|
|
260
|
+
// Envelope encrypt/decrypt
|
|
261
|
+
encrypt: encrypt,
|
|
262
|
+
decrypt: decrypt,
|
|
263
|
+
// Symmetric buffer encrypt/decrypt
|
|
264
|
+
encryptPacked: encryptPacked,
|
|
265
|
+
decryptPacked: decryptPacked,
|
|
266
|
+
};
|