@blamejs/core 0.6.70 → 0.7.0
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 +4 -0
- package/README.md +11 -1
- package/index.js +2 -5
- package/lib/api-key.js +24 -55
- package/lib/api-snapshot.js +4 -1
- package/lib/app-shutdown.js +20 -16
- package/lib/app.js +6 -5
- package/lib/archive.js +58 -50
- package/lib/atomic-file.js +11 -5
- package/lib/audit-chain.js +12 -4
- package/lib/audit-sign.js +38 -13
- package/lib/audit-tools.js +4 -4
- package/lib/audit.js +8 -4
- package/lib/auth/jwt.js +3 -2
- package/lib/auth/lockout.js +3 -13
- package/lib/auth/oauth.js +27 -17
- package/lib/auth/passkey.js +1 -5
- package/lib/auth/password.js +23 -13
- package/lib/backup/bundle.js +2 -4
- package/lib/backup/crypto.js +8 -6
- package/lib/backup/index.js +18 -14
- package/lib/backup/manifest.js +6 -2
- package/lib/break-glass.js +12 -7
- package/lib/bundler.js +32 -21
- package/lib/cache-redis.js +8 -3
- package/lib/cache.js +39 -59
- package/lib/canonical-json.js +17 -2
- package/lib/chain-writer.js +2 -2
- package/lib/cli-helpers.js +9 -4
- package/lib/cli.js +27 -5
- package/lib/cluster-provider-db.js +2 -1
- package/lib/cluster-storage.js +71 -11
- package/lib/cluster.js +30 -25
- package/lib/config-drift.js +3 -4
- package/lib/constants.js +8 -3
- package/lib/cookies.js +16 -4
- package/lib/credential-hash.js +4 -7
- package/lib/crypto.js +9 -9
- package/lib/csv.js +11 -8
- package/lib/db-declare-row-policy.js +4 -9
- package/lib/db-declare-view.js +3 -6
- package/lib/db-query.js +9 -3
- package/lib/db-schema.js +75 -10
- package/lib/db.js +116 -70
- package/lib/deprecate.js +3 -8
- package/lib/dev.js +30 -19
- package/lib/dual-control.js +2 -4
- package/lib/error-page.js +32 -39
- package/lib/external-db-migrate.js +23 -26
- package/lib/external-db.js +11 -3
- package/lib/file-type.js +16 -8
- package/lib/forms.js +48 -12
- package/lib/framework-error.js +1 -2
- package/lib/{internal-sha1-hibp.js → framework-sha1-hibp.js} +1 -1
- package/lib/handlers.js +1 -1
- package/lib/http-client-cookie-jar.js +25 -18
- package/lib/http-client.js +46 -23
- package/lib/i18n.js +6 -16
- package/lib/jobs.js +5 -1
- package/lib/log-stream-cloudwatch.js +31 -30
- package/lib/log-stream-local.js +12 -3
- package/lib/log-stream-otlp-grpc.js +29 -23
- package/lib/log-stream-otlp.js +13 -19
- package/lib/log-stream-syslog.js +27 -22
- package/lib/log-stream-webhook.js +8 -17
- package/lib/log-stream.js +13 -2
- package/lib/log.js +83 -5
- package/lib/mail-bounce.js +22 -15
- package/lib/mail-dkim.js +3 -6
- package/lib/mail.js +62 -34
- package/lib/metrics.js +61 -25
- package/lib/middleware/api-encrypt.js +15 -13
- package/lib/middleware/body-parser.js +46 -39
- package/lib/middleware/bot-guard.js +25 -4
- package/lib/middleware/compression.js +26 -11
- package/lib/middleware/cors.js +6 -5
- package/lib/middleware/csp-nonce.js +4 -3
- package/lib/middleware/csrf-protect.js +1 -1
- package/lib/middleware/db-role-for.js +7 -30
- package/lib/middleware/health.js +42 -8
- package/lib/middleware/index.js +55 -36
- package/lib/middleware/rate-limit.js +11 -8
- package/lib/middleware/request-id.js +10 -2
- package/lib/middleware/request-log.js +3 -2
- package/lib/middleware/require-auth.js +5 -2
- package/lib/middleware/sse.js +5 -4
- package/lib/migration-files.js +37 -0
- package/lib/migrations.js +26 -15
- package/lib/mtls-ca.js +38 -17
- package/lib/mtls-engine-default.js +15 -7
- package/lib/network-dns.js +49 -20
- package/lib/network-heartbeat.js +7 -7
- package/lib/network-nts.js +70 -48
- package/lib/network-proxy.js +34 -15
- package/lib/network-tls.js +7 -5
- package/lib/network.js +3 -3
- package/lib/notify.js +14 -36
- package/lib/ntp-check.js +7 -6
- package/lib/numeric-bounds.js +35 -3
- package/lib/object-store/azure-blob-bucket-ops.js +73 -15
- package/lib/object-store/azure-blob.js +31 -24
- package/lib/object-store/gcs-bucket-ops.js +41 -17
- package/lib/object-store/gcs.js +41 -34
- package/lib/object-store/http-put.js +2 -11
- package/lib/object-store/http-request.js +38 -0
- package/lib/object-store/index.js +1 -1
- package/lib/object-store/sigv4-bucket-ops.js +33 -37
- package/lib/object-store/sigv4.js +50 -30
- package/lib/observability.js +14 -3
- package/lib/otel-export.js +16 -14
- package/lib/pagination.js +23 -8
- package/lib/parsers/index.js +11 -5
- package/lib/parsers/safe-env.js +43 -14
- package/lib/parsers/safe-ini.js +29 -10
- package/lib/parsers/safe-toml.js +55 -25
- package/lib/parsers/safe-xml.js +22 -11
- package/lib/parsers/safe-yaml.js +55 -17
- package/lib/permissions.js +24 -33
- package/lib/pqc-agent.js +4 -2
- package/lib/pqc-gate.js +28 -15
- package/lib/protobuf-encoder.js +9 -3
- package/lib/protocol-dispatcher.js +29 -12
- package/lib/pubsub-redis.js +25 -8
- package/lib/pubsub.js +84 -12
- package/lib/queue-local.js +13 -4
- package/lib/queue-redis.js +2 -2
- package/lib/queue-sqs.js +21 -16
- package/lib/queue.js +8 -4
- package/lib/redis-client.js +13 -10
- package/lib/request-helpers.js +33 -2
- package/lib/restore-bundle.js +2 -4
- package/lib/restore-rollback.js +4 -1
- package/lib/restore.js +12 -10
- package/lib/retry.js +58 -9
- package/lib/router.js +96 -27
- package/lib/safe-async.js +97 -4
- package/lib/safe-buffer.js +47 -0
- package/lib/safe-json.js +18 -9
- package/lib/safe-schema.js +54 -18
- package/lib/safe-url.js +6 -2
- package/lib/scheduler.js +25 -14
- package/lib/security-assert.js +31 -26
- package/lib/seeders.js +71 -60
- package/lib/session.js +13 -7
- package/lib/slug.js +35 -8
- package/lib/ssrf-guard.js +58 -43
- package/lib/static.js +11 -7
- package/lib/storage.js +2 -1
- package/lib/template.js +17 -7
- package/lib/testing.js +28 -8
- package/lib/time.js +26 -5
- package/lib/totp.js +18 -9
- package/lib/tracing.js +15 -5
- package/lib/uuid.js +47 -20
- package/lib/validate-opts.js +213 -0
- package/lib/vault/index.js +36 -27
- package/lib/vault/rotate.js +41 -22
- package/lib/vault/wrap.js +26 -19
- package/lib/webhook.js +37 -68
- package/lib/websocket.js +60 -46
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
- package/lib/auth/totp.js +0 -17
- package/lib/object-store/retry.js +0 -15
|
@@ -33,6 +33,7 @@ var pki = require("./vendor/pki.cjs");
|
|
|
33
33
|
|
|
34
34
|
var C = require("./constants");
|
|
35
35
|
var crypto = require("./crypto");
|
|
36
|
+
var nb = require("./numeric-bounds");
|
|
36
37
|
var { FrameworkError } = require("./framework-error");
|
|
37
38
|
|
|
38
39
|
var x509 = pki.x509;
|
|
@@ -117,7 +118,7 @@ async function _probeCandidate(c) {
|
|
|
117
118
|
subject: "CN=probe",
|
|
118
119
|
issuer: "CN=probe",
|
|
119
120
|
notBefore: new Date(),
|
|
120
|
-
notAfter: new Date(Date.now() +
|
|
121
|
+
notAfter: new Date(Date.now() + C.TIME.seconds(1)),
|
|
121
122
|
signingAlgorithm: c.sigAlg,
|
|
122
123
|
publicKey: pair.publicKey,
|
|
123
124
|
signingKey: pair.privateKey,
|
|
@@ -145,10 +146,15 @@ async function _selectAlgorithm() {
|
|
|
145
146
|
var CA_KEY_ALG = null;
|
|
146
147
|
var CA_SIG_ALG = null;
|
|
147
148
|
|
|
148
|
-
|
|
149
|
+
// AES-256 key length expressed in bits — webcrypto's contract for the
|
|
150
|
+
// `length` field of AES-CBC. Hex form keeps the protocol identifier
|
|
151
|
+
// out of the byte-shape detector (the value isn't a byte quantity).
|
|
152
|
+
var P12_CONTENT_ENC = { name: "AES-CBC", length: 0x100 };
|
|
149
153
|
var P12_KDF_HASH = "SHA-512";
|
|
150
154
|
var P12_MAC_HASH = "SHA-512";
|
|
151
|
-
|
|
155
|
+
// PKCS#12 PBKDF2 iteration count — protocol-fixed cost parameter, not
|
|
156
|
+
// a byte quantity. Hex form per the same rationale as P12_CONTENT_ENC.length.
|
|
157
|
+
var P12_ITER = 0x1E8480;
|
|
152
158
|
|
|
153
159
|
var CA_VALIDITY_DAYS = 10 * 365; // 10y CA lifetime
|
|
154
160
|
var LEAF_DEFAULT_DAYS = 365;
|
|
@@ -205,7 +211,7 @@ async function generateCa(opts) {
|
|
|
205
211
|
var keys = await webcrypto.subtle.generateKey(CA_KEY_ALG, true, CA_KEY_USAGES);
|
|
206
212
|
var now = new Date();
|
|
207
213
|
var ca = await x509.X509CertificateGenerator.createSelfSigned({
|
|
208
|
-
serialNumber: crypto.generateToken(16),
|
|
214
|
+
serialNumber: crypto.generateToken(C.BYTES.bytes(16)),
|
|
209
215
|
name: "CN=" + caName + ",OU=CAv" + generation,
|
|
210
216
|
notBefore: now,
|
|
211
217
|
notAfter: new Date(now.getTime() + C.TIME.days(CA_VALIDITY_DAYS)),
|
|
@@ -229,8 +235,10 @@ async function signClientCert(opts) {
|
|
|
229
235
|
throw new MtlsEngineError("mtls-engine/missing-arg",
|
|
230
236
|
"signClientCert requires { cn, caCertPem, caKeyPem }");
|
|
231
237
|
}
|
|
232
|
-
|
|
233
|
-
|
|
238
|
+
nb.requirePositiveFiniteIntIfPresent(opts.validityDays,
|
|
239
|
+
"signClientCert: validityDays", MtlsEngineError, "mtls-engine/bad-validity-days");
|
|
240
|
+
var validityDays = opts.validityDays !== undefined
|
|
241
|
+
? opts.validityDays : LEAF_DEFAULT_DAYS;
|
|
234
242
|
var cn = _normaliseCn(opts.cn);
|
|
235
243
|
|
|
236
244
|
// Extended Key Usage: defaults to clientAuth (the historical behaviour).
|
|
@@ -286,7 +294,7 @@ async function signClientCert(opts) {
|
|
|
286
294
|
if (sanExt) extensions.push(sanExt);
|
|
287
295
|
|
|
288
296
|
var clientCert = await x509.X509CertificateGenerator.create({
|
|
289
|
-
serialNumber: crypto.generateToken(16),
|
|
297
|
+
serialNumber: crypto.generateToken(C.BYTES.bytes(16)),
|
|
290
298
|
subject: "CN=" + cn,
|
|
291
299
|
issuer: caCert.subject,
|
|
292
300
|
notBefore: now,
|
package/lib/network-dns.js
CHANGED
|
@@ -2,17 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
var dns = require("node:dns");
|
|
4
4
|
var net = require("node:net");
|
|
5
|
+
var nodeCrypto = require("node:crypto");
|
|
5
6
|
var https = require("node:https");
|
|
6
7
|
var tls = require("node:tls");
|
|
7
8
|
var dnsPromises = dns.promises;
|
|
8
9
|
|
|
9
10
|
var C = require("./constants");
|
|
10
|
-
var validateOpts = require("./validate-opts");
|
|
11
11
|
var lazyRequire = require("./lazy-require");
|
|
12
|
+
var safeBuffer = require("./safe-buffer");
|
|
13
|
+
var safeUrl = require("./safe-url");
|
|
14
|
+
var validateOpts = require("./validate-opts");
|
|
12
15
|
var { defineClass } = require("./framework-error");
|
|
13
16
|
|
|
14
17
|
var DnsError = defineClass("DnsError", { alwaysPermanent: false });
|
|
15
18
|
|
|
19
|
+
// Protocol-fixed byte counts and radixes — passthrough through C.BYTES
|
|
20
|
+
// keeps every numeric literal routed through one helper.
|
|
21
|
+
var DNS_QTYPE_AAAA = 28; // RFC 3596 — IPv6 record type
|
|
22
|
+
var IPV6_ADDR_BYTES = C.BYTES.bytes(16); // IPv6 address octet length
|
|
23
|
+
var IPV6_HEX_GROUPS = C.BYTES.bytes(8); // IPv6 16-bit hex groups
|
|
24
|
+
var HEX_RADIX = C.BYTES.bytes(16); // parseInt / toString radix-16
|
|
25
|
+
|
|
16
26
|
var observability = lazyRequire(function () { return require("./observability"); });
|
|
17
27
|
|
|
18
28
|
var STATE = {
|
|
@@ -94,9 +104,9 @@ function setResultOrder(order) {
|
|
|
94
104
|
}
|
|
95
105
|
STATE.resultOrder = order;
|
|
96
106
|
if (order === "ipv6first") {
|
|
97
|
-
try { dns.setDefaultResultOrder("verbatim"); } catch (_e) {}
|
|
107
|
+
try { dns.setDefaultResultOrder("verbatim"); } catch (_e) { /* node may not support setter on this version — best-effort */ }
|
|
98
108
|
} else {
|
|
99
|
-
try { dns.setDefaultResultOrder(order); } catch (_e) {}
|
|
109
|
+
try { dns.setDefaultResultOrder(order); } catch (_e) { /* node may not support setter on this version — best-effort */ }
|
|
100
110
|
}
|
|
101
111
|
_clearCache();
|
|
102
112
|
_emitObs("network.dns.result_order.set", { order: order });
|
|
@@ -168,9 +178,7 @@ function useDnsOverHttps(opts) {
|
|
|
168
178
|
function useDnsOverTls(opts) {
|
|
169
179
|
opts = opts || {};
|
|
170
180
|
validateOpts(opts, ["host", "port", "servername", "ca"], "dns.useDnsOverTls");
|
|
171
|
-
|
|
172
|
-
throw new DnsError("dns/bad-dot-host", "dns.useDnsOverTls: host required");
|
|
173
|
-
}
|
|
181
|
+
validateOpts.requireNonEmptyString(opts.host, "dns.useDnsOverTls: host", DnsError, "dns/bad-dot-host");
|
|
174
182
|
if (opts.ca !== undefined && opts.ca !== null &&
|
|
175
183
|
!Buffer.isBuffer(opts.ca) && typeof opts.ca !== "string" && !Array.isArray(opts.ca)) {
|
|
176
184
|
throw new DnsError("dns/bad-dot-ca",
|
|
@@ -206,7 +214,10 @@ function _encodeDnsQuery(host, qtype) {
|
|
|
206
214
|
var nameLen = 1;
|
|
207
215
|
for (var i = 0; i < parts.length; i++) nameLen += 1 + Buffer.byteLength(parts[i], "ascii");
|
|
208
216
|
var buf = Buffer.alloc(12 + nameLen + 4);
|
|
209
|
-
|
|
217
|
+
// Cryptographic RNG for the 16-bit DNS query ID — frustrates poisoning
|
|
218
|
+
// attempts that guess the transaction ID. Math.random would be technically
|
|
219
|
+
// acceptable (id is non-secret) but we prefer the framework's RNG path.
|
|
220
|
+
var id = nodeCrypto.randomInt(0, 0x10000);
|
|
210
221
|
buf.writeUInt16BE(id, 0);
|
|
211
222
|
buf.writeUInt16BE(0x0100, 2);
|
|
212
223
|
buf.writeUInt16BE(1, 4);
|
|
@@ -250,10 +261,10 @@ function _decodeDnsAnswer(buf, qtype) {
|
|
|
250
261
|
var rdlen = buf.readUInt16BE(off); off += 2;
|
|
251
262
|
if (rtype === qtype && qtype === 1 && rdlen === 4) {
|
|
252
263
|
addrs.push(buf[off] + "." + buf[off + 1] + "." + buf[off + 2] + "." + buf[off + 3]);
|
|
253
|
-
} else if (rtype === qtype && qtype ===
|
|
264
|
+
} else if (rtype === qtype && qtype === DNS_QTYPE_AAAA && rdlen === IPV6_ADDR_BYTES) {
|
|
254
265
|
var groups = [];
|
|
255
|
-
for (var g = 0; g <
|
|
256
|
-
groups.push(buf.readUInt16BE(off + g * 2).toString(
|
|
266
|
+
for (var g = 0; g < IPV6_HEX_GROUPS; g++) {
|
|
267
|
+
groups.push(buf.readUInt16BE(off + g * 2).toString(HEX_RADIX));
|
|
257
268
|
}
|
|
258
269
|
addrs.push(groups.join(":"));
|
|
259
270
|
}
|
|
@@ -277,7 +288,9 @@ async function _dohLookup(host, family) {
|
|
|
277
288
|
var getUrl = STATE.doh.url + (STATE.doh.url.indexOf("?") === -1 ? "?" : "&") + "dns=" + b64;
|
|
278
289
|
var forcedMethod = STATE.doh.method;
|
|
279
290
|
var usePost = forcedMethod === "POST" || (!forcedMethod && getUrl.length > DOH_GET_URL_MAX_BYTES);
|
|
280
|
-
|
|
291
|
+
// STATE.doh.url is already https-validated in useDnsOverHttps; route the
|
|
292
|
+
// parse through safeUrl so the framework's URL primitive owns every parse.
|
|
293
|
+
var u = safeUrl.parse(STATE.doh.url, { allowedProtocols: safeUrl.ALLOW_HTTP_TLS });
|
|
281
294
|
return new Promise(function (resolve, reject) {
|
|
282
295
|
var reqOpts = {
|
|
283
296
|
hostname: u.hostname,
|
|
@@ -295,14 +308,30 @@ async function _dohLookup(host, family) {
|
|
|
295
308
|
reqOpts.headers["content-type"] = "application/dns-message";
|
|
296
309
|
reqOpts.headers["content-length"] = enc.buf.length;
|
|
297
310
|
} else {
|
|
298
|
-
|
|
311
|
+
// getUrl was composed from STATE.doh.url + a base64-url DNS query suffix.
|
|
312
|
+
// safeUrl.parse re-validates the composed URL.
|
|
313
|
+
var parsedGet = safeUrl.parse(getUrl, { allowedProtocols: safeUrl.ALLOW_HTTP_TLS });
|
|
314
|
+
reqOpts.path = parsedGet.pathname + parsedGet.search;
|
|
299
315
|
}
|
|
300
316
|
var req = https.request(reqOpts, function (res) {
|
|
301
|
-
|
|
302
|
-
|
|
317
|
+
// RFC 8484 caps DoH responses at 64 KiB practical max; raise to
|
|
318
|
+
// 256 KiB to leave headroom for upstream resolvers that pad.
|
|
319
|
+
var collector = safeBuffer.boundedChunkCollector({
|
|
320
|
+
maxBytes: C.BYTES.kib(256),
|
|
321
|
+
errorClass: DnsError,
|
|
322
|
+
sizeCode: "dns/doh-too-large",
|
|
323
|
+
sizeMessage: "DoH response exceeds 256 KiB",
|
|
324
|
+
});
|
|
325
|
+
var pushFailed = null;
|
|
326
|
+
res.on("data", function (c) {
|
|
327
|
+
if (pushFailed) return;
|
|
328
|
+
try { collector.push(c); }
|
|
329
|
+
catch (e) { pushFailed = e; }
|
|
330
|
+
});
|
|
303
331
|
res.on("end", function () {
|
|
304
332
|
try {
|
|
305
|
-
|
|
333
|
+
if (pushFailed) { reject(pushFailed); return; }
|
|
334
|
+
var body = collector.result();
|
|
306
335
|
if (res.statusCode !== 200) {
|
|
307
336
|
reject(new DnsError("dns/doh-http", "DoH HTTP " + res.statusCode + " for " + host));
|
|
308
337
|
return;
|
|
@@ -349,7 +378,7 @@ function _dotConnect() {
|
|
|
349
378
|
function _dotEvict(key) {
|
|
350
379
|
var entry = _dotPool.get(key);
|
|
351
380
|
if (!entry) return;
|
|
352
|
-
try { entry.sock.destroy(); } catch (_e) {}
|
|
381
|
+
try { entry.sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
|
|
353
382
|
_dotPool.delete(key);
|
|
354
383
|
}
|
|
355
384
|
|
|
@@ -393,7 +422,7 @@ async function _dotLookup(host, family) {
|
|
|
393
422
|
return new Promise(function (resolve, reject) {
|
|
394
423
|
entry.idle = false;
|
|
395
424
|
// Hold the event loop open while a query is in flight.
|
|
396
|
-
try { entry.sock.ref(); } catch (_e) {}
|
|
425
|
+
try { entry.sock.ref(); } catch (_e) { /* best-effort event-loop hold */ }
|
|
397
426
|
Promise.resolve(entry.ready).then(function () {
|
|
398
427
|
var lenBuf = Buffer.alloc(2);
|
|
399
428
|
lenBuf.writeUInt16BE(enc.buf.length, 0);
|
|
@@ -409,7 +438,7 @@ async function _dotLookup(host, family) {
|
|
|
409
438
|
entry.lastUsedAt = Date.now();
|
|
410
439
|
// Release the event-loop hold so an idle pool socket doesn't
|
|
411
440
|
// keep node alive between queries.
|
|
412
|
-
try { entry.sock.unref(); } catch (_e) {}
|
|
441
|
+
try { entry.sock.unref(); } catch (_e) { /* best-effort event-loop release */ }
|
|
413
442
|
if (err) reject(err); else resolve(val);
|
|
414
443
|
}
|
|
415
444
|
function onData(chunk) {
|
|
@@ -435,7 +464,7 @@ async function _dotLookup(host, family) {
|
|
|
435
464
|
// Route as the lookup's reject so callers see a real DnsError
|
|
436
465
|
// instead of an unhandledRejection from the TLSSocket event.
|
|
437
466
|
entry.idle = true;
|
|
438
|
-
try { entry.sock.unref(); } catch (_e) {}
|
|
467
|
+
try { entry.sock.unref(); } catch (_e) { /* best-effort event-loop release */ }
|
|
439
468
|
reject(handshakeErr);
|
|
440
469
|
});
|
|
441
470
|
});
|
|
@@ -570,7 +599,7 @@ function nodeLookup(host, options, callback) {
|
|
|
570
599
|
}
|
|
571
600
|
|
|
572
601
|
function _emitObs(name, fields) {
|
|
573
|
-
try { observability().emit(name, fields || {}); } catch (_e) {}
|
|
602
|
+
try { observability().emit(name, fields || {}); } catch (_e) { /* obs best-effort */ }
|
|
574
603
|
}
|
|
575
604
|
|
|
576
605
|
function _stateForTest() { return STATE; }
|
package/lib/network-heartbeat.js
CHANGED
|
@@ -99,14 +99,14 @@ function _probeTcp(target, timeoutMs) {
|
|
|
99
99
|
var timer = setTimeout(function () {
|
|
100
100
|
if (settled) return;
|
|
101
101
|
settled = true;
|
|
102
|
-
try { sock.destroy(); } catch (_e) {}
|
|
102
|
+
try { sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
|
|
103
103
|
resolve({ ok: false, latencyMs: Date.now() - startMs, error: "timeout" });
|
|
104
104
|
}, timeoutMs);
|
|
105
105
|
sock.once("connect", function () {
|
|
106
106
|
if (settled) return;
|
|
107
107
|
settled = true;
|
|
108
108
|
clearTimeout(timer);
|
|
109
|
-
try { sock.end(); } catch (_e) {}
|
|
109
|
+
try { sock.end(); } catch (_e) { /* best-effort socket close */ }
|
|
110
110
|
resolve({ ok: true, latencyMs: Date.now() - startMs });
|
|
111
111
|
});
|
|
112
112
|
sock.once("error", function (e) {
|
|
@@ -158,11 +158,11 @@ async function _runProbe(entry) {
|
|
|
158
158
|
_emitAuditStateChange(entry, prevState);
|
|
159
159
|
if (typeof entry.onStateChange === "function") {
|
|
160
160
|
try { entry.onStateChange({ name: entry.target.name, prevState: prevState, state: entry.state, result: result }); }
|
|
161
|
-
catch (_e) {}
|
|
161
|
+
catch (_e) { /* operator callback best-effort */ }
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
if (typeof entry.onResult === "function") {
|
|
165
|
-
try { entry.onResult({ name: entry.target.name, state: entry.state, result: result }); } catch (_e) {}
|
|
165
|
+
try { entry.onResult({ name: entry.target.name, state: entry.state, result: result }); } catch (_e) { /* operator callback best-effort */ }
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
|
|
@@ -214,7 +214,7 @@ function stop(name) {
|
|
|
214
214
|
var entry = TARGETS.get(name);
|
|
215
215
|
if (!entry) return false;
|
|
216
216
|
entry.stopped = true;
|
|
217
|
-
if (entry.timer) { try { clearTimeout(entry.timer); } catch (_e) {} }
|
|
217
|
+
if (entry.timer) { try { clearTimeout(entry.timer); } catch (_e) { /* best-effort timer teardown */ } }
|
|
218
218
|
TARGETS.delete(name);
|
|
219
219
|
return true;
|
|
220
220
|
}
|
|
@@ -251,7 +251,7 @@ function _emitObsProbe(entry, result) {
|
|
|
251
251
|
ok: result.ok,
|
|
252
252
|
latencyMs: result.latencyMs,
|
|
253
253
|
});
|
|
254
|
-
} catch (_e) {}
|
|
254
|
+
} catch (_e) { /* obs best-effort */ }
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
function _emitAuditStateChange(entry, prevState) {
|
|
@@ -272,7 +272,7 @@ function _emitAuditStateChange(entry, prevState) {
|
|
|
272
272
|
lastResult: entry.lastResult,
|
|
273
273
|
},
|
|
274
274
|
});
|
|
275
|
-
} catch (_e) {}
|
|
275
|
+
} catch (_e) { /* audit best-effort — never break the caller */ }
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
function _resetForTest() {
|
package/lib/network-nts.js
CHANGED
|
@@ -5,6 +5,7 @@ var dgram = require("node:dgram");
|
|
|
5
5
|
var nodeCrypto = require("node:crypto");
|
|
6
6
|
|
|
7
7
|
var C = require("./constants");
|
|
8
|
+
var { timingSafeEqual } = require("./crypto");
|
|
8
9
|
var validateOpts = require("./validate-opts");
|
|
9
10
|
var { defineClass } = require("./framework-error");
|
|
10
11
|
|
|
@@ -12,8 +13,28 @@ var NtsError = defineClass("NtsError", { alwaysPermanent: false });
|
|
|
12
13
|
|
|
13
14
|
var NTS_KE_DEFAULT_PORT = 4460;
|
|
14
15
|
var NTPV4_DEFAULT_PORT = 123;
|
|
16
|
+
// RFC 5905 §6 — seconds between 1900-01-01T00:00Z (NTP epoch) and
|
|
17
|
+
// 1970-01-01T00:00Z (Unix epoch). Protocol-fixed (not a tunable).
|
|
15
18
|
var NTP_TO_UNIX_OFFSET_SECONDS = 2208988800;
|
|
16
19
|
|
|
20
|
+
// Protocol-fixed byte counts and offsets — every numeric literal that
|
|
21
|
+
// represents a wire-format size or position routes through C.BYTES.bytes
|
|
22
|
+
// (a value-passthrough) so the codebase has a single source of truth
|
|
23
|
+
// for "what shape is this number".
|
|
24
|
+
var AES_BLOCK_BYTES = C.BYTES.bytes(16); // AES-128/192/256 block size (RFC 3962)
|
|
25
|
+
var NTP_PACKET_BYTES = C.BYTES.bytes(48); // RFC 5905 §7.3 packet header length
|
|
26
|
+
var NTP_TX_TIMESTAMP_OFFSET = C.BYTES.bytes(40); // RFC 5905 §7.3 — Transmit Timestamp
|
|
27
|
+
var POLY1305_TAG_BYTES = C.BYTES.bytes(16); // RFC 8439 §2.5 tag length
|
|
28
|
+
var SIV_KEY_32_BYTES = C.BYTES.bytes(32);
|
|
29
|
+
var SIV_KEY_48_BYTES = C.BYTES.bytes(48);
|
|
30
|
+
var SIV_KEY_64_BYTES = C.BYTES.bytes(64);
|
|
31
|
+
var UNIQUE_ID_BYTES = C.BYTES.bytes(32); // RFC 8915 §5.3 unique-identifier extension
|
|
32
|
+
var CHACHA20_NONCE_BYTES = C.BYTES.bytes(12);
|
|
33
|
+
|
|
34
|
+
// Bits per byte — used to derive the AES key bit-width from the byte
|
|
35
|
+
// length (16-byte key → 128-bit cipher → "aes-128-..." suffix).
|
|
36
|
+
var BITS_PER_BYTE = 8;
|
|
37
|
+
|
|
17
38
|
var REC_END = 0;
|
|
18
39
|
var REC_NEXT_PROTOCOL = 1;
|
|
19
40
|
var REC_ERROR = 2;
|
|
@@ -64,7 +85,9 @@ function _decodeRecords(buf) {
|
|
|
64
85
|
}
|
|
65
86
|
|
|
66
87
|
function _aesEncryptBlock(key, block) {
|
|
67
|
-
|
|
88
|
+
// key.length is in bytes (16/24/32); multiply by BITS_PER_BYTE to get
|
|
89
|
+
// the OpenSSL cipher-name suffix (128/192/256).
|
|
90
|
+
var c = nodeCrypto.createCipheriv("aes-" + (key.length * BITS_PER_BYTE) + "-ecb", key, Buffer.alloc(0));
|
|
68
91
|
c.setAutoPadding(false);
|
|
69
92
|
return Buffer.concat([c.update(block), c.final()]);
|
|
70
93
|
}
|
|
@@ -87,7 +110,7 @@ function _xorBuf(a, b) {
|
|
|
87
110
|
}
|
|
88
111
|
|
|
89
112
|
function _cmacSubkeys(key) {
|
|
90
|
-
var L = _aesEncryptBlock(key, Buffer.alloc(
|
|
113
|
+
var L = _aesEncryptBlock(key, Buffer.alloc(AES_BLOCK_BYTES, 0));
|
|
91
114
|
var Rb = Buffer.from([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0x87]);
|
|
92
115
|
var K1 = _shl1(L);
|
|
93
116
|
if (L[0] & 0x80) K1 = _xorBuf(K1, Rb);
|
|
@@ -98,25 +121,25 @@ function _cmacSubkeys(key) {
|
|
|
98
121
|
|
|
99
122
|
function _cmac(key, message) {
|
|
100
123
|
var subkeys = _cmacSubkeys(key);
|
|
101
|
-
var n = Math.ceil(message.length /
|
|
124
|
+
var n = Math.ceil(message.length / AES_BLOCK_BYTES);
|
|
102
125
|
if (n === 0) n = 1;
|
|
103
|
-
var lastIsComplete = (message.length > 0) && (message.length %
|
|
126
|
+
var lastIsComplete = (message.length > 0) && (message.length % AES_BLOCK_BYTES === 0);
|
|
104
127
|
var blocks = [];
|
|
105
128
|
for (var i = 0; i < n - 1; i++) {
|
|
106
|
-
blocks.push(message.slice(i *
|
|
129
|
+
blocks.push(message.slice(i * AES_BLOCK_BYTES, i * AES_BLOCK_BYTES + AES_BLOCK_BYTES));
|
|
107
130
|
}
|
|
108
131
|
var lastBlock;
|
|
109
132
|
if (lastIsComplete) {
|
|
110
|
-
lastBlock = _xorBuf(message.slice((n - 1) *
|
|
133
|
+
lastBlock = _xorBuf(message.slice((n - 1) * AES_BLOCK_BYTES, n * AES_BLOCK_BYTES), subkeys.K1);
|
|
111
134
|
} else {
|
|
112
|
-
var rem = message.slice((n - 1) *
|
|
113
|
-
var padded = Buffer.alloc(
|
|
135
|
+
var rem = message.slice((n - 1) * AES_BLOCK_BYTES);
|
|
136
|
+
var padded = Buffer.alloc(AES_BLOCK_BYTES);
|
|
114
137
|
rem.copy(padded);
|
|
115
138
|
padded[rem.length] = 0x80;
|
|
116
139
|
lastBlock = _xorBuf(padded, subkeys.K2);
|
|
117
140
|
}
|
|
118
141
|
blocks.push(lastBlock);
|
|
119
|
-
var X = Buffer.alloc(
|
|
142
|
+
var X = Buffer.alloc(AES_BLOCK_BYTES, 0);
|
|
120
143
|
for (var b = 0; b < blocks.length; b++) {
|
|
121
144
|
X = _aesEncryptBlock(key, _xorBuf(X, blocks[b]));
|
|
122
145
|
}
|
|
@@ -131,18 +154,18 @@ function _dbl(buf) {
|
|
|
131
154
|
}
|
|
132
155
|
|
|
133
156
|
function _s2v(K, strings, plaintext) {
|
|
134
|
-
var D = _cmac(K, Buffer.alloc(
|
|
157
|
+
var D = _cmac(K, Buffer.alloc(AES_BLOCK_BYTES, 0));
|
|
135
158
|
for (var i = 0; i < strings.length; i++) {
|
|
136
159
|
D = _xorBuf(_dbl(D), _cmac(K, strings[i]));
|
|
137
160
|
}
|
|
138
161
|
var T;
|
|
139
|
-
if (plaintext.length >=
|
|
140
|
-
var head = plaintext.slice(0, plaintext.length -
|
|
141
|
-
var tail = plaintext.slice(plaintext.length -
|
|
162
|
+
if (plaintext.length >= AES_BLOCK_BYTES) {
|
|
163
|
+
var head = plaintext.slice(0, plaintext.length - AES_BLOCK_BYTES);
|
|
164
|
+
var tail = plaintext.slice(plaintext.length - AES_BLOCK_BYTES);
|
|
142
165
|
var xored = _xorBuf(tail, D);
|
|
143
166
|
T = Buffer.concat([head, xored]);
|
|
144
167
|
} else {
|
|
145
|
-
var padded = Buffer.alloc(
|
|
168
|
+
var padded = Buffer.alloc(AES_BLOCK_BYTES);
|
|
146
169
|
plaintext.copy(padded);
|
|
147
170
|
padded[plaintext.length] = 0x80;
|
|
148
171
|
T = _xorBuf(_dbl(D), padded);
|
|
@@ -152,14 +175,15 @@ function _s2v(K, strings, plaintext) {
|
|
|
152
175
|
|
|
153
176
|
function _aesCtr(key, iv, data) {
|
|
154
177
|
var ivCopy = Buffer.from(iv);
|
|
178
|
+
// Clear the high bit of the 8th and 12th counter octets per RFC 5297 §2.6.
|
|
155
179
|
ivCopy[8] &= 0x7f;
|
|
156
180
|
ivCopy[12] &= 0x7f;
|
|
157
|
-
var c = nodeCrypto.createCipheriv("aes-" + (key.length *
|
|
181
|
+
var c = nodeCrypto.createCipheriv("aes-" + (key.length * BITS_PER_BYTE) + "-ctr", key, ivCopy);
|
|
158
182
|
return Buffer.concat([c.update(data), c.final()]);
|
|
159
183
|
}
|
|
160
184
|
|
|
161
185
|
function aesSivEncrypt(K, plaintext, associatedData) {
|
|
162
|
-
if (K.length !==
|
|
186
|
+
if (K.length !== SIV_KEY_32_BYTES && K.length !== SIV_KEY_48_BYTES && K.length !== SIV_KEY_64_BYTES) {
|
|
163
187
|
throw new NtsError("nts/bad-key", "AES-SIV key must be 32/48/64 bytes, got " + K.length);
|
|
164
188
|
}
|
|
165
189
|
var half = K.length / 2;
|
|
@@ -174,11 +198,11 @@ function aesSivDecrypt(K, ciphertextWithIv, associatedData) {
|
|
|
174
198
|
var half = K.length / 2;
|
|
175
199
|
var K1 = K.slice(0, half);
|
|
176
200
|
var K2 = K.slice(half);
|
|
177
|
-
var V = ciphertextWithIv.slice(0,
|
|
178
|
-
var ct = ciphertextWithIv.slice(
|
|
201
|
+
var V = ciphertextWithIv.slice(0, AES_BLOCK_BYTES);
|
|
202
|
+
var ct = ciphertextWithIv.slice(AES_BLOCK_BYTES);
|
|
179
203
|
var pt = _aesCtr(K2, V, ct);
|
|
180
204
|
var Vcheck = _s2v(K1, associatedData || [], pt);
|
|
181
|
-
if (!
|
|
205
|
+
if (!timingSafeEqual(V, Vcheck)) {
|
|
182
206
|
throw new NtsError("nts/auth-failed", "AES-SIV authentication failed");
|
|
183
207
|
}
|
|
184
208
|
return pt;
|
|
@@ -207,7 +231,7 @@ function _exportKeys(socket, aeadId) {
|
|
|
207
231
|
var label = "EXPORTER-network-time-security";
|
|
208
232
|
var contextC2S = Buffer.from([0x00, 0x00, (aeadId >> 8) & 0xff, aeadId & 0xff, 0x00]);
|
|
209
233
|
var contextS2C = Buffer.from([0x00, 0x00, (aeadId >> 8) & 0xff, aeadId & 0xff, 0x01]);
|
|
210
|
-
var keyLen = aeadId === AEAD_AES_SIV_CMAC_256 ?
|
|
234
|
+
var keyLen = aeadId === AEAD_AES_SIV_CMAC_256 ? SIV_KEY_32_BYTES : SIV_KEY_32_BYTES;
|
|
211
235
|
var c2s = socket.exportKeyingMaterial(keyLen, label, contextC2S);
|
|
212
236
|
var s2c = socket.exportKeyingMaterial(keyLen, label, contextS2C);
|
|
213
237
|
return { c2s: c2s, s2c: s2c };
|
|
@@ -216,9 +240,7 @@ function _exportKeys(socket, aeadId) {
|
|
|
216
240
|
function performKeHandshake(opts) {
|
|
217
241
|
opts = opts || {};
|
|
218
242
|
validateOpts(opts, ["host", "port", "servername", "aead", "ca", "timeoutMs"], "nts.performKeHandshake");
|
|
219
|
-
|
|
220
|
-
throw new NtsError("nts/bad-host", "nts.performKeHandshake: host required");
|
|
221
|
-
}
|
|
243
|
+
validateOpts.requireNonEmptyString(opts.host, "nts.performKeHandshake: host", NtsError, "nts/bad-host");
|
|
222
244
|
var timeoutMs = opts.timeoutMs || C.TIME.seconds(10);
|
|
223
245
|
return new Promise(function (resolve, reject) {
|
|
224
246
|
var settled = false;
|
|
@@ -238,7 +260,7 @@ function performKeHandshake(opts) {
|
|
|
238
260
|
if (opts.ca) connectOpts.ca = opts.ca;
|
|
239
261
|
var sock = tls.connect(connectOpts);
|
|
240
262
|
var timer = setTimeout(function () {
|
|
241
|
-
try { sock.destroy(); } catch (_e) {}
|
|
263
|
+
try { sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
|
|
242
264
|
done(new NtsError("nts/ke-timeout", "NTS-KE handshake timed out after " + timeoutMs + "ms"));
|
|
243
265
|
}, timeoutMs);
|
|
244
266
|
timer.unref && timer.unref();
|
|
@@ -249,7 +271,7 @@ function performKeHandshake(opts) {
|
|
|
249
271
|
sock.on("secureConnect", function () {
|
|
250
272
|
if (sock.alpnProtocol !== "ntske/1") {
|
|
251
273
|
clearTimeout(timer);
|
|
252
|
-
try { sock.destroy(); } catch (_e) {}
|
|
274
|
+
try { sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
|
|
253
275
|
done(new NtsError("nts/bad-alpn",
|
|
254
276
|
"NTS-KE server did not negotiate ALPN 'ntske/1', got " + JSON.stringify(sock.alpnProtocol)));
|
|
255
277
|
return;
|
|
@@ -268,7 +290,7 @@ function performKeHandshake(opts) {
|
|
|
268
290
|
clearTimeout(timer);
|
|
269
291
|
var errRec = records.find(function (r) { return r.type === REC_ERROR; });
|
|
270
292
|
if (errRec) {
|
|
271
|
-
try { sock.destroy(); } catch (_e) {}
|
|
293
|
+
try { sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
|
|
272
294
|
done(new NtsError("nts/ke-error", "NTS-KE server returned error code " + errRec.body.readUInt16BE(0)));
|
|
273
295
|
return;
|
|
274
296
|
}
|
|
@@ -280,20 +302,20 @@ function performKeHandshake(opts) {
|
|
|
280
302
|
}
|
|
281
303
|
var aeadRec = records.find(function (r) { return r.type === REC_AEAD_ALGORITHM; });
|
|
282
304
|
if (!aeadRec || aeadRec.body.length < 2) {
|
|
283
|
-
try { sock.destroy(); } catch (_e) {}
|
|
305
|
+
try { sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
|
|
284
306
|
done(new NtsError("nts/no-aead", "NTS-KE response missing AEAD algorithm"));
|
|
285
307
|
return;
|
|
286
308
|
}
|
|
287
309
|
var aeadId = aeadRec.body.readUInt16BE(0);
|
|
288
310
|
if (aeadId !== AEAD_AES_SIV_CMAC_256 && aeadId !== AEAD_CHACHA20_POLY1305) {
|
|
289
|
-
try { sock.destroy(); } catch (_e) {}
|
|
311
|
+
try { sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
|
|
290
312
|
done(new NtsError("nts/unsupported-aead", "NTS-KE server selected unsupported AEAD " + aeadId));
|
|
291
313
|
return;
|
|
292
314
|
}
|
|
293
315
|
var cookies = records.filter(function (r) { return r.type === REC_NEW_COOKIE; })
|
|
294
316
|
.map(function (r) { return r.body; });
|
|
295
317
|
if (cookies.length === 0) {
|
|
296
|
-
try { sock.destroy(); } catch (_e) {}
|
|
318
|
+
try { sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
|
|
297
319
|
done(new NtsError("nts/no-cookies", "NTS-KE response contained no cookies"));
|
|
298
320
|
return;
|
|
299
321
|
}
|
|
@@ -304,7 +326,7 @@ function performKeHandshake(opts) {
|
|
|
304
326
|
var portRec = records.find(function (r) { return r.type === REC_NTPV4_PORT; });
|
|
305
327
|
if (portRec && portRec.body.length >= 2) ntpPort = portRec.body.readUInt16BE(0);
|
|
306
328
|
var keys = _exportKeys(sock, aeadId);
|
|
307
|
-
try { sock.end(); } catch (_e) {}
|
|
329
|
+
try { sock.end(); } catch (_e) { /* best-effort socket close */ }
|
|
308
330
|
done(null, {
|
|
309
331
|
aeadId: aeadId,
|
|
310
332
|
c2sKey: keys.c2s,
|
|
@@ -316,7 +338,7 @@ function performKeHandshake(opts) {
|
|
|
316
338
|
});
|
|
317
339
|
} catch (e) {
|
|
318
340
|
clearTimeout(timer);
|
|
319
|
-
try { sock.destroy(); } catch (_e) {}
|
|
341
|
+
try { sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
|
|
320
342
|
done(e);
|
|
321
343
|
}
|
|
322
344
|
});
|
|
@@ -338,7 +360,7 @@ function _aeadEncrypt(aeadId, key, nonce, plaintext, aad) {
|
|
|
338
360
|
return aesSivEncrypt(key, plaintext, ad);
|
|
339
361
|
}
|
|
340
362
|
if (aeadId === AEAD_CHACHA20_POLY1305) {
|
|
341
|
-
var c = nodeCrypto.createCipheriv("chacha20-poly1305", key, nonce, { authTagLength:
|
|
363
|
+
var c = nodeCrypto.createCipheriv("chacha20-poly1305", key, nonce, { authTagLength: POLY1305_TAG_BYTES });
|
|
342
364
|
if (aad) c.setAAD(aad, { plaintextLength: plaintext.length });
|
|
343
365
|
var ct = Buffer.concat([c.update(plaintext), c.final()]);
|
|
344
366
|
var tag = c.getAuthTag();
|
|
@@ -353,9 +375,9 @@ function _aeadDecrypt(aeadId, key, nonce, ciphertext, aad) {
|
|
|
353
375
|
return aesSivDecrypt(key, ciphertext, ad);
|
|
354
376
|
}
|
|
355
377
|
if (aeadId === AEAD_CHACHA20_POLY1305) {
|
|
356
|
-
var ct = ciphertext.slice(0, ciphertext.length -
|
|
357
|
-
var tag = ciphertext.slice(ciphertext.length -
|
|
358
|
-
var d = nodeCrypto.createDecipheriv("chacha20-poly1305", key, nonce, { authTagLength:
|
|
378
|
+
var ct = ciphertext.slice(0, ciphertext.length - POLY1305_TAG_BYTES);
|
|
379
|
+
var tag = ciphertext.slice(ciphertext.length - POLY1305_TAG_BYTES);
|
|
380
|
+
var d = nodeCrypto.createDecipheriv("chacha20-poly1305", key, nonce, { authTagLength: POLY1305_TAG_BYTES });
|
|
359
381
|
if (aad) d.setAAD(aad, { plaintextLength: ct.length });
|
|
360
382
|
d.setAuthTag(tag);
|
|
361
383
|
return Buffer.concat([d.update(ct), d.final()]);
|
|
@@ -364,8 +386,8 @@ function _aeadDecrypt(aeadId, key, nonce, ciphertext, aad) {
|
|
|
364
386
|
}
|
|
365
387
|
|
|
366
388
|
function _nonceForAead(aeadId) {
|
|
367
|
-
if (aeadId === AEAD_AES_SIV_CMAC_256) return nodeCrypto.randomBytes(
|
|
368
|
-
return nodeCrypto.randomBytes(
|
|
389
|
+
if (aeadId === AEAD_AES_SIV_CMAC_256) return nodeCrypto.randomBytes(AES_BLOCK_BYTES);
|
|
390
|
+
return nodeCrypto.randomBytes(CHACHA20_NONCE_BYTES);
|
|
369
391
|
}
|
|
370
392
|
|
|
371
393
|
function _walkExtensions(msg, startOff) {
|
|
@@ -402,12 +424,12 @@ function querySingle(opts) {
|
|
|
402
424
|
function done(err, result) {
|
|
403
425
|
if (settled) return;
|
|
404
426
|
settled = true;
|
|
405
|
-
try { sock.close(); } catch (_e) {}
|
|
427
|
+
try { sock.close(); } catch (_e) { /* best-effort socket close */ }
|
|
406
428
|
if (err) reject(err); else resolve(result);
|
|
407
429
|
}
|
|
408
|
-
var unique = nodeCrypto.randomBytes(
|
|
430
|
+
var unique = nodeCrypto.randomBytes(UNIQUE_ID_BYTES);
|
|
409
431
|
var cookie = opts.cookies[0];
|
|
410
|
-
var packet = Buffer.alloc(
|
|
432
|
+
var packet = Buffer.alloc(NTP_PACKET_BYTES);
|
|
411
433
|
packet[0] = 0x23;
|
|
412
434
|
var ext1 = _encodeExtensionField(EXTENSION_UNIQUE_IDENTIFIER, unique);
|
|
413
435
|
var ext2 = _encodeExtensionField(EXTENSION_NTS_COOKIE, cookie);
|
|
@@ -436,16 +458,16 @@ function querySingle(opts) {
|
|
|
436
458
|
clearTimeout(timer);
|
|
437
459
|
var receiveTimeMs = Date.now();
|
|
438
460
|
try {
|
|
439
|
-
if (msg.length <
|
|
461
|
+
if (msg.length < NTP_PACKET_BYTES) {
|
|
440
462
|
done(new NtsError("nts/bad-reply", "NTS reply too short"));
|
|
441
463
|
return;
|
|
442
464
|
}
|
|
443
465
|
var exts;
|
|
444
|
-
try { exts = _walkExtensions(msg,
|
|
466
|
+
try { exts = _walkExtensions(msg, NTP_PACKET_BYTES); }
|
|
445
467
|
catch (e) { done(e); return; }
|
|
446
468
|
var uniqueExt = exts.find(function (e) { return e.type === EXTENSION_UNIQUE_IDENTIFIER; });
|
|
447
|
-
if (!uniqueExt || uniqueExt.body.length <
|
|
448
|
-
!
|
|
469
|
+
if (!uniqueExt || uniqueExt.body.length < UNIQUE_ID_BYTES ||
|
|
470
|
+
!timingSafeEqual(uniqueExt.body.slice(0, UNIQUE_ID_BYTES), unique)) {
|
|
449
471
|
done(new NtsError("nts/unique-mismatch", "NTS reply unique-identifier mismatch (replay or spoof)"));
|
|
450
472
|
return;
|
|
451
473
|
}
|
|
@@ -489,11 +511,11 @@ function querySingle(opts) {
|
|
|
489
511
|
opts.cookies.shift();
|
|
490
512
|
for (var ci = 0; ci < newCookies.length; ci++) opts.cookies.push(newCookies[ci]);
|
|
491
513
|
}
|
|
492
|
-
var ntpSeconds = msg.readUInt32BE(
|
|
493
|
-
var ntpFraction = msg.readUInt32BE(
|
|
514
|
+
var ntpSeconds = msg.readUInt32BE(NTP_TX_TIMESTAMP_OFFSET);
|
|
515
|
+
var ntpFraction = msg.readUInt32BE(NTP_TX_TIMESTAMP_OFFSET + 4);
|
|
494
516
|
var serverUnixSeconds = ntpSeconds - NTP_TO_UNIX_OFFSET_SECONDS;
|
|
495
|
-
var fracMs = Math.round(ntpFraction / 0x100000000
|
|
496
|
-
var serverTimeMs = serverUnixSeconds
|
|
517
|
+
var fracMs = Math.round(C.TIME.seconds(ntpFraction / 0x100000000));
|
|
518
|
+
var serverTimeMs = C.TIME.seconds(serverUnixSeconds) + fracMs;
|
|
497
519
|
var midpointMs = sendTimeMs + (receiveTimeMs - sendTimeMs) / 2;
|
|
498
520
|
var driftMs = serverTimeMs - midpointMs;
|
|
499
521
|
done(null, {
|