@layervai/qurl 0.3.1 → 0.5.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.
Files changed (72) hide show
  1. package/README.md +261 -11
  2. package/dist/cjs/client.d.ts +22 -12
  3. package/dist/cjs/client.js +675 -100
  4. package/dist/cjs/errors.d.ts +11 -11
  5. package/dist/cjs/errors.js +25 -20
  6. package/dist/cjs/index.d.ts +1 -1
  7. package/dist/cjs/internal.d.ts +4 -0
  8. package/dist/cjs/internal.js +16 -0
  9. package/dist/cjs/node/deployment.d.ts +44 -0
  10. package/dist/cjs/node/deployment.js +167 -0
  11. package/dist/cjs/node/native-udp.d.ts +35 -0
  12. package/dist/cjs/node/native-udp.js +294 -0
  13. package/dist/cjs/node/nhp-wire.d.ts +35 -0
  14. package/dist/cjs/node/nhp-wire.js +261 -0
  15. package/dist/cjs/node/portal-opener.d.ts +114 -0
  16. package/dist/cjs/node/portal-opener.js +1070 -0
  17. package/dist/cjs/node/qv2.d.ts +38 -0
  18. package/dist/cjs/node/qv2.js +292 -0
  19. package/dist/cjs/node/strict-json.d.ts +12 -0
  20. package/dist/cjs/node/strict-json.js +174 -0
  21. package/dist/cjs/node.d.ts +3 -0
  22. package/dist/cjs/node.js +18 -0
  23. package/dist/cjs/types.d.ts +17 -20
  24. package/dist/cjs/version.d.ts +1 -1
  25. package/dist/cjs/version.js +1 -1
  26. package/dist/esm/client.d.ts +22 -12
  27. package/dist/esm/client.d.ts.map +1 -1
  28. package/dist/esm/client.js +673 -98
  29. package/dist/esm/client.js.map +1 -1
  30. package/dist/esm/errors.d.ts +11 -11
  31. package/dist/esm/errors.d.ts.map +1 -1
  32. package/dist/esm/errors.js +25 -20
  33. package/dist/esm/errors.js.map +1 -1
  34. package/dist/esm/index.d.ts +1 -1
  35. package/dist/esm/index.d.ts.map +1 -1
  36. package/dist/esm/internal.d.ts +5 -0
  37. package/dist/esm/internal.d.ts.map +1 -0
  38. package/dist/esm/internal.js +14 -0
  39. package/dist/esm/internal.js.map +1 -0
  40. package/dist/esm/node/deployment.d.ts +45 -0
  41. package/dist/esm/node/deployment.d.ts.map +1 -0
  42. package/dist/esm/node/deployment.js +163 -0
  43. package/dist/esm/node/deployment.js.map +1 -0
  44. package/dist/esm/node/native-udp.d.ts +36 -0
  45. package/dist/esm/node/native-udp.d.ts.map +1 -0
  46. package/dist/esm/node/native-udp.js +291 -0
  47. package/dist/esm/node/native-udp.js.map +1 -0
  48. package/dist/esm/node/nhp-wire.d.ts +36 -0
  49. package/dist/esm/node/nhp-wire.d.ts.map +1 -0
  50. package/dist/esm/node/nhp-wire.js +257 -0
  51. package/dist/esm/node/nhp-wire.js.map +1 -0
  52. package/dist/esm/node/portal-opener.d.ts +115 -0
  53. package/dist/esm/node/portal-opener.d.ts.map +1 -0
  54. package/dist/esm/node/portal-opener.js +1053 -0
  55. package/dist/esm/node/portal-opener.js.map +1 -0
  56. package/dist/esm/node/qv2.d.ts +39 -0
  57. package/dist/esm/node/qv2.d.ts.map +1 -0
  58. package/dist/esm/node/qv2.js +286 -0
  59. package/dist/esm/node/qv2.js.map +1 -0
  60. package/dist/esm/node/strict-json.d.ts +13 -0
  61. package/dist/esm/node/strict-json.d.ts.map +1 -0
  62. package/dist/esm/node/strict-json.js +171 -0
  63. package/dist/esm/node/strict-json.js.map +1 -0
  64. package/dist/esm/node.d.ts +4 -0
  65. package/dist/esm/node.d.ts.map +1 -0
  66. package/dist/esm/node.js +2 -0
  67. package/dist/esm/node.js.map +1 -0
  68. package/dist/esm/types.d.ts +17 -20
  69. package/dist/esm/types.d.ts.map +1 -1
  70. package/dist/esm/version.d.ts +1 -1
  71. package/dist/esm/version.js +1 -1
  72. package/package.json +26 -7
