@blamejs/core 0.6.12 → 0.6.20
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 +8 -0
- package/NOTICE +16 -0
- package/README.md +9 -8
- package/index.js +12 -0
- package/lib/api-key.js +2 -3
- package/lib/audit.js +4 -0
- package/lib/auth/password.js +449 -4
- package/lib/cache.js +3 -7
- package/lib/cli.js +598 -4
- package/lib/config-drift.js +309 -0
- package/lib/crypto-field.js +37 -0
- package/lib/crypto.js +8 -0
- package/lib/db.js +17 -2
- package/lib/dual-control.js +475 -0
- package/lib/file-type.js +265 -0
- package/lib/http-client.js +77 -0
- package/lib/internal-sha1-hibp.js +34 -0
- package/lib/middleware/csp-nonce.js +7 -4
- package/lib/middleware/index.js +2 -0
- package/lib/middleware/network-allowlist.js +199 -0
- package/lib/network-dns.js +469 -0
- package/lib/network-heartbeat.js +290 -0
- package/lib/network-nts.js +552 -0
- package/lib/network-proxy.js +246 -0
- package/lib/network-tls.js +326 -0
- package/lib/network.js +233 -0
- package/lib/notify.js +2 -3
- package/lib/ntp-check.js +50 -4
- package/lib/numeric-checks.js +40 -0
- package/lib/object-store/azure-blob.js +16 -42
- package/lib/permissions.js +223 -9
- package/lib/pqc-agent.js +4 -4
- package/lib/queue.js +5 -5
- package/lib/restore.js +5 -3
- package/lib/retention.js +439 -0
- package/lib/retry.js +3 -6
- package/lib/security-assert.js +368 -0
- package/lib/session.js +138 -8
- package/lib/slug.js +2 -3
- package/lib/ssrf-guard.js +9 -0
- package/lib/testing.js +3 -7
- package/lib/vendor/MANIFEST.json +12 -0
- package/lib/vendor/common-passwords-top-10000.txt +10000 -0
- package/lib/webhook.js +3 -6
- package/package.json +3 -2
- package/sbom.cyclonedx.json +61 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* config-drift — boot-time config-baseline capture + signed sidecar +
|
|
4
|
+
* next-boot drift detection.
|
|
5
|
+
*
|
|
6
|
+
* The framework's audit chain captures DATA writes (every row, every
|
|
7
|
+
* key change). It does NOT capture RUNTIME CONFIG (the operator
|
|
8
|
+
* silently changed `allowedOrigins` 3 weeks ago and we have no
|
|
9
|
+
* signal). config-drift fills that gap: at every boot, the operator
|
|
10
|
+
* passes the baseline config snapshot they want tracked. The
|
|
11
|
+
* primitive hashes it with SHA3-512, signs the digest with the audit-
|
|
12
|
+
* signing key, and writes the result to a sidecar at
|
|
13
|
+
* `<dataDir>/config-baseline.sig`. On the next boot, the sidecar is
|
|
14
|
+
* loaded + verified + diffed against the new snapshot. Drift surfaces
|
|
15
|
+
* as an audit event (`config.drift.detected`); no boot block — the
|
|
16
|
+
* operator may have a legitimate reason to change config and the
|
|
17
|
+
* framework's job is to make the change auditable, not to refuse to
|
|
18
|
+
* start.
|
|
19
|
+
*
|
|
20
|
+
* var configDrift = b.configDrift.create({
|
|
21
|
+
* dataDir: "/data",
|
|
22
|
+
* audit: b.audit,
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* await configDrift.checkpoint({
|
|
26
|
+
* // operator decides what's tracked. JSON-stringifiable.
|
|
27
|
+
* allowedOrigins: ["https://app.example.com"],
|
|
28
|
+
* csp: "default-src 'self'",
|
|
29
|
+
* auditMode: b.audit.getMode(),
|
|
30
|
+
* vaultMode: b.vault.getMode(),
|
|
31
|
+
* dbAtRest: b.db.getAtRestMode(),
|
|
32
|
+
* });
|
|
33
|
+
* // → { signed: true, drifted: false, previousAt: 1730000000000 }
|
|
34
|
+
*
|
|
35
|
+
* The signed sidecar uses b.auditSign — same SLH-DSA-SHAKE-256f keypair
|
|
36
|
+
* the audit chain anchors on. An attacker who flips a config value
|
|
37
|
+
* would also need to forge the signing key to update the sidecar
|
|
38
|
+
* cleanly; otherwise next-boot verify catches the tamper.
|
|
39
|
+
*
|
|
40
|
+
* Validation:
|
|
41
|
+
* - create() opts: throw at boot on bad shape
|
|
42
|
+
* - checkpoint() snapshot: must be a JSON-serialisable object
|
|
43
|
+
* - sidecar verify failure (tampered, key rotated, missing pubkey)
|
|
44
|
+
* surfaces as `config.baseline.tamper` audit event AND the call
|
|
45
|
+
* returns { tamper: true } so the operator can decide whether
|
|
46
|
+
* to refuse boot
|
|
47
|
+
*/
|
|
48
|
+
var fs = require("node:fs");
|
|
49
|
+
var path = require("node:path");
|
|
50
|
+
var auditSign = require("./audit-sign");
|
|
51
|
+
var crypto = require("./crypto");
|
|
52
|
+
var lazyRequire = require("./lazy-require");
|
|
53
|
+
var validateOpts = require("./validate-opts");
|
|
54
|
+
var { defineClass } = require("./framework-error");
|
|
55
|
+
|
|
56
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
57
|
+
|
|
58
|
+
var ConfigDriftError = defineClass("ConfigDriftError", { alwaysPermanent: true });
|
|
59
|
+
var _err = ConfigDriftError.factory;
|
|
60
|
+
|
|
61
|
+
var SIDECAR_NAME = "config-baseline.sig";
|
|
62
|
+
var SIDECAR_VERSION = 1;
|
|
63
|
+
|
|
64
|
+
// Stable JSON serialization: deterministic key order so the same
|
|
65
|
+
// snapshot always hashes to the same digest. Without this, an object
|
|
66
|
+
// reordered between boots would falsely flag as drift.
|
|
67
|
+
function _stableStringify(value) {
|
|
68
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
69
|
+
if (Array.isArray(value)) {
|
|
70
|
+
return "[" + value.map(_stableStringify).join(",") + "]";
|
|
71
|
+
}
|
|
72
|
+
var keys = Object.keys(value).sort();
|
|
73
|
+
var pairs = [];
|
|
74
|
+
for (var i = 0; i < keys.length; i++) {
|
|
75
|
+
pairs.push(JSON.stringify(keys[i]) + ":" + _stableStringify(value[keys[i]]));
|
|
76
|
+
}
|
|
77
|
+
return "{" + pairs.join(",") + "}";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function _hashSnapshot(snapshot) {
|
|
81
|
+
return crypto.sha3Hash(_stableStringify(snapshot));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function _diffShallow(prev, next) {
|
|
85
|
+
// For drift reporting only — names the keys that changed without
|
|
86
|
+
// dumping the full snapshot (which may carry sensitive values like
|
|
87
|
+
// a CSP nonce-derivation key). Operators reading the drift event
|
|
88
|
+
// get "these keys changed" + a hash on each side.
|
|
89
|
+
var changed = [];
|
|
90
|
+
var added = [];
|
|
91
|
+
var removed = [];
|
|
92
|
+
var allKeys = {};
|
|
93
|
+
Object.keys(prev || {}).forEach(function (k) { allKeys[k] = true; });
|
|
94
|
+
Object.keys(next || {}).forEach(function (k) { allKeys[k] = true; });
|
|
95
|
+
Object.keys(allKeys).forEach(function (k) {
|
|
96
|
+
var inPrev = Object.prototype.hasOwnProperty.call(prev || {}, k);
|
|
97
|
+
var inNext = Object.prototype.hasOwnProperty.call(next || {}, k);
|
|
98
|
+
if (inPrev && !inNext) { removed.push(k); return; }
|
|
99
|
+
if (!inPrev && inNext) { added.push(k); return; }
|
|
100
|
+
if (_stableStringify(prev[k]) !== _stableStringify(next[k])) changed.push(k);
|
|
101
|
+
});
|
|
102
|
+
return { changed: changed, added: added, removed: removed };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function create(opts) {
|
|
106
|
+
opts = opts || {};
|
|
107
|
+
validateOpts(opts, [
|
|
108
|
+
"dataDir", "audit", "baseline", "criticalKeys", "ignoreKeys",
|
|
109
|
+
], "configDrift");
|
|
110
|
+
if (typeof opts.dataDir !== "string" || opts.dataDir.length === 0) {
|
|
111
|
+
throw _err("BAD_OPT", "create: opts.dataDir is required");
|
|
112
|
+
}
|
|
113
|
+
var dataDir = opts.dataDir;
|
|
114
|
+
var auditOn = opts.audit !== false;
|
|
115
|
+
var auditInstance = (opts.audit && opts.audit !== true) ? opts.audit : null;
|
|
116
|
+
// Multi-baseline support — each operator-named baseline lives in its
|
|
117
|
+
// own sidecar so production / staging / disaster-recovery deploys
|
|
118
|
+
// each track their own drift independently.
|
|
119
|
+
var baselineName = (typeof opts.baseline === "string" && opts.baseline.length > 0)
|
|
120
|
+
? opts.baseline : "default";
|
|
121
|
+
// Critical-keys allowlist: drift in these keys raises severity to
|
|
122
|
+
// "high" in the audit emission so SIEM rules can page on them
|
|
123
|
+
// separately from cosmetic drift. null = every key is treated as
|
|
124
|
+
// high severity (the safer default).
|
|
125
|
+
var criticalKeys = Array.isArray(opts.criticalKeys) ? opts.criticalKeys.slice() : null;
|
|
126
|
+
// Ignore-keys: drift in these keys is excluded from drift detection
|
|
127
|
+
// (e.g. operator-tracked metadata that legitimately changes per
|
|
128
|
+
// boot). Captured in the snapshot but never flagged.
|
|
129
|
+
var ignoreKeys = Array.isArray(opts.ignoreKeys) ? opts.ignoreKeys.slice() : [];
|
|
130
|
+
var sidecarPath = path.join(dataDir,
|
|
131
|
+
baselineName === "default" ? SIDECAR_NAME : ("config-baseline-" + baselineName + ".sig"));
|
|
132
|
+
|
|
133
|
+
function _emit(action, info, outcome) {
|
|
134
|
+
if (!auditOn) return;
|
|
135
|
+
var sink = auditInstance || audit();
|
|
136
|
+
try {
|
|
137
|
+
sink.safeEmit({
|
|
138
|
+
action: action,
|
|
139
|
+
outcome: outcome,
|
|
140
|
+
metadata: info || {},
|
|
141
|
+
reason: info && info.reason ? info.reason : null,
|
|
142
|
+
});
|
|
143
|
+
} catch (_e) { /* audit best-effort */ }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function _readSidecar() {
|
|
147
|
+
if (!fs.existsSync(sidecarPath)) return null;
|
|
148
|
+
var raw;
|
|
149
|
+
try { raw = fs.readFileSync(sidecarPath, "utf8"); }
|
|
150
|
+
catch (_e) { return null; }
|
|
151
|
+
var parsed;
|
|
152
|
+
try { parsed = JSON.parse(raw); }
|
|
153
|
+
catch (_e) { return { unreadable: true }; }
|
|
154
|
+
if (!parsed || parsed.version !== SIDECAR_VERSION) return { unreadable: true };
|
|
155
|
+
if (typeof parsed.digestHex !== "string" || typeof parsed.signatureBase64 !== "string" ||
|
|
156
|
+
typeof parsed.publicKeyPem !== "string" || typeof parsed.snapshot !== "object") {
|
|
157
|
+
return { unreadable: true };
|
|
158
|
+
}
|
|
159
|
+
return parsed;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function _writeSidecar(snapshot, digestHex) {
|
|
163
|
+
// Sign over the digest (not the snapshot bytes directly) so the
|
|
164
|
+
// sidecar stays small even when the snapshot is large.
|
|
165
|
+
var signature = auditSign.sign(digestHex);
|
|
166
|
+
var payload = {
|
|
167
|
+
version: SIDECAR_VERSION,
|
|
168
|
+
capturedAt: Date.now(),
|
|
169
|
+
digestHex: digestHex,
|
|
170
|
+
signatureBase64: Buffer.from(signature).toString("base64"),
|
|
171
|
+
publicKeyPem: auditSign.getPublicKey(),
|
|
172
|
+
snapshot: snapshot,
|
|
173
|
+
};
|
|
174
|
+
var tmp = sidecarPath + ".tmp";
|
|
175
|
+
fs.writeFileSync(tmp, JSON.stringify(payload, null, 2));
|
|
176
|
+
fs.renameSync(tmp, sidecarPath);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function _verifySidecar(parsed) {
|
|
180
|
+
// Verify the recorded signature against the recorded public key
|
|
181
|
+
// first — this catches tampering with the digest+sig pair on its
|
|
182
|
+
// own. Then verify the digest matches a re-hash of the recorded
|
|
183
|
+
// snapshot — catches tampering with the snapshot field.
|
|
184
|
+
var sigBuf = Buffer.from(parsed.signatureBase64, "base64");
|
|
185
|
+
if (!auditSign.verify(parsed.digestHex, sigBuf, parsed.publicKeyPem)) {
|
|
186
|
+
return { ok: false, reason: "signature-invalid" };
|
|
187
|
+
}
|
|
188
|
+
if (_hashSnapshot(parsed.snapshot) !== parsed.digestHex) {
|
|
189
|
+
return { ok: false, reason: "digest-mismatch" };
|
|
190
|
+
}
|
|
191
|
+
return { ok: true };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function checkpoint(snapshot) {
|
|
195
|
+
if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) {
|
|
196
|
+
throw _err("BAD_OPT", "checkpoint: snapshot must be a plain object");
|
|
197
|
+
}
|
|
198
|
+
var newDigest = _hashSnapshot(snapshot);
|
|
199
|
+
|
|
200
|
+
var existing = _readSidecar();
|
|
201
|
+
if (existing && existing.unreadable) {
|
|
202
|
+
_emit("config.baseline.unreadable",
|
|
203
|
+
{ sidecar: sidecarPath, reason: "sidecar present but malformed or wrong version" },
|
|
204
|
+
"warning");
|
|
205
|
+
_writeSidecar(snapshot, newDigest);
|
|
206
|
+
return { signed: true, drifted: false, tamper: false, previousAt: null, reason: "sidecar-unreadable-rewritten" };
|
|
207
|
+
}
|
|
208
|
+
if (!existing) {
|
|
209
|
+
_writeSidecar(snapshot, newDigest);
|
|
210
|
+
_emit("config.baseline.captured",
|
|
211
|
+
{ digestHex: newDigest, capturedAt: Date.now() }, "success");
|
|
212
|
+
return { signed: true, drifted: false, tamper: false, previousAt: null };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
var verified = _verifySidecar(existing);
|
|
216
|
+
if (!verified.ok) {
|
|
217
|
+
_emit("config.baseline.tamper",
|
|
218
|
+
{ sidecar: sidecarPath, reason: verified.reason, previousAt: existing.capturedAt },
|
|
219
|
+
"failure");
|
|
220
|
+
// DO NOT auto-rewrite on tamper — let the operator inspect first.
|
|
221
|
+
return { signed: false, drifted: false, tamper: true, reason: verified.reason, previousAt: existing.capturedAt };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (existing.digestHex === newDigest) {
|
|
225
|
+
// No drift; refresh capturedAt so the sidecar timestamp moves
|
|
226
|
+
// forward each successful boot (operator can see "last verified
|
|
227
|
+
// clean at <T>" in the file).
|
|
228
|
+
_writeSidecar(snapshot, newDigest);
|
|
229
|
+
return { signed: true, drifted: false, tamper: false, previousAt: existing.capturedAt };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
var diff = _diffShallow(existing.snapshot, snapshot);
|
|
233
|
+
// Filter ignore-keys out of the drift report.
|
|
234
|
+
function _stripIgnored(arr) {
|
|
235
|
+
return arr.filter(function (k) { return ignoreKeys.indexOf(k) === -1; });
|
|
236
|
+
}
|
|
237
|
+
diff.changed = _stripIgnored(diff.changed);
|
|
238
|
+
diff.added = _stripIgnored(diff.added);
|
|
239
|
+
diff.removed = _stripIgnored(diff.removed);
|
|
240
|
+
if (diff.changed.length === 0 && diff.added.length === 0 && diff.removed.length === 0) {
|
|
241
|
+
// All drift was in ignore-keys — refresh sidecar without
|
|
242
|
+
// emitting a drift event.
|
|
243
|
+
_writeSidecar(snapshot, newDigest);
|
|
244
|
+
return { signed: true, drifted: false, tamper: false,
|
|
245
|
+
previousAt: existing.capturedAt, ignoredOnly: true };
|
|
246
|
+
}
|
|
247
|
+
// Severity classification: HIGH when any drifted key is in
|
|
248
|
+
// criticalKeys (or no allowlist is configured — every key is
|
|
249
|
+
// critical by default). LOW when criticalKeys is set and none
|
|
250
|
+
// of the drifted keys are in it.
|
|
251
|
+
var severity = "high";
|
|
252
|
+
if (criticalKeys !== null) {
|
|
253
|
+
var anyCritical = false;
|
|
254
|
+
var allDrifted = diff.changed.concat(diff.added, diff.removed);
|
|
255
|
+
for (var di = 0; di < allDrifted.length; di++) {
|
|
256
|
+
if (criticalKeys.indexOf(allDrifted[di]) !== -1) { anyCritical = true; break; }
|
|
257
|
+
}
|
|
258
|
+
severity = anyCritical ? "high" : "low";
|
|
259
|
+
}
|
|
260
|
+
_emit("config.drift.detected",
|
|
261
|
+
{
|
|
262
|
+
baseline: baselineName,
|
|
263
|
+
previousDigestHex: existing.digestHex,
|
|
264
|
+
currentDigestHex: newDigest,
|
|
265
|
+
previousAt: existing.capturedAt,
|
|
266
|
+
keysChanged: diff.changed,
|
|
267
|
+
keysAdded: diff.added,
|
|
268
|
+
keysRemoved: diff.removed,
|
|
269
|
+
severity: severity,
|
|
270
|
+
},
|
|
271
|
+
severity === "high" ? "failure" : "warning");
|
|
272
|
+
_writeSidecar(snapshot, newDigest);
|
|
273
|
+
return {
|
|
274
|
+
signed: true,
|
|
275
|
+
drifted: true,
|
|
276
|
+
tamper: false,
|
|
277
|
+
severity: severity,
|
|
278
|
+
previousAt: existing.capturedAt,
|
|
279
|
+
diff: diff,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function read() {
|
|
284
|
+
var existing = _readSidecar();
|
|
285
|
+
if (!existing || existing.unreadable) return null;
|
|
286
|
+
var verified = _verifySidecar(existing);
|
|
287
|
+
return {
|
|
288
|
+
capturedAt: existing.capturedAt,
|
|
289
|
+
digestHex: existing.digestHex,
|
|
290
|
+
snapshot: existing.snapshot,
|
|
291
|
+
verified: verified.ok,
|
|
292
|
+
tamperReason: verified.ok ? null : verified.reason,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
checkpoint: checkpoint,
|
|
298
|
+
read: read,
|
|
299
|
+
sidecarPath: sidecarPath,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
module.exports = {
|
|
304
|
+
create: create,
|
|
305
|
+
ConfigDriftError: ConfigDriftError,
|
|
306
|
+
// Test-only export for hashing — operators don't need this directly.
|
|
307
|
+
_hashSnapshot: _hashSnapshot,
|
|
308
|
+
_stableStringify: _stableStringify,
|
|
309
|
+
};
|
package/lib/crypto-field.js
CHANGED
|
@@ -126,6 +126,42 @@ function unsealRow(table, row) {
|
|
|
126
126
|
return out;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
// ---- Erasure (GDPR Art. 17 / "right to be forgotten") ----
|
|
130
|
+
//
|
|
131
|
+
// eraseRow(table, row) returns a tombstoned copy of the row: every
|
|
132
|
+
// sealed column is replaced with NULL, every derived hash column
|
|
133
|
+
// (computed from a sealed source) is replaced with NULL, and a
|
|
134
|
+
// `__erasedAt` field is added carrying the erasure timestamp. The
|
|
135
|
+
// row itself stays in the table (referential integrity), but the
|
|
136
|
+
// sealed cleartext is unrecoverable — even with the vault key, NULL
|
|
137
|
+
// decrypts to NULL.
|
|
138
|
+
//
|
|
139
|
+
// Callers that need the row removed entirely should DELETE; eraseRow
|
|
140
|
+
// is for the case where downstream FKs / audit references make
|
|
141
|
+
// outright deletion infeasible.
|
|
142
|
+
function eraseRow(table, row) {
|
|
143
|
+
if (!row) return row;
|
|
144
|
+
var s = schemas[table];
|
|
145
|
+
if (!s) return row;
|
|
146
|
+
var out = Object.assign({}, row);
|
|
147
|
+
// Erase sealed columns — set to null. After this, unsealRow on the
|
|
148
|
+
// erased row returns null for these columns; no key recovers them
|
|
149
|
+
// because there's no ciphertext to decrypt.
|
|
150
|
+
for (var i = 0; i < s.sealedFields.length; i++) {
|
|
151
|
+
out[s.sealedFields[i]] = null;
|
|
152
|
+
}
|
|
153
|
+
// Erase derived hashes — they're indexed lookup mirrors of sealed
|
|
154
|
+
// sources and would otherwise let an attacker reverse the cleartext
|
|
155
|
+
// via dictionary enumeration of the hash.
|
|
156
|
+
if (s.derivedHashes) {
|
|
157
|
+
for (var derivedField in s.derivedHashes) {
|
|
158
|
+
out[derivedField] = null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
out.__erasedAt = Date.now();
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
|
|
129
165
|
// ---- Lookup translation ----
|
|
130
166
|
|
|
131
167
|
// where({ email: 'x' }) → where({ emailHash: hash(...) }).
|
|
@@ -153,6 +189,7 @@ module.exports = {
|
|
|
153
189
|
getSealedFields: getSealedFields,
|
|
154
190
|
sealRow: sealRow,
|
|
155
191
|
unsealRow: unsealRow,
|
|
192
|
+
eraseRow: eraseRow,
|
|
156
193
|
computeDerived: computeDerived,
|
|
157
194
|
lookupHash: lookupHash,
|
|
158
195
|
clearForTest: clearForTest,
|
package/lib/crypto.js
CHANGED
|
@@ -80,6 +80,14 @@ function timingSafeEqual(a, b) {
|
|
|
80
80
|
function sha3Hash(data) { return hash(data, "sha3-512").toString("hex"); }
|
|
81
81
|
function hmacSha3(key, data) { return hmac(key, data, "sha3-512"); }
|
|
82
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/internal-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
|
+
|
|
83
91
|
// ---- KDF ----
|
|
84
92
|
function kdf(input, outputLength) { return hash(input, "shake256", outputLength); }
|
|
85
93
|
|
package/lib/db.js
CHANGED
|
@@ -1037,11 +1037,26 @@ async function _runNtpBootCheck(opts) {
|
|
|
1037
1037
|
try { ntpCheck = require("./ntp-check"); }
|
|
1038
1038
|
catch (_e) { return; /* module not present — skip silently */ }
|
|
1039
1039
|
|
|
1040
|
+
var envServersRaw = safeEnv.readVar("BLAMEJS_NTP_SERVERS", { default: "" });
|
|
1041
|
+
var envTimeout = safeEnv.readVar("BLAMEJS_NTP_TIMEOUT_MS", { default: "" });
|
|
1042
|
+
var envWarn = safeEnv.readVar("BLAMEJS_NTP_DRIFT_WARN_MS", { default: "" });
|
|
1043
|
+
var envFatal = safeEnv.readVar("BLAMEJS_NTP_DRIFT_FATAL_MS", { default: "" });
|
|
1044
|
+
var resolvedServers = (opts && opts.ntpServers) ||
|
|
1045
|
+
(envServersRaw ? envServersRaw.split(",").map(function (s) { return s.trim(); }).filter(Boolean) : undefined);
|
|
1046
|
+
var resolvedTimeout = (opts && opts.ntpTimeoutMs) ||
|
|
1047
|
+
(envTimeout ? parseInt(envTimeout, 10) : undefined);
|
|
1048
|
+
if (envWarn || envFatal) {
|
|
1049
|
+
var thr = {};
|
|
1050
|
+
if (envWarn) thr.warnMs = parseInt(envWarn, 10);
|
|
1051
|
+
if (envFatal) thr.fatalMs = parseInt(envFatal, 10);
|
|
1052
|
+
try { ntpCheck.setThresholds(thr); } catch (_e) {}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1040
1055
|
var result;
|
|
1041
1056
|
try {
|
|
1042
1057
|
result = await ntpCheck.bootCheck({
|
|
1043
|
-
servers:
|
|
1044
|
-
timeoutMs:
|
|
1058
|
+
servers: resolvedServers,
|
|
1059
|
+
timeoutMs: resolvedTimeout,
|
|
1045
1060
|
});
|
|
1046
1061
|
} catch (e) {
|
|
1047
1062
|
log.error("ntp boot check threw unexpectedly: " + e.message + " (continuing)");
|