@blamejs/core 0.4.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.
- package/CHANGELOG.md +230 -0
- package/LICENSE +201 -0
- package/LTS-CALENDAR.md +29 -0
- package/MIGRATING.md +7 -0
- package/NOTICE +59 -0
- package/README.md +100 -0
- package/bin/blamejs.js +13 -0
- package/index.js +253 -0
- package/lib/api-key.js +705 -0
- package/lib/api-snapshot.js +335 -0
- package/lib/app-shutdown.js +381 -0
- package/lib/app.js +364 -0
- package/lib/atomic-file.js +525 -0
- package/lib/audit-chain.js +168 -0
- package/lib/audit-sign.js +319 -0
- package/lib/audit-tools.js +682 -0
- package/lib/audit.js +753 -0
- package/lib/auth/jwt.js +280 -0
- package/lib/auth/oauth.js +691 -0
- package/lib/auth/passkey.js +185 -0
- package/lib/auth/password.js +139 -0
- package/lib/auth/totp.js +17 -0
- package/lib/auth-header.js +81 -0
- package/lib/backup/bundle.js +219 -0
- package/lib/backup/crypto.js +174 -0
- package/lib/backup/index.js +490 -0
- package/lib/backup/manifest.js +275 -0
- package/lib/bundler.js +295 -0
- package/lib/cache.js +819 -0
- package/lib/chain-writer.js +234 -0
- package/lib/cli-helpers.js +201 -0
- package/lib/cli.js +1377 -0
- package/lib/cluster-provider-db.js +245 -0
- package/lib/cluster-storage.js +166 -0
- package/lib/cluster.js +691 -0
- package/lib/consent.js +222 -0
- package/lib/constants.js +186 -0
- package/lib/cookies.js +293 -0
- package/lib/credential-hash.js +303 -0
- package/lib/crypto-field.js +159 -0
- package/lib/crypto.js +250 -0
- package/lib/db-query.js +297 -0
- package/lib/db-schema.js +250 -0
- package/lib/db.js +1054 -0
- package/lib/deprecate.js +226 -0
- package/lib/dev.js +324 -0
- package/lib/error-page.js +424 -0
- package/lib/events.js +135 -0
- package/lib/external-db.js +422 -0
- package/lib/forms.js +378 -0
- package/lib/framework-error.js +189 -0
- package/lib/framework-schema.js +604 -0
- package/lib/handlers.js +350 -0
- package/lib/html-balance.js +227 -0
- package/lib/http-client.js +615 -0
- package/lib/i18n.js +780 -0
- package/lib/jobs.js +181 -0
- package/lib/lazy-require.js +48 -0
- package/lib/log-stream-local.js +137 -0
- package/lib/log-stream-webhook.js +170 -0
- package/lib/log-stream.js +211 -0
- package/lib/log.js +355 -0
- package/lib/mail-bounce.js +507 -0
- package/lib/mail.js +701 -0
- package/lib/metrics.js +647 -0
- package/lib/middleware/api-encrypt.js +553 -0
- package/lib/middleware/attach-user.js +156 -0
- package/lib/middleware/body-parser.js +883 -0
- package/lib/middleware/bot-guard.js +148 -0
- package/lib/middleware/compression.js +436 -0
- package/lib/middleware/cors.js +236 -0
- package/lib/middleware/csp-nonce.js +332 -0
- package/lib/middleware/csrf-protect.js +275 -0
- package/lib/middleware/error-handler.js +46 -0
- package/lib/middleware/health.js +358 -0
- package/lib/middleware/index.js +52 -0
- package/lib/middleware/rate-limit.js +319 -0
- package/lib/middleware/request-id.js +53 -0
- package/lib/middleware/require-auth.js +95 -0
- package/lib/middleware/security-headers.js +91 -0
- package/lib/migrations.js +353 -0
- package/lib/mtls-ca.js +333 -0
- package/lib/mtls-engine-default.js +285 -0
- package/lib/nonce-store.js +177 -0
- package/lib/notify.js +643 -0
- package/lib/ntp-check.js +178 -0
- package/lib/object-store/azure-blob.js +467 -0
- package/lib/object-store/gcs.js +469 -0
- package/lib/object-store/http-put.js +153 -0
- package/lib/object-store/index.js +140 -0
- package/lib/object-store/local.js +163 -0
- package/lib/object-store/retry.js +15 -0
- package/lib/object-store/sigv4.js +535 -0
- package/lib/observability.js +114 -0
- package/lib/pagination.js +371 -0
- package/lib/parsers/index.js +64 -0
- package/lib/parsers/safe-csv.js +224 -0
- package/lib/parsers/safe-env.js +614 -0
- package/lib/parsers/safe-toml.js +745 -0
- package/lib/parsers/safe-xml.js +379 -0
- package/lib/parsers/safe-yaml.js +977 -0
- package/lib/permissions.js +430 -0
- package/lib/pqc-agent.js +85 -0
- package/lib/pqc-gate.js +266 -0
- package/lib/protocol-dispatcher.js +144 -0
- package/lib/queue-local.js +327 -0
- package/lib/queue.js +430 -0
- package/lib/redact.js +192 -0
- package/lib/render.js +193 -0
- package/lib/request-helpers.js +178 -0
- package/lib/restore-bundle.js +239 -0
- package/lib/restore-rollback.js +254 -0
- package/lib/restore.js +301 -0
- package/lib/retry.js +329 -0
- package/lib/router.js +437 -0
- package/lib/safe-async.js +520 -0
- package/lib/safe-buffer.js +162 -0
- package/lib/safe-json.js +532 -0
- package/lib/safe-schema.js +1176 -0
- package/lib/safe-sql.js +157 -0
- package/lib/safe-url.js +109 -0
- package/lib/scheduler.js +680 -0
- package/lib/seeders.js +622 -0
- package/lib/session.js +304 -0
- package/lib/slug.js +243 -0
- package/lib/static.js +268 -0
- package/lib/storage.js +470 -0
- package/lib/subject.js +281 -0
- package/lib/template.js +781 -0
- package/lib/testing.js +621 -0
- package/lib/totp.js +285 -0
- package/lib/tracing.js +484 -0
- package/lib/validate-opts.js +56 -0
- package/lib/vault/index.js +299 -0
- package/lib/vault/passphrase-ops.js +311 -0
- package/lib/vault/passphrase-source.js +198 -0
- package/lib/vault/rotate.js +761 -0
- package/lib/vault/wrap.js +289 -0
- package/lib/vendor/MANIFEST.json +84 -0
- package/lib/vendor/argon2/argon2.cjs +466 -0
- package/lib/vendor/argon2/argon2.d.cts +62 -0
- package/lib/vendor/argon2/package.json +1 -0
- package/lib/vendor/argon2/prebuilds/darwin-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/darwin-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/freebsd-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/freebsd-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-x64/argon2.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/win32-x64/argon2.glibc.node +0 -0
- package/lib/vendor/noble-ciphers.cjs +9 -0
- package/lib/vendor/pki.cjs +181 -0
- package/lib/vendor/simplewebauthn-server.cjs +328 -0
- package/lib/webhook.js +632 -0
- package/lib/websocket-channels.js +413 -0
- package/lib/websocket.js +833 -0
- package/package.json +39 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* mtls-engine-default — pure-JS X.509 engine wired into b.mtlsCa.
|
|
4
|
+
*
|
|
5
|
+
* Implements the engine contract documented at the top of lib/mtls-ca.js:
|
|
6
|
+
* generateCa({ generation }) -> { caCertPem, caKeyPem }
|
|
7
|
+
* signClientCert({ cn, validityDays,
|
|
8
|
+
* caCertPem, caKeyPem }) -> { cert, key, ca, issuedAt, expiresAt }
|
|
9
|
+
* packageP12({ cn, password, validityDays,
|
|
10
|
+
* caCertPem, caKeyPem }) -> { p12, certPem, issuedAt, expiresAt }
|
|
11
|
+
*
|
|
12
|
+
* Backed by lib/vendor/pki.cjs (vendored @peculiar/x509 + pkijs +
|
|
13
|
+
* reflect-metadata + ASN.1 schema chain). node:crypto.webcrypto is bound
|
|
14
|
+
* inside the bundle entry; nothing here calls openssl CLI.
|
|
15
|
+
*
|
|
16
|
+
* Algorithm envelope:
|
|
17
|
+
* CA + leaf signatures: ECDSA P-384 + SHA-384
|
|
18
|
+
* PKCS#12 key bag : PBES2 + AES-256-CBC + PBKDF2-HMAC-SHA-512, 2,000,000 iter
|
|
19
|
+
* PKCS#12 cert bag : same as key bag
|
|
20
|
+
* PKCS#12 outer MAC : HMAC-SHA-512 + PBKDF2, 2,000,000 iter
|
|
21
|
+
*
|
|
22
|
+
* The X.509 ecosystem doesn't yet accept SLH-DSA / ML-DSA on shipping
|
|
23
|
+
* client certs, so the cert sigs stay classical ECDSA-P384 — matching
|
|
24
|
+
* the framework's hybrid KEM posture rather than its standalone PQ
|
|
25
|
+
* signing posture. Swap atomically when browsers + OS cert stores can
|
|
26
|
+
* verify a PQ algorithm; bump CA_GENERATION on the same release so
|
|
27
|
+
* b.mtlsCa.status reports legacy correctly.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
var nodeCrypto = require("node:crypto");
|
|
31
|
+
|
|
32
|
+
var pki = require("./vendor/pki.cjs");
|
|
33
|
+
|
|
34
|
+
var C = require("./constants");
|
|
35
|
+
var { FrameworkError } = require("./framework-error");
|
|
36
|
+
|
|
37
|
+
var x509 = pki.x509;
|
|
38
|
+
var pkijs = pki.pkijs;
|
|
39
|
+
var webcrypto = pki.crypto;
|
|
40
|
+
|
|
41
|
+
class MtlsEngineError extends FrameworkError {
|
|
42
|
+
constructor(code, message) {
|
|
43
|
+
super(message, code);
|
|
44
|
+
this.name = "MtlsEngineError";
|
|
45
|
+
this.permanent = true;
|
|
46
|
+
this.isMtlsEngineError = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
var CA_KEY_ALG = { name: "ECDSA", namedCurve: "P-384" };
|
|
51
|
+
var CA_SIG_ALG = { name: "ECDSA", hash: "SHA-384" };
|
|
52
|
+
var CA_KEY_USAGES = ["sign", "verify"];
|
|
53
|
+
|
|
54
|
+
var P12_CONTENT_ENC = { name: "AES-CBC", length: 256 };
|
|
55
|
+
var P12_KDF_HASH = "SHA-512";
|
|
56
|
+
var P12_MAC_HASH = "SHA-512";
|
|
57
|
+
var P12_ITER = 2000000;
|
|
58
|
+
|
|
59
|
+
var CA_VALIDITY_DAYS = 10 * 365; // 10y CA lifetime
|
|
60
|
+
var LEAF_DEFAULT_DAYS = 365;
|
|
61
|
+
var DEFAULT_CA_NAME = "blamejs CA";
|
|
62
|
+
var BAG_ID_KEY = "1.2.840.113549.1.12.10.1.2"; // pkcs-12-pkcs-8ShroudedKeyBag
|
|
63
|
+
var BAG_ID_CERT = "1.2.840.113549.1.12.10.1.3"; // pkcs-12-certBag
|
|
64
|
+
var EKU_CLIENT_AUTH_OID = "1.3.6.1.5.5.7.3.2";
|
|
65
|
+
|
|
66
|
+
function _pemBlock(label, der) {
|
|
67
|
+
var b64 = Buffer.from(der).toString("base64");
|
|
68
|
+
return "-----BEGIN " + label + "-----\n" + b64.match(/.{1,64}/g).join("\n") + "\n-----END " + label + "-----\n";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function _exportKeyPairToPem(keyPair) {
|
|
72
|
+
var pkcs8 = await webcrypto.subtle.exportKey("pkcs8", keyPair.privateKey);
|
|
73
|
+
var spki = await webcrypto.subtle.exportKey("spki", keyPair.publicKey);
|
|
74
|
+
return {
|
|
75
|
+
privatePem: _pemBlock("PRIVATE KEY", pkcs8),
|
|
76
|
+
publicPem: _pemBlock("PUBLIC KEY", spki),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Import a PEM private key regardless of its on-disk encoding.
|
|
81
|
+
// Existing keys may be SEC1, PKCS#1, or PKCS#8 — Node's createPrivateKey
|
|
82
|
+
// normalises all three; webcrypto.importKey then reads the PKCS#8 DER.
|
|
83
|
+
async function _importPemPrivateKey(pem, alg, usages, extractable) {
|
|
84
|
+
var keyObj = nodeCrypto.createPrivateKey(pem);
|
|
85
|
+
var pkcs8 = keyObj.export({ format: "der", type: "pkcs8" });
|
|
86
|
+
return webcrypto.subtle.importKey("pkcs8", pkcs8, alg, !!extractable, usages);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function _parseCertPem(pem) {
|
|
90
|
+
return new x509.X509Certificate(pem);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function _normaliseCn(cn) {
|
|
94
|
+
var s = String(cn || "").replace(/[^a-zA-Z0-9_.-]/g, "").slice(0, 63);
|
|
95
|
+
if (!s) {
|
|
96
|
+
throw new MtlsEngineError("mtls-engine/bad-cn",
|
|
97
|
+
"cn must contain at least one [A-Za-z0-9._-] character (post-sanitisation)");
|
|
98
|
+
}
|
|
99
|
+
return s;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function generateCa(opts) {
|
|
103
|
+
opts = opts || {};
|
|
104
|
+
var generation = (typeof opts.generation === "number" && opts.generation >= 1)
|
|
105
|
+
? Math.floor(opts.generation) : 1;
|
|
106
|
+
var caName = opts.name || DEFAULT_CA_NAME;
|
|
107
|
+
|
|
108
|
+
var keys = await webcrypto.subtle.generateKey(CA_KEY_ALG, true, CA_KEY_USAGES);
|
|
109
|
+
var now = new Date();
|
|
110
|
+
var ca = await x509.X509CertificateGenerator.createSelfSigned({
|
|
111
|
+
serialNumber: nodeCrypto.randomBytes(16).toString("hex"),
|
|
112
|
+
name: "CN=" + caName + ",OU=CAv" + generation,
|
|
113
|
+
notBefore: now,
|
|
114
|
+
notAfter: new Date(now.getTime() + C.TIME.days(CA_VALIDITY_DAYS)),
|
|
115
|
+
signingAlgorithm: CA_SIG_ALG,
|
|
116
|
+
keys: keys,
|
|
117
|
+
extensions: [
|
|
118
|
+
new x509.BasicConstraintsExtension(true, 0, true),
|
|
119
|
+
new x509.KeyUsagesExtension(
|
|
120
|
+
x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign,
|
|
121
|
+
true
|
|
122
|
+
),
|
|
123
|
+
],
|
|
124
|
+
});
|
|
125
|
+
var pem = await _exportKeyPairToPem(keys);
|
|
126
|
+
return { caCertPem: ca.toString("pem"), caKeyPem: pem.privatePem };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function signClientCert(opts) {
|
|
130
|
+
opts = opts || {};
|
|
131
|
+
if (typeof opts.cn !== "string" || !opts.caCertPem || !opts.caKeyPem) {
|
|
132
|
+
throw new MtlsEngineError("mtls-engine/missing-arg",
|
|
133
|
+
"signClientCert requires { cn, caCertPem, caKeyPem }");
|
|
134
|
+
}
|
|
135
|
+
var validityDays = (typeof opts.validityDays === "number" && opts.validityDays > 0)
|
|
136
|
+
? Math.floor(opts.validityDays) : LEAF_DEFAULT_DAYS;
|
|
137
|
+
var cn = _normaliseCn(opts.cn);
|
|
138
|
+
|
|
139
|
+
var caKey = await _importPemPrivateKey(opts.caKeyPem, CA_KEY_ALG, ["sign"]);
|
|
140
|
+
var caCert = _parseCertPem(opts.caCertPem);
|
|
141
|
+
var clientKeys = await webcrypto.subtle.generateKey(CA_KEY_ALG, true, CA_KEY_USAGES);
|
|
142
|
+
|
|
143
|
+
var now = new Date();
|
|
144
|
+
var notAfter = new Date(now.getTime() + C.TIME.days(validityDays));
|
|
145
|
+
var clientCert = await x509.X509CertificateGenerator.create({
|
|
146
|
+
serialNumber: nodeCrypto.randomBytes(16).toString("hex"),
|
|
147
|
+
subject: "CN=" + cn,
|
|
148
|
+
issuer: caCert.subject,
|
|
149
|
+
notBefore: now,
|
|
150
|
+
notAfter: notAfter,
|
|
151
|
+
signingAlgorithm: CA_SIG_ALG,
|
|
152
|
+
publicKey: clientKeys.publicKey,
|
|
153
|
+
signingKey: caKey,
|
|
154
|
+
extensions: [
|
|
155
|
+
new x509.BasicConstraintsExtension(false, undefined, true),
|
|
156
|
+
new x509.KeyUsagesExtension(
|
|
157
|
+
x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment,
|
|
158
|
+
true
|
|
159
|
+
),
|
|
160
|
+
new x509.ExtendedKeyUsageExtension([EKU_CLIENT_AUTH_OID], true),
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
var pem = await _exportKeyPairToPem(clientKeys);
|
|
164
|
+
return {
|
|
165
|
+
cert: clientCert.toString("pem"),
|
|
166
|
+
key: pem.privatePem,
|
|
167
|
+
ca: opts.caCertPem,
|
|
168
|
+
issuedAt: now.toISOString(),
|
|
169
|
+
expiresAt: notAfter.toISOString(),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function packageP12(opts) {
|
|
174
|
+
opts = opts || {};
|
|
175
|
+
if (typeof opts.password !== "string" || opts.password.length < 1) {
|
|
176
|
+
throw new MtlsEngineError("mtls-engine/no-password",
|
|
177
|
+
"packageP12 requires opts.password (non-empty string)");
|
|
178
|
+
}
|
|
179
|
+
var leaf = await signClientCert(opts);
|
|
180
|
+
|
|
181
|
+
// Re-import the leaf key as extractable so we can re-export PKCS#8 DER
|
|
182
|
+
// for the shrouded key bag.
|
|
183
|
+
var leafKey = await _importPemPrivateKey(leaf.key, CA_KEY_ALG, ["sign"], true);
|
|
184
|
+
var leafPkcs8 = await webcrypto.subtle.exportKey("pkcs8", leafKey);
|
|
185
|
+
var privateKeyInfo = pkijs.PrivateKeyInfo.fromBER(leafPkcs8);
|
|
186
|
+
|
|
187
|
+
var leafX509 = _parseCertPem(leaf.cert);
|
|
188
|
+
var caX509 = _parseCertPem(leaf.ca);
|
|
189
|
+
var leafPkijsCert = pkijs.Certificate.fromBER(leafX509.rawData);
|
|
190
|
+
var caPkijsCert = pkijs.Certificate.fromBER(caX509.rawData);
|
|
191
|
+
|
|
192
|
+
var passwordBuf = Buffer.from(opts.password, "utf8");
|
|
193
|
+
|
|
194
|
+
var pfx = new pkijs.PFX({
|
|
195
|
+
parsedValue: {
|
|
196
|
+
integrityMode: 0, // PasswordMode (outer HMAC-PBKDF2)
|
|
197
|
+
authenticatedSafe: new pkijs.AuthenticatedSafe({
|
|
198
|
+
parsedValue: {
|
|
199
|
+
safeContents: [
|
|
200
|
+
{
|
|
201
|
+
privacyMode: 1, // PasswordPrivacyMode (PBES2)
|
|
202
|
+
value: new pkijs.SafeContents({
|
|
203
|
+
safeBags: [
|
|
204
|
+
new pkijs.SafeBag({
|
|
205
|
+
bagId: BAG_ID_KEY,
|
|
206
|
+
bagValue: new pkijs.PKCS8ShroudedKeyBag({ parsedValue: privateKeyInfo }),
|
|
207
|
+
}),
|
|
208
|
+
],
|
|
209
|
+
}),
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
privacyMode: 1,
|
|
213
|
+
value: new pkijs.SafeContents({
|
|
214
|
+
safeBags: [
|
|
215
|
+
new pkijs.SafeBag({
|
|
216
|
+
bagId: BAG_ID_CERT,
|
|
217
|
+
bagValue: new pkijs.CertBag({ parsedValue: leafPkijsCert }),
|
|
218
|
+
}),
|
|
219
|
+
new pkijs.SafeBag({
|
|
220
|
+
bagId: BAG_ID_CERT,
|
|
221
|
+
bagValue: new pkijs.CertBag({ parsedValue: caPkijsCert }),
|
|
222
|
+
}),
|
|
223
|
+
],
|
|
224
|
+
}),
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
},
|
|
228
|
+
}),
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Inner protection on the shrouded-key bag itself.
|
|
233
|
+
await pfx.parsedValue.authenticatedSafe.parsedValue.safeContents[0]
|
|
234
|
+
.value.safeBags[0].bagValue.makeInternalValues({
|
|
235
|
+
password: passwordBuf,
|
|
236
|
+
contentEncryptionAlgorithm: P12_CONTENT_ENC,
|
|
237
|
+
hmacHashAlgorithm: P12_KDF_HASH,
|
|
238
|
+
iterationCount: P12_ITER,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Encrypt each SafeContents envelope.
|
|
242
|
+
await pfx.parsedValue.authenticatedSafe.makeInternalValues({
|
|
243
|
+
safeContents: [
|
|
244
|
+
{ password: passwordBuf, contentEncryptionAlgorithm: P12_CONTENT_ENC, hmacHashAlgorithm: P12_KDF_HASH, iterationCount: P12_ITER },
|
|
245
|
+
{ password: passwordBuf, contentEncryptionAlgorithm: P12_CONTENT_ENC, hmacHashAlgorithm: P12_KDF_HASH, iterationCount: P12_ITER },
|
|
246
|
+
],
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Outer integrity MAC.
|
|
250
|
+
await pfx.makeInternalValues({
|
|
251
|
+
password: passwordBuf,
|
|
252
|
+
iterations: P12_ITER,
|
|
253
|
+
pbkdf2HashAlgorithm: P12_KDF_HASH,
|
|
254
|
+
hmacHashAlgorithm: P12_MAC_HASH,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
p12: Buffer.from(pfx.toSchema().toBER(false)),
|
|
259
|
+
certPem: leaf.cert,
|
|
260
|
+
issuedAt: leaf.issuedAt,
|
|
261
|
+
expiresAt: leaf.expiresAt,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function algorithmEnvelope() {
|
|
266
|
+
return {
|
|
267
|
+
cert: { keyAlg: CA_KEY_ALG, sigAlg: CA_SIG_ALG },
|
|
268
|
+
p12: {
|
|
269
|
+
contentEncryption: P12_CONTENT_ENC,
|
|
270
|
+
kdfHash: P12_KDF_HASH,
|
|
271
|
+
macHash: P12_MAC_HASH,
|
|
272
|
+
iterationCount: P12_ITER,
|
|
273
|
+
},
|
|
274
|
+
caValidityDays: CA_VALIDITY_DAYS,
|
|
275
|
+
leafDefaultDays: LEAF_DEFAULT_DAYS,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
module.exports = {
|
|
280
|
+
generateCa: generateCa,
|
|
281
|
+
signClientCert: signClientCert,
|
|
282
|
+
packageP12: packageP12,
|
|
283
|
+
algorithmEnvelope: algorithmEnvelope,
|
|
284
|
+
MtlsEngineError: MtlsEngineError,
|
|
285
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* nonce-store — pluggable replay-protection store.
|
|
4
|
+
*
|
|
5
|
+
* The api-encrypt middleware and any other surface that needs
|
|
6
|
+
* "first-time-only" semantics for a request-supplied nonce uses this
|
|
7
|
+
* primitive. The store is shaped around two operations:
|
|
8
|
+
*
|
|
9
|
+
* await store.checkAndInsert(nonce, expireAt) → boolean
|
|
10
|
+
* Returns true if the nonce was unseen (and is now recorded);
|
|
11
|
+
* returns false if it was already present (= replay attempt).
|
|
12
|
+
* The check + insert MUST be atomic — checking first and
|
|
13
|
+
* inserting second would race on concurrent requests carrying
|
|
14
|
+
* the same nonce.
|
|
15
|
+
*
|
|
16
|
+
* await store.purgeExpired() → number
|
|
17
|
+
* Removes entries past their expireAt. Called periodically by the
|
|
18
|
+
* middleware that uses the store. Returns the count purged.
|
|
19
|
+
*
|
|
20
|
+
* store.close()
|
|
21
|
+
* Releases any timer / pool resources. Memory backend stops its
|
|
22
|
+
* periodic sweep; cluster backend is a no-op (table prune is
|
|
23
|
+
* called explicitly).
|
|
24
|
+
*
|
|
25
|
+
* Backends:
|
|
26
|
+
*
|
|
27
|
+
* 'memory' (default) — Map-backed; periodic sweep evicts expired
|
|
28
|
+
* entries. Single-process accuracy only — a request hitting
|
|
29
|
+
* node A and a replay hitting node B will NOT be caught.
|
|
30
|
+
*
|
|
31
|
+
* 'cluster' — INSERT...ON CONFLICT DO NOTHING into the framework
|
|
32
|
+
* table _blamejs_api_encrypt_nonces. The PRIMARY KEY race is what
|
|
33
|
+
* makes the check + insert atomic across nodes.
|
|
34
|
+
*
|
|
35
|
+
* { checkAndInsert, purgeExpired, close } — operator-supplied
|
|
36
|
+
* custom backend (Redis SETNX, Memcached add, etc.). Use this
|
|
37
|
+
* for cross-process accuracy without a per-request SQL hop.
|
|
38
|
+
*
|
|
39
|
+
* Sweep cadence: memory backend sweeps every opts.sweepIntervalMs
|
|
40
|
+
* (default 5 minutes). Cluster backend's purgeExpired is invoked by
|
|
41
|
+
* the api-encrypt middleware on a rate-limited schedule.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
var clusterStorage = require("./cluster-storage");
|
|
45
|
+
var C = require("./constants");
|
|
46
|
+
var { defineClass } = require("./framework-error");
|
|
47
|
+
|
|
48
|
+
var NonceStoreError = defineClass("NonceStoreError");
|
|
49
|
+
|
|
50
|
+
var DEFAULT_SWEEP_INTERVAL_MS = C.TIME.minutes(5);
|
|
51
|
+
|
|
52
|
+
function _err(code, message) {
|
|
53
|
+
return new NonceStoreError(code, message, true);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---- Memory backend ----
|
|
57
|
+
|
|
58
|
+
function _memoryBackend(opts) {
|
|
59
|
+
var sweepIntervalMs = opts.sweepIntervalMs || DEFAULT_SWEEP_INTERVAL_MS;
|
|
60
|
+
var seen = new Map(); // nonce -> expireAt
|
|
61
|
+
|
|
62
|
+
var sweepTimer = setInterval(function () {
|
|
63
|
+
var now = Date.now();
|
|
64
|
+
for (var entry of seen) {
|
|
65
|
+
if (entry[1] <= now) seen.delete(entry[0]);
|
|
66
|
+
}
|
|
67
|
+
}, sweepIntervalMs);
|
|
68
|
+
if (typeof sweepTimer.unref === "function") sweepTimer.unref();
|
|
69
|
+
|
|
70
|
+
function checkAndInsert(nonce, expireAt) {
|
|
71
|
+
if (typeof nonce !== "string" || nonce.length === 0) {
|
|
72
|
+
return Promise.reject(_err("INVALID_NONCE", "nonce must be a non-empty string"));
|
|
73
|
+
}
|
|
74
|
+
if (typeof expireAt !== "number" || !Number.isFinite(expireAt)) {
|
|
75
|
+
return Promise.reject(_err("INVALID_EXPIRE", "expireAt must be a finite number (unix ms)"));
|
|
76
|
+
}
|
|
77
|
+
var existing = seen.get(nonce);
|
|
78
|
+
if (existing !== undefined && existing > Date.now()) {
|
|
79
|
+
return Promise.resolve(false); // replay
|
|
80
|
+
}
|
|
81
|
+
seen.set(nonce, expireAt);
|
|
82
|
+
return Promise.resolve(true);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function purgeExpired() {
|
|
86
|
+
var now = Date.now();
|
|
87
|
+
var removed = 0;
|
|
88
|
+
for (var entry of seen) {
|
|
89
|
+
if (entry[1] <= now) { seen.delete(entry[0]); removed++; }
|
|
90
|
+
}
|
|
91
|
+
return Promise.resolve(removed);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function close() {
|
|
95
|
+
if (sweepTimer) { clearInterval(sweepTimer); sweepTimer = null; }
|
|
96
|
+
seen.clear();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
name: "memory",
|
|
101
|
+
checkAndInsert: checkAndInsert,
|
|
102
|
+
purgeExpired: purgeExpired,
|
|
103
|
+
close: close,
|
|
104
|
+
// Test hook — direct read of the underlying Map size
|
|
105
|
+
_size: function () { return seen.size; },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---- Cluster backend ----
|
|
110
|
+
|
|
111
|
+
function _clusterBackend(_opts) {
|
|
112
|
+
async function checkAndInsert(nonce, expireAt) {
|
|
113
|
+
if (typeof nonce !== "string" || nonce.length === 0) {
|
|
114
|
+
throw _err("INVALID_NONCE", "nonce must be a non-empty string");
|
|
115
|
+
}
|
|
116
|
+
if (typeof expireAt !== "number" || !Number.isFinite(expireAt)) {
|
|
117
|
+
throw _err("INVALID_EXPIRE", "expireAt must be a finite number (unix ms)");
|
|
118
|
+
}
|
|
119
|
+
// INSERT ... ON CONFLICT DO NOTHING is the atomic primitive.
|
|
120
|
+
// rowCount === 1 → won (first sighting); rowCount === 0 → lost
|
|
121
|
+
// (someone else already inserted the same nonce, i.e. replay).
|
|
122
|
+
// The middleware hashes the raw nonce before passing it here so
|
|
123
|
+
// the table only ever sees hashes, not the originals.
|
|
124
|
+
var result = await clusterStorage.execute(
|
|
125
|
+
"INSERT INTO _blamejs_api_encrypt_nonces (nonceHash, expireAt) " +
|
|
126
|
+
"VALUES (?, ?) ON CONFLICT (nonceHash) DO NOTHING",
|
|
127
|
+
[nonce, expireAt]
|
|
128
|
+
);
|
|
129
|
+
return (result && result.rowCount > 0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function purgeExpired() {
|
|
133
|
+
var result = await clusterStorage.execute(
|
|
134
|
+
"DELETE FROM _blamejs_api_encrypt_nonces WHERE expireAt <= ?",
|
|
135
|
+
[Date.now()]
|
|
136
|
+
);
|
|
137
|
+
return (result && result.rowCount) || 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function close() { /* no resources held */ }
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
name: "cluster",
|
|
144
|
+
checkAndInsert: checkAndInsert,
|
|
145
|
+
purgeExpired: purgeExpired,
|
|
146
|
+
close: close,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---- Resolution ----
|
|
151
|
+
|
|
152
|
+
function create(opts) {
|
|
153
|
+
opts = opts || {};
|
|
154
|
+
var backend = opts.backend;
|
|
155
|
+
if (backend && typeof backend === "object" && typeof backend.checkAndInsert === "function") {
|
|
156
|
+
// Operator-supplied custom backend (Redis, Memcached, etc.). Fill
|
|
157
|
+
// in any missing optional methods so the rest of the framework
|
|
158
|
+
// can call them unconditionally.
|
|
159
|
+
return Object.assign({
|
|
160
|
+
name: "custom",
|
|
161
|
+
purgeExpired: function () { return Promise.resolve(0); },
|
|
162
|
+
close: function () {},
|
|
163
|
+
}, backend);
|
|
164
|
+
}
|
|
165
|
+
if (backend === "cluster") return _clusterBackend(opts);
|
|
166
|
+
if (!backend || backend === "memory") return _memoryBackend(opts);
|
|
167
|
+
throw _err("UNKNOWN_BACKEND",
|
|
168
|
+
"nonce-store: unknown backend '" + backend +
|
|
169
|
+
"' (must be 'memory', 'cluster', or { checkAndInsert, purgeExpired, close })");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
create: create,
|
|
174
|
+
NonceStoreError: NonceStoreError,
|
|
175
|
+
_memoryBackend: _memoryBackend,
|
|
176
|
+
_clusterBackend: _clusterBackend,
|
|
177
|
+
};
|