@@ -0,0 +1,294 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.nativeUdpTesting = void 0;
4
+ exports.nativeKnock = nativeKnock;
5
+ const promises_1 = require("node:dns/promises");
6
+ const node_dgram_1 = require("node:dgram");
7
+ const node_net_1 = require("node:net");
8
+ const nhp_wire_js_1 = require("./nhp-wire.js");
9
+ // These are internal per-address exchange bounds. PortalOpener applies its own
10
+ // whole-open deadline and does not expose per-address tuning.
11
+ const NATIVE_DEFAULT_TIMEOUT_MS = 3_000;
12
+ const NATIVE_DEFAULT_MAX_ADDRESSES = 3;
13
+ const ipv4Denied = new node_net_1.BlockList();
14
+ // Keep this set in lockstep with qurl-go's nonRoutablePrefixes plus the
15
+ // IsPrivate/IsLoopback/IsLinkLocal/IsMulticast predicates it applies first.
16
+ for (const [network, prefix] of [
17
+ ["0.0.0.0", 8],
18
+ ["10.0.0.0", 8],
19
+ ["100.64.0.0", 10],
20
+ ["127.0.0.0", 8],
21
+ ["169.254.0.0", 16],
22
+ ["172.16.0.0", 12],
23
+ ["192.0.0.0", 24],
24
+ ["192.0.2.0", 24],
25
+ ["192.168.0.0", 16],
26
+ ["192.88.99.0", 24],
27
+ ["198.18.0.0", 15],
28
+ ["198.51.100.0", 24],
29
+ ["203.0.113.0", 24],
30
+ ["224.0.0.0", 4],
31
+ ["240.0.0.0", 4],
32
+ ]) {
33
+ ipv4Denied.addSubnet(network, prefix, "ipv4");
34
+ }
35
+ const ipv6Allowed = new node_net_1.BlockList();
36
+ for (const [network, prefix] of [
37
+ ["2001:200::", 23],
38
+ ["2001:400::", 23],
39
+ ["2001:600::", 23],
40
+ ["2001:800::", 22],
41
+ ["2001:c00::", 23],
42
+ ["2001:e00::", 23],
43
+ ["2001:1200::", 23],
44
+ ["2001:1400::", 22],
45
+ ["2001:1800::", 23],
46
+ ["2001:1a00::", 23],
47
+ ["2001:1c00::", 22],
48
+ ["2001:2000::", 19],
49
+ ["2001:4000::", 23],
50
+ ["2001:4200::", 23],
51
+ ["2001:4400::", 23],
52
+ ["2001:4600::", 23],
53
+ ["2001:4800::", 23],
54
+ ["2001:4a00::", 23],
55
+ ["2001:4c00::", 23],
56
+ ["2001:5000::", 20],
57
+ ["2001:8000::", 19],
58
+ ["2001:a000::", 20],
59
+ ["2001:b000::", 20],
60
+ ["2003::", 18],
61
+ ["2400::", 12],
62
+ ["2410::", 12],
63
+ ["2600::", 12],
64
+ ["2610::", 23],
65
+ ["2620::", 23],
66
+ ["2630::", 12],
67
+ ["2800::", 12],
68
+ ["2a00::", 12],
69
+ ["2a10::", 12],
70
+ ["2c00::", 12],
71
+ ]) {
72
+ ipv6Allowed.addSubnet(network, prefix, "ipv6");
73
+ }
74
+ const ipv6Denied = new node_net_1.BlockList();
75
+ for (const [network, prefix] of [
76
+ ["100::", 64],
77
+ ["64:ff9b::", 96],
78
+ ["64:ff9b:1::", 48],
79
+ ["2001::", 23],
80
+ ["2001:db8::", 32],
81
+ ["2002::", 16],
82
+ ["3fff::", 20],
83
+ ["5f00::", 16],
84
+ ["fc00::", 7],
85
+ ["fe80::", 10],
86
+ ["fec0::", 10],
87
+ ["ff00::", 8],
88
+ ]) {
89
+ ipv6Denied.addSubnet(network, prefix, "ipv6");
90
+ }
91
+ const defaultNativeKnockRuntime = {
92
+ buildMessage: nhp_wire_js_1.buildNHPMessage,
93
+ resolveAddresses: resolvePublicAddresses,
94
+ exchange: exchangeDatagram,
95
+ decryptReply: nhp_wire_js_1.decryptNHPReply,
96
+ };
97
+ async function nativeKnock(cell, devicePrivateKey, body, options = {}) {
98
+ return nativeKnockWithRuntime(cell, devicePrivateKey, body, options, defaultNativeKnockRuntime);
99
+ }
100
+ async function nativeKnockWithRuntime(cell, devicePrivateKey, body, options, runtime) {
101
+ if (options.signal?.aborted)
102
+ throw options.signal.reason;
103
+ const built = runtime.buildMessage({
104
+ type: nhp_wire_js_1.NHP_TYPE_KNOCK,
105
+ devicePrivateKey,
106
+ serverPublicKey: cell.serverPublicKey,
107
+ body,
108
+ });
109
+ try {
110
+ const addresses = await runtime.resolveAddresses(cell.host, options.maxAddresses ?? NATIVE_DEFAULT_MAX_ADDRESSES, options.signal);
111
+ let lastError;
112
+ for (const address of addresses) {
113
+ try {
114
+ const packet = await runtime.exchange(address.address, address.family, cell.port, built.packet, options.timeoutMs ?? NATIVE_DEFAULT_TIMEOUT_MS, options.signal);
115
+ const reply = runtime.decryptReply(devicePrivateKey, cell.serverPublicKey, packet);
116
+ // NHP COOKIE is a stateless busy response. Go intentionally does
117
+ // not bind its counter to the request; only an ACK must match it.
118
+ if (reply.type === nhp_wire_js_1.NHP_TYPE_COOKIE)
119
+ return reply;
120
+ if (reply.counter !== built.counter) {
121
+ reply.body.fill(0);
122
+ throw new Error("NHP reply counter does not match the request");
123
+ }
124
+ return reply;
125
+ }
126
+ catch (error) {
127
+ if (options.signal?.aborted)
128
+ throw options.signal.reason;
129
+ // Authentication and reply-policy failures happen after a datagram was
130
+ // received. They are terminal and must not fall through to another DNS
131
+ // address. Only local socket failures carry this private marker.
132
+ if (!isSocketExchangeError(error))
133
+ throw error;
134
+ lastError = error.cause;
135
+ }
136
+ }
137
+ throw new Error("native NHP exchange failed", { cause: lastError });
138
+ }
139
+ finally {
140
+ built.packet.fill(0);
141
+ }
142
+ }
143
+ async function resolvePublicAddresses(host, maximum, signal, lookupAddresses = promises_1.lookup) {
144
+ if (!Number.isInteger(maximum) || maximum < 1 || maximum > 16) {
145
+ throw new Error("native NHP maxAddresses must be an integer from 1 to 16");
146
+ }
147
+ if (signal?.aborted)
148
+ throw signal.reason;
149
+ let rows;
150
+ try {
151
+ rows = await waitForAbort(lookupAddresses(host, { all: true, verbatim: true }), signal);
152
+ }
153
+ catch (error) {
154
+ if (signal?.aborted)
155
+ throw signal.reason;
156
+ throw new Error("native NHP endpoint DNS resolution failed", { cause: error });
157
+ }
158
+ if (signal?.aborted)
159
+ throw signal.reason;
160
+ const accepted = [];
161
+ for (const row of rows) {
162
+ if (row.family !== 4 && row.family !== 6)
163
+ continue;
164
+ if (!isPublicAddress(row.address, row.family))
165
+ continue;
166
+ accepted.push({ address: row.address, family: row.family });
167
+ if (accepted.length === maximum)
168
+ break;
169
+ }
170
+ if (accepted.length === 0)
171
+ throw new Error("native NHP endpoint resolved to no public address");
172
+ return accepted;
173
+ }
174
+ function isPublicAddress(address, family) {
175
+ if ((0, node_net_1.isIP)(address) !== family)
176
+ return false;
177
+ if (family === 4)
178
+ return !ipv4Denied.check(address, "ipv4");
179
+ return ipv6Allowed.check(address, "ipv6") && !ipv6Denied.check(address, "ipv6");
180
+ }
181
+ class SocketExchangeError extends Error {
182
+ constructor(cause) {
183
+ super("native NHP socket exchange failed", { cause });
184
+ this.name = "SocketExchangeError";
185
+ }
186
+ }
187
+ // A received datagram is an authority-bearing response attempt. Its malformed
188
+ // shape must stop address fallback instead of being treated as a local socket
189
+ // miss that can send the same knock to another address.
190
+ class ReceivedDatagramError extends Error {
191
+ }
192
+ function isSocketExchangeError(error) {
193
+ return error instanceof SocketExchangeError;
194
+ }
195
+ function exchangeDatagram(address, family, port, packet, timeoutMs, signal) {
196
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 60_000) {
197
+ return Promise.reject(new Error("native NHP timeout must be from 1 to 60000 milliseconds"));
198
+ }
199
+ if (signal?.aborted) {
200
+ return Promise.reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
201
+ }
202
+ return new Promise((resolve, reject) => {
203
+ const socket = (0, node_dgram_1.createSocket)(family === 6 ? "udp6" : "udp4");
204
+ let settled = false;
205
+ const finish = (error, reply) => {
206
+ if (settled)
207
+ return;
208
+ settled = true;
209
+ clearTimeout(timer);
210
+ signal?.removeEventListener("abort", abort);
211
+ // An error that arrives while close settles must not become an unhandled
212
+ // EventEmitter error after the one-shot operational listener ran.
213
+ socket.on("error", () => undefined);
214
+ try {
215
+ socket.close();
216
+ }
217
+ catch {
218
+ // The socket can already be closed by a synchronous setup failure.
219
+ }
220
+ if (error instanceof ReceivedDatagramError)
221
+ reject(error);
222
+ else if (error !== undefined)
223
+ reject(new SocketExchangeError(error));
224
+ else
225
+ resolve(new Uint8Array(reply));
226
+ };
227
+ const abort = () => finish(signal?.reason ?? new DOMException("Aborted", "AbortError"));
228
+ const timer = setTimeout(() => finish(new Error("native NHP reply timed out")), timeoutMs);
229
+ timer.unref();
230
+ signal?.addEventListener("abort", abort, { once: true });
231
+ socket.once("error", (error) => finish(error));
232
+ socket.once("message", (reply) => {
233
+ if (reply.byteLength > nhp_wire_js_1.NHP_PACKET_SIZE) {
234
+ finish(new ReceivedDatagramError("native NHP reply is too large"));
235
+ }
236
+ else
237
+ finish(undefined, reply);
238
+ });
239
+ socket.connect(port, address, () => {
240
+ sendOwnedDatagram(socket, packet, finish);
241
+ });
242
+ });
243
+ }
244
+ function sendOwnedDatagram(socket, packet, finish) {
245
+ // dgram owns its input until this callback. Use an independent encrypted
246
+ // datagram so nativeKnock can wipe the builder's packet immediately when an
247
+ // abort settles the exchange. Wipe this copy only after libuv releases it.
248
+ const outbound = Buffer.from(packet);
249
+ try {
250
+ socket.send(outbound, (error) => {
251
+ try {
252
+ if (error)
253
+ finish(error);
254
+ }
255
+ finally {
256
+ outbound.fill(0);
257
+ }
258
+ });
259
+ }
260
+ catch (error) {
261
+ outbound.fill(0);
262
+ finish(error);
263
+ }
264
+ }
265
+ function waitForAbort(promise, signal) {
266
+ if (!signal)
267
+ return promise;
268
+ if (signal.aborted) {
269
+ return Promise.reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
270
+ }
271
+ return new Promise((resolve, reject) => {
272
+ const abort = () => {
273
+ cleanup();
274
+ reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
275
+ };
276
+ const cleanup = () => signal.removeEventListener("abort", abort);
277
+ signal.addEventListener("abort", abort, { once: true });
278
+ promise.then((value) => {
279
+ cleanup();
280
+ resolve(value);
281
+ }, (error) => {
282
+ cleanup();
283
+ reject(error);
284
+ });
285
+ });
286
+ }
287
+ exports.nativeUdpTesting = {
288
+ isPublicAddress,
289
+ resolvePublicAddresses,
290
+ nativeKnockWithRuntime,
291
+ exchangeDatagram,
292
+ sendOwnedDatagram,
293
+ socketExchangeError: (cause) => new SocketExchangeError(cause),
294
+ };
@@ -0,0 +1,35 @@
1
+ export declare const NHP_TYPE_KNOCK = 1;
2
+ export declare const NHP_TYPE_ACK = 2;
3
+ export declare const NHP_TYPE_COOKIE = 7;
4
+ export declare const NHP_HEADER_SIZE = 240;
5
+ export declare const NHP_PACKET_SIZE = 4096;
6
+ export declare const NHP_MAX_BODY_SIZE: number;
7
+ export interface NHPMessage {
8
+ readonly type: number;
9
+ readonly flags: number;
10
+ readonly counter: bigint;
11
+ readonly timestampNanos: bigint;
12
+ readonly body: Uint8Array;
13
+ }
14
+ export interface BuildNHPMessageInput {
15
+ readonly type: number;
16
+ readonly devicePrivateKey: Uint8Array;
17
+ readonly serverPublicKey: Uint8Array;
18
+ readonly body: Uint8Array;
19
+ readonly ephemeralPrivateKey?: Uint8Array;
20
+ readonly timestampNanos?: bigint;
21
+ readonly counter?: bigint;
22
+ readonly preamble?: number;
23
+ }
24
+ export declare function buildNHPMessage(input: BuildNHPMessageInput): {
25
+ packet: Uint8Array;
26
+ counter: bigint;
27
+ };
28
+ export declare function decryptNHPReply(devicePrivateKey: Uint8Array, expectedServerPublicKey: Uint8Array, packetValue: Uint8Array): NHPMessage;
29
+ declare function validateReplyProfile(type: number, flags: number): void;
30
+ declare function inflateReplyBody(body: Buffer, flags: number): Buffer;
31
+ export declare const nhpWireTesting: {
32
+ validateReplyProfile: typeof validateReplyProfile;
33
+ inflateReplyBody: typeof inflateReplyBody;
34
+ };
35
+ export {};
@@ -0,0 +1,261 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.nhpWireTesting = exports.NHP_MAX_BODY_SIZE = exports.NHP_PACKET_SIZE = exports.NHP_HEADER_SIZE = exports.NHP_TYPE_COOKIE = exports.NHP_TYPE_ACK = exports.NHP_TYPE_KNOCK = void 0;
4
+ exports.buildNHPMessage = buildNHPMessage;
5
+ exports.decryptNHPReply = decryptNHPReply;
6
+ const node_crypto_1 = require("node:crypto");
7
+ const node_zlib_1 = require("node:zlib");
8
+ exports.NHP_TYPE_KNOCK = 1;
9
+ exports.NHP_TYPE_ACK = 2;
10
+ exports.NHP_TYPE_COOKIE = 7;
11
+ exports.NHP_HEADER_SIZE = 240;
12
+ exports.NHP_PACKET_SIZE = 4_096;
13
+ exports.NHP_MAX_BODY_SIZE = exports.NHP_PACKET_SIZE - exports.NHP_HEADER_SIZE - 16;
14
+ const HEADER_COMMON_SIZE = 24;
15
+ const OFFSET_EPHEMERAL = 24;
16
+ const OFFSET_STATIC = 136;
17
+ const OFFSET_TIMESTAMP = 184;
18
+ const OFFSET_DIGEST = 208;
19
+ const FLAG_COMPRESS = 1 << 1;
20
+ const INITIAL_HASH = Buffer.from("NHP hashgen v.20230421@deepcloudsdp.com", "utf8");
21
+ const INITIAL_CHAIN_KEY = Buffer.from("NHP keygen v.20230421@clouddeep.cn", "utf8");
22
+ const X25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b656e04220420", "hex");
23
+ const X25519_SPKI_PREFIX = Buffer.from("302a300506032b656e032100", "hex");
24
+ function buildNHPMessage(input) {
25
+ validateKey(input.devicePrivateKey, "device private key");
26
+ validateKey(input.serverPublicKey, "server public key");
27
+ if (input.body.byteLength > exports.NHP_MAX_BODY_SIZE)
28
+ throw new Error("NHP application body is too large");
29
+ if (input.type !== exports.NHP_TYPE_KNOCK)
30
+ throw new Error("unsupported NHP initiator message type");
31
+ const ephemeralPrivateKey = input.ephemeralPrivateKey
32
+ ? Buffer.from(input.ephemeralPrivateKey)
33
+ : (0, node_crypto_1.randomBytes)(32);
34
+ const secrets = [ephemeralPrivateKey];
35
+ try {
36
+ validateKey(ephemeralPrivateKey, "ephemeral private key");
37
+ const random = input.counter === undefined || input.preamble === undefined ? (0, node_crypto_1.randomBytes)(12) : undefined;
38
+ const counter = input.counter ?? random.readBigUInt64BE(0);
39
+ const preamble = input.preamble ?? random.readUInt32BE(8);
40
+ const timestampNanos = input.timestampNanos ?? BigInt(Date.now()) * 1000000n;
41
+ const devicePublicKey = rawPublicFromPrivate(input.devicePrivateKey);
42
+ const ephemeralPublicKey = rawPublicFromPrivate(ephemeralPrivateKey);
43
+ const nonce = nonceForCounter(counter);
44
+ const header = Buffer.alloc(exports.NHP_HEADER_SIZE);
45
+ ephemeralPublicKey.copy(header, OFFSET_EPHEMERAL);
46
+ let chainHashParts = [INITIAL_HASH];
47
+ let chainKey = trackSecret(secrets, mixKey(hashParts(chainHashParts), INITIAL_CHAIN_KEY));
48
+ chainHashParts = [...chainHashParts, Buffer.from(input.serverPublicKey), ephemeralPublicKey];
49
+ chainKey = trackSecret(secrets, mixKey(chainKey, ephemeralPublicKey));
50
+ const ephemeralShared = trackSecret(secrets, sharedSecret(ephemeralPrivateKey, input.serverPublicKey));
51
+ let derived = keyGen2(chainKey, ephemeralShared);
52
+ secrets.push(...derived);
53
+ chainKey = derived[0];
54
+ const sealedStatic = seal(derived[1], nonce, devicePublicKey, hashParts(chainHashParts));
55
+ sealedStatic.copy(header, OFFSET_STATIC);
56
+ chainHashParts = [...chainHashParts, sealedStatic];
57
+ const staticShared = trackSecret(secrets, sharedSecret(input.devicePrivateKey, input.serverPublicKey));
58
+ derived = keyGen2(chainKey, staticShared);
59
+ secrets.push(...derived);
60
+ chainKey = derived[0];
61
+ const timestamp = Buffer.alloc(8);
62
+ timestamp.writeBigUInt64BE(timestampNanos);
63
+ const sealedTimestamp = seal(derived[1], nonce, timestamp, hashParts(chainHashParts));
64
+ sealedTimestamp.copy(header, OFFSET_TIMESTAMP);
65
+ chainHashParts = [...chainHashParts, sealedTimestamp];
66
+ derived = keyGen2(chainKey, sealedTimestamp);
67
+ secrets.push(...derived);
68
+ const bodyKey = derived[1];
69
+ header.writeUInt8(1, 8);
70
+ header.writeUInt8(1, 9);
71
+ header.writeUInt16BE(0, 10);
72
+ header.writeBigUInt64BE(counter, 16);
73
+ const payloadSize = input.body.byteLength === 0 ? 0 : input.body.byteLength + 16;
74
+ header.writeUInt32BE(preamble >>> 0, 0);
75
+ header.writeUInt32BE((preamble ^ ((input.type << 16) | payloadSize)) >>> 0, 4);
76
+ const bodyAad = hashParts([...chainHashParts, header.subarray(0, HEADER_COMMON_SIZE)]);
77
+ const sealedBody = input.body.byteLength === 0 ? Buffer.alloc(0) : seal(bodyKey, nonce, input.body, bodyAad);
78
+ headerDigest(input.serverPublicKey, header).copy(header, OFFSET_DIGEST);
79
+ return { packet: Buffer.concat([header, sealedBody]), counter };
80
+ }
81
+ finally {
82
+ wipeBuffers(secrets);
83
+ }
84
+ }
85
+ function decryptNHPReply(devicePrivateKey, expectedServerPublicKey, packetValue) {
86
+ validateKey(devicePrivateKey, "device private key");
87
+ validateKey(expectedServerPublicKey, "server public key");
88
+ const packet = Buffer.from(packetValue);
89
+ if (packet.byteLength < exports.NHP_HEADER_SIZE || packet.byteLength > exports.NHP_PACKET_SIZE) {
90
+ throw new Error("NHP reply size is outside the protocol bounds");
91
+ }
92
+ if (packet[8] !== 1 || packet[9] < 1)
93
+ throw new Error("unsupported NHP protocol version");
94
+ const header = packet.subarray(0, exports.NHP_HEADER_SIZE);
95
+ const sealedBody = packet.subarray(exports.NHP_HEADER_SIZE);
96
+ const secrets = [];
97
+ let body;
98
+ try {
99
+ const devicePublicKey = rawPublicFromPrivate(devicePrivateKey);
100
+ const expectedDigest = headerDigest(devicePublicKey, header);
101
+ if (!(0, node_crypto_1.timingSafeEqual)(expectedDigest, header.subarray(OFFSET_DIGEST))) {
102
+ throw new Error("NHP reply header authentication failed");
103
+ }
104
+ const counter = header.readBigUInt64BE(16);
105
+ const nonce = nonceForCounter(counter);
106
+ const serverEphemeral = header.subarray(OFFSET_EPHEMERAL, OFFSET_EPHEMERAL + 32);
107
+ const sealedStatic = header.subarray(OFFSET_STATIC, OFFSET_STATIC + 48);
108
+ const sealedTimestamp = header.subarray(OFFSET_TIMESTAMP, OFFSET_TIMESTAMP + 24);
109
+ let chainHashParts = [INITIAL_HASH];
110
+ let chainKey = trackSecret(secrets, mixKey(hashParts(chainHashParts), INITIAL_CHAIN_KEY));
111
+ chainHashParts = [...chainHashParts, devicePublicKey, serverEphemeral];
112
+ chainKey = trackSecret(secrets, mixKey(chainKey, serverEphemeral));
113
+ const ephemeralShared = trackSecret(secrets, sharedSecret(devicePrivateKey, serverEphemeral));
114
+ let derived = keyGen2(chainKey, ephemeralShared);
115
+ secrets.push(...derived);
116
+ chainKey = derived[0];
117
+ const serverStatic = open(derived[1], nonce, sealedStatic, hashParts(chainHashParts));
118
+ if (!(0, node_crypto_1.timingSafeEqual)(serverStatic, Buffer.from(expectedServerPublicKey))) {
119
+ throw new Error("NHP reply came from an unexpected server key");
120
+ }
121
+ chainHashParts = [...chainHashParts, sealedStatic];
122
+ const staticShared = trackSecret(secrets, sharedSecret(devicePrivateKey, serverStatic));
123
+ derived = keyGen2(chainKey, staticShared);
124
+ secrets.push(...derived);
125
+ chainKey = derived[0];
126
+ const timestamp = open(derived[1], nonce, sealedTimestamp, hashParts(chainHashParts));
127
+ chainHashParts = [...chainHashParts, sealedTimestamp];
128
+ derived = keyGen2(chainKey, sealedTimestamp);
129
+ secrets.push(...derived);
130
+ const bodyKey = derived[1];
131
+ const bodyAad = hashParts([...chainHashParts, header.subarray(0, HEADER_COMMON_SIZE)]);
132
+ body =
133
+ sealedBody.byteLength === 0 ? Buffer.alloc(0) : open(bodyKey, nonce, sealedBody, bodyAad);
134
+ const preamble = header.readUInt32BE(0);
135
+ const encodedTypeSize = (preamble ^ header.readUInt32BE(4)) >>> 0;
136
+ const type = (encodedTypeSize >>> 16) & 0xffff;
137
+ const flags = header.readUInt16BE(10);
138
+ validateReplyProfile(type, flags);
139
+ body = inflateReplyBody(body, flags);
140
+ if (body.byteLength > exports.NHP_PACKET_SIZE) {
141
+ throw new Error("NHP reply body exceeds the post-inflate limit");
142
+ }
143
+ const returnedBody = body;
144
+ body = undefined;
145
+ return {
146
+ type,
147
+ flags,
148
+ counter,
149
+ timestampNanos: timestamp.readBigUInt64BE(0),
150
+ body: returnedBody,
151
+ };
152
+ }
153
+ finally {
154
+ body?.fill(0);
155
+ wipeBuffers(secrets);
156
+ }
157
+ }
158
+ function validateReplyProfile(type, flags) {
159
+ if (![exports.NHP_TYPE_ACK, exports.NHP_TYPE_COOKIE].includes(type) || (flags !== 0 && flags !== FLAG_COMPRESS)) {
160
+ throw new Error("NHP reply type or flags are outside the reply profile");
161
+ }
162
+ }
163
+ function inflateReplyBody(body, flags) {
164
+ if (body.byteLength === 0 || (flags & FLAG_COMPRESS) === 0)
165
+ return body;
166
+ try {
167
+ return (0, node_zlib_1.inflateSync)(body, { maxOutputLength: exports.NHP_PACKET_SIZE });
168
+ }
169
+ finally {
170
+ body.fill(0);
171
+ }
172
+ }
173
+ function validateKey(key, name) {
174
+ if (key.byteLength !== 32)
175
+ throw new Error(`${name} must be 32 bytes`);
176
+ }
177
+ function rawPrivateKey(raw) {
178
+ const der = Buffer.alloc(X25519_PKCS8_PREFIX.byteLength + raw.byteLength);
179
+ X25519_PKCS8_PREFIX.copy(der);
180
+ der.set(raw, X25519_PKCS8_PREFIX.byteLength);
181
+ try {
182
+ return (0, node_crypto_1.createPrivateKey)({ key: der, format: "der", type: "pkcs8" });
183
+ }
184
+ finally {
185
+ der.fill(0);
186
+ }
187
+ }
188
+ function rawPublicKey(raw) {
189
+ return (0, node_crypto_1.createPublicKey)({
190
+ key: Buffer.concat([X25519_SPKI_PREFIX, Buffer.from(raw)]),
191
+ format: "der",
192
+ type: "spki",
193
+ });
194
+ }
195
+ function rawPublicFromPrivate(raw) {
196
+ const spki = (0, node_crypto_1.createPublicKey)(rawPrivateKey(raw)).export({ format: "der", type: "spki" });
197
+ return spki.subarray(spki.byteLength - 32);
198
+ }
199
+ function sharedSecret(privateValue, publicValue) {
200
+ return (0, node_crypto_1.diffieHellman)({
201
+ privateKey: rawPrivateKey(privateValue),
202
+ publicKey: rawPublicKey(publicValue),
203
+ });
204
+ }
205
+ function hashParts(parts) {
206
+ const hash = (0, node_crypto_1.createHash)("blake2s256");
207
+ for (const part of parts)
208
+ hash.update(part);
209
+ return hash.digest();
210
+ }
211
+ function hmac(key, ...parts) {
212
+ const mac = (0, node_crypto_1.createHmac)("blake2s256", key);
213
+ for (const part of parts)
214
+ mac.update(part);
215
+ return mac.digest();
216
+ }
217
+ function mixKey(key, input) {
218
+ const prk = hmac(key, input);
219
+ const mixed = hmac(prk, Uint8Array.of(1));
220
+ prk.fill(0);
221
+ return mixed;
222
+ }
223
+ function keyGen2(key, input) {
224
+ const prk = hmac(key, input);
225
+ const first = hmac(prk, Uint8Array.of(1));
226
+ const second = hmac(prk, first, Uint8Array.of(2));
227
+ prk.fill(0);
228
+ return [first, second];
229
+ }
230
+ function nonceForCounter(counter) {
231
+ const nonce = Buffer.alloc(12);
232
+ nonce.writeBigUInt64BE(counter, 4);
233
+ return nonce;
234
+ }
235
+ function seal(key, nonce, plaintext, aad) {
236
+ const cipher = (0, node_crypto_1.createCipheriv)("aes-256-gcm", key, nonce);
237
+ cipher.setAAD(aad);
238
+ return Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]);
239
+ }
240
+ function open(key, nonce, sealed, aad) {
241
+ if (sealed.byteLength < 16)
242
+ throw new Error("NHP sealed field is shorter than its tag");
243
+ const ciphertext = sealed.subarray(0, sealed.byteLength - 16);
244
+ const tag = sealed.subarray(sealed.byteLength - 16);
245
+ const decipher = (0, node_crypto_1.createDecipheriv)("aes-256-gcm", key, nonce);
246
+ decipher.setAAD(aad);
247
+ decipher.setAuthTag(tag);
248
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
249
+ }
250
+ function headerDigest(peerStaticPublicKey, header) {
251
+ return hashParts([INITIAL_HASH, peerStaticPublicKey, header.subarray(0, OFFSET_DIGEST)]);
252
+ }
253
+ function trackSecret(secrets, value) {
254
+ secrets.push(value);
255
+ return value;
256
+ }
257
+ function wipeBuffers(values) {
258
+ for (const value of values)
259
+ value.fill(0);
260
+ }
261
+ exports.nhpWireTesting = { validateReplyProfile, inflateReplyBody };