@basmilius/apple-common 0.7.2 → 0.8.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/dist/index.js DELETED
@@ -1,942 +0,0 @@
1
- // ../../node_modules/uuid/dist-node/stringify.js
2
- var byteToHex = [];
3
- for (let i = 0;i < 256; ++i) {
4
- byteToHex.push((i + 256).toString(16).slice(1));
5
- }
6
- function unsafeStringify(arr, offset = 0) {
7
- return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
8
- }
9
-
10
- // ../../node_modules/uuid/dist-node/rng.js
11
- import { randomFillSync } from "node:crypto";
12
- var rnds8Pool = new Uint8Array(256);
13
- var poolPtr = rnds8Pool.length;
14
- function rng() {
15
- if (poolPtr > rnds8Pool.length - 16) {
16
- randomFillSync(rnds8Pool);
17
- poolPtr = 0;
18
- }
19
- return rnds8Pool.slice(poolPtr, poolPtr += 16);
20
- }
21
-
22
- // ../../node_modules/uuid/dist-node/native.js
23
- import { randomUUID } from "node:crypto";
24
- var native_default = { randomUUID };
25
-
26
- // ../../node_modules/uuid/dist-node/v4.js
27
- function _v4(options, buf, offset) {
28
- options = options || {};
29
- const rnds = options.random ?? options.rng?.() ?? rng();
30
- if (rnds.length < 16) {
31
- throw new Error("Random bytes length must be >= 16");
32
- }
33
- rnds[6] = rnds[6] & 15 | 64;
34
- rnds[8] = rnds[8] & 63 | 128;
35
- if (buf) {
36
- offset = offset || 0;
37
- if (offset < 0 || offset + 16 > buf.length) {
38
- throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
39
- }
40
- for (let i = 0;i < 16; ++i) {
41
- buf[offset + i] = rnds[i];
42
- }
43
- return buf;
44
- }
45
- return unsafeStringify(rnds);
46
- }
47
- function v4(options, buf, offset) {
48
- if (native_default.randomUUID && !buf && !options) {
49
- return native_default.randomUUID();
50
- }
51
- return _v4(options, buf, offset);
52
- }
53
- var v4_default = v4;
54
- // src/discovery.ts
55
- import mdns from "node-dns-sd";
56
-
57
- // src/cli.ts
58
- import { createInterface } from "node:readline";
59
- async function prompt(message) {
60
- const cli = createInterface({
61
- input: process.stdin,
62
- output: process.stdout
63
- });
64
- const answer = await new Promise((resolve) => cli.question(`${message}: `, resolve));
65
- cli.close();
66
- return answer;
67
- }
68
- async function waitFor(ms) {
69
- return new Promise((resolve) => setTimeout(resolve, ms));
70
- }
71
-
72
- // src/const.ts
73
- var AIRPLAY_TRANSIENT_PIN = "3939";
74
- var HTTP_TIMEOUT = 6000;
75
- var SOCKET_TIMEOUT = 1e4;
76
- var AIRPLAY_SERVICE = "_airplay._tcp.local";
77
- var COMPANION_LINK_SERVICE = "_companion-link._tcp.local";
78
- var RAOP_SERVICE = "_raop._tcp.local";
79
-
80
- // src/discovery.ts
81
- class Discovery {
82
- #service;
83
- constructor(service) {
84
- this.#service = service;
85
- }
86
- async find() {
87
- const results = await mdns.discover({
88
- name: this.#service
89
- });
90
- return results.map((result) => ({
91
- id: generateId(result) ?? result.fqdn,
92
- txt: getTxt(result),
93
- ...result
94
- }));
95
- }
96
- async findUntil(id, tries = 10, timeout = 1000) {
97
- while (tries > 0) {
98
- const devices = await this.find();
99
- const device = devices.find((device2) => device2.id === id);
100
- if (device) {
101
- return device;
102
- }
103
- console.log();
104
- console.log(`Device not found, retrying in ${timeout}ms...`);
105
- console.log(devices.map((d) => ` ● ${d.id} (${d.fqdn})`).join(`
106
- `));
107
- tries--;
108
- await waitFor(timeout);
109
- }
110
- throw new Error("Device not found after serveral tries, aborting.");
111
- }
112
- static airplay() {
113
- return new Discovery(AIRPLAY_SERVICE);
114
- }
115
- static companionLink() {
116
- return new Discovery(COMPANION_LINK_SERVICE);
117
- }
118
- static raop() {
119
- return new Discovery(RAOP_SERVICE);
120
- }
121
- }
122
- function generateId(result) {
123
- if (!result?.packet) {
124
- return null;
125
- }
126
- const { answers = [], additionals = [] } = result.packet;
127
- const allRecords = [...answers, ...additionals];
128
- const srvRecord = allRecords.find((record) => record.type === "SRV");
129
- if (srvRecord?.rdata?.target) {
130
- return srvRecord.rdata.target;
131
- }
132
- if (result.address) {
133
- const addressRecord = allRecords.find((record) => (record.type === "A" || record.type === "AAAA") && record.rdata === result.address);
134
- if (addressRecord?.name) {
135
- return addressRecord.name;
136
- }
137
- }
138
- const aRecord = allRecords.find((record) => record.type === "A");
139
- if (aRecord?.name) {
140
- return aRecord.name;
141
- }
142
- if (result.modelName) {
143
- const hostname = result.modelName.replace(/\s+/g, "-").replace(/[^a-zA-Z0-9-]/g, "");
144
- return `${hostname}.local`;
145
- }
146
- return null;
147
- }
148
- function getTxt(result) {
149
- if (!result.packet) {
150
- return {};
151
- }
152
- const { answers = [], additionals = [] } = result.packet;
153
- const records = [
154
- ...answers,
155
- ...additionals
156
- ];
157
- const txt = {};
158
- for (const record of records) {
159
- if (record.type === "TXT" && record.rdata) {
160
- Object.assign(txt, record.rdata);
161
- }
162
- }
163
- return txt;
164
- }
165
- // src/connection.ts
166
- import { EventEmitter } from "node:events";
167
- import { Socket } from "node:net";
168
-
169
- // src/symbols.ts
170
- var ENCRYPTION = Symbol();
171
-
172
- // src/connection.ts
173
- var NOOP_PROMISE_HANDLER = {
174
- resolve: () => {},
175
- reject: (_) => {}
176
- };
177
-
178
- class Connection extends EventEmitter {
179
- get address() {
180
- return this.#address;
181
- }
182
- get context() {
183
- return this.#context;
184
- }
185
- get port() {
186
- return this.#port;
187
- }
188
- get isConnected() {
189
- return this.#state === "connected";
190
- }
191
- get state() {
192
- if (this.#state === "closing" || this.#state === "failed") {
193
- return this.#state;
194
- }
195
- if (!this.#socket) {
196
- return "disconnected";
197
- }
198
- switch (this.#socket.readyState) {
199
- case "opening":
200
- return "connecting";
201
- case "open":
202
- return "connected";
203
- default:
204
- return this.#state;
205
- }
206
- }
207
- #address;
208
- #port;
209
- #context;
210
- #debug = false;
211
- #retryAttempt = 0;
212
- #retryAttempts = 3;
213
- #retryEnabled = true;
214
- #retryInterval = 3000;
215
- #retryTimeout;
216
- #socket;
217
- #state;
218
- #connectPromise;
219
- constructor(context, address, port) {
220
- super();
221
- this.#address = address;
222
- this.#port = port;
223
- this.#context = context;
224
- this.#state = "disconnected";
225
- }
226
- async connect() {
227
- if (this.#state === "connected") {
228
- return;
229
- }
230
- if (this.#state === "connecting") {
231
- throw new Error("A connection is already being established.");
232
- }
233
- this.#retryEnabled = true;
234
- this.#retryAttempt = 0;
235
- return this.#attemptConnect();
236
- }
237
- async destroy() {
238
- this.#socket?.destroy();
239
- }
240
- async disconnect() {
241
- if (this.#retryTimeout) {
242
- clearTimeout(this.#retryTimeout);
243
- this.#retryTimeout = undefined;
244
- }
245
- this.#retryEnabled = false;
246
- if (!this.#socket || this.#state === "disconnected") {
247
- return;
248
- }
249
- return new Promise((resolve) => {
250
- this.#state = "closing";
251
- this.#socket.once("close", () => {
252
- this.#cleanup();
253
- resolve();
254
- });
255
- this.#socket.end();
256
- });
257
- }
258
- debug(enabled) {
259
- this.#debug = enabled;
260
- return this;
261
- }
262
- retry(attempts, interval = 3000) {
263
- this.#retryAttempts = attempts;
264
- this.#retryInterval = interval;
265
- return this;
266
- }
267
- write(data) {
268
- if (!this.#socket || this.state !== "connected" || !this.#socket.writable) {
269
- this.emit("error", new Error("Cannot write to a disconnected connection."));
270
- return;
271
- }
272
- this.#socket.write(data, (err) => {
273
- if (!err) {
274
- return;
275
- }
276
- this.#context.logger.error("Failed to write data to socket.");
277
- this.emit("error", err);
278
- });
279
- }
280
- async#attemptConnect() {
281
- return new Promise((resolve, reject) => {
282
- this.#state = "connecting";
283
- this.#connectPromise = { resolve, reject };
284
- this.#socket?.removeAllListeners();
285
- this.#socket = undefined;
286
- this.#socket = new Socket;
287
- this.#socket.setNoDelay(true);
288
- this.#socket.setTimeout(SOCKET_TIMEOUT);
289
- this.#socket.on("close", this.#onClose.bind(this));
290
- this.#socket.on("connect", this.#onConnect.bind(this));
291
- this.#socket.on("data", this.#onData.bind(this));
292
- this.#socket.on("end", this.#onEnd.bind(this));
293
- this.#socket.on("error", this.#onError.bind(this));
294
- this.#socket.on("timeout", this.#onTimeout.bind(this));
295
- this.#context.logger.net(`Connecting to ${this.#address}:${this.#port}...`);
296
- this.#socket.connect({
297
- host: this.#address,
298
- port: this.#port,
299
- keepAlive: true
300
- });
301
- });
302
- }
303
- #cleanup() {
304
- if (this.#retryTimeout) {
305
- clearTimeout(this.#retryTimeout);
306
- this.#retryTimeout = undefined;
307
- }
308
- if (this.#socket) {
309
- this.#socket.removeAllListeners();
310
- this.#socket.destroy();
311
- this.#socket = undefined;
312
- }
313
- this.#state = "disconnected";
314
- this.#connectPromise = undefined;
315
- }
316
- #scheduleRetry(err) {
317
- if (!this.#retryEnabled || this.#retryAttempt >= this.#retryAttempts) {
318
- this.#state = "failed";
319
- this.#connectPromise?.reject(err);
320
- this.#connectPromise = undefined;
321
- return;
322
- }
323
- if (this.#retryTimeout) {
324
- clearTimeout(this.#retryTimeout);
325
- this.#retryTimeout = undefined;
326
- }
327
- this.#retryAttempt++;
328
- this.#context.logger.net(`Retry attempt ${this.#retryAttempt} / ${this.#retryAttempts} in ${this.#retryInterval}ms...`);
329
- const { resolve, reject } = this.#connectPromise ?? NOOP_PROMISE_HANDLER;
330
- this.#cleanup();
331
- this.#retryTimeout = setTimeout(async () => {
332
- this.#retryTimeout = undefined;
333
- try {
334
- this.#connectPromise = { resolve, reject };
335
- await this.#attemptConnect();
336
- resolve();
337
- } catch (retryErr) {}
338
- }, this.#retryInterval);
339
- }
340
- #onClose(hadError) {
341
- const wasConnected = this.#state === "connected";
342
- if (this.#state !== "closing") {
343
- this.#state = "disconnected";
344
- this.#context.logger.net(`Connection closed (${hadError ? "with error" : "normally"}).`);
345
- }
346
- this.emit("close", hadError);
347
- if (wasConnected && this.#retryEnabled && hadError) {
348
- this.#scheduleRetry(new Error("Connection closed unexpectedly."));
349
- }
350
- }
351
- #onConnect() {
352
- this.#state = "connected";
353
- this.#retryAttempt = 0;
354
- this.#socket.setKeepAlive(true, 1e4);
355
- this.#socket.setTimeout(0);
356
- this.emit("connect");
357
- this.#connectPromise?.resolve();
358
- this.#connectPromise = undefined;
359
- }
360
- #onData(data) {
361
- if (this.#debug) {
362
- const cutoff = Math.min(data.byteLength, 64);
363
- this.#context.logger.debug(`Received ${data.byteLength} bytes of data.`);
364
- this.#context.logger.debug(`hex=${data.subarray(0, cutoff).toString("hex")}`);
365
- this.#context.logger.debug(`ascii=${data.toString("ascii").replace(/[^\x20-\x7E]/g, ".").substring(0, cutoff)}`);
366
- }
367
- this.emit("data", data);
368
- }
369
- #onEnd() {
370
- this.emit("end");
371
- }
372
- #onError(err) {
373
- this.#context.logger.error(`Connection error: ${err.message}`);
374
- if (this.listenerCount("error") > 0) {
375
- this.emit("error", err);
376
- } else {
377
- this.#context.logger.warn("No error handler registered. This is likely a bug.", this.constructor.name, "#onError");
378
- }
379
- if (this.#state === "connecting") {
380
- this.#scheduleRetry(err);
381
- } else {
382
- this.#state = "failed";
383
- }
384
- }
385
- #onTimeout() {
386
- this.#context.logger.error("Connection timed out.");
387
- const err = new Error("Connection timed out.");
388
- this.emit("timeout");
389
- if (this.#state === "connecting") {
390
- this.#scheduleRetry(err);
391
- } else {
392
- this.#state = "failed";
393
- this.#socket?.destroy();
394
- }
395
- }
396
- }
397
-
398
- class EncryptionAwareConnection extends Connection {
399
- get isEncrypted() {
400
- return !!this[ENCRYPTION];
401
- }
402
- [ENCRYPTION];
403
- enableEncryption(readKey, writeKey) {
404
- this[ENCRYPTION] = new EncryptionState(readKey, writeKey);
405
- }
406
- }
407
-
408
- class EncryptionState {
409
- readKey;
410
- readCount;
411
- writeKey;
412
- writeCount;
413
- constructor(readKey, writeKey) {
414
- this.readCount = 0;
415
- this.readKey = readKey;
416
- this.writeCount = 0;
417
- this.writeKey = writeKey;
418
- }
419
- }
420
- // src/reporter.ts
421
- class Logger {
422
- get id() {
423
- return this.#id;
424
- }
425
- get label() {
426
- return this.#label;
427
- }
428
- #id;
429
- #label;
430
- constructor(id) {
431
- this.#id = id;
432
- this.#label = `\x1B[36m[${id}]\x1B[39m`;
433
- }
434
- debug(...data) {
435
- debug(this.#label, ...data);
436
- }
437
- error(...data) {
438
- error(this.#label, ...data);
439
- }
440
- info(...data) {
441
- info(this.#label, ...data);
442
- }
443
- net(...data) {
444
- net(this.#label, ...data);
445
- }
446
- raw(...data) {
447
- raw(this.#label, ...data);
448
- }
449
- warn(...data) {
450
- warn(this.#label, ...data);
451
- }
452
- }
453
-
454
- class Reporter {
455
- #enabled = [];
456
- all() {
457
- this.#enabled = ["debug", "error", "info", "net", "raw", "warn"];
458
- }
459
- disable(group) {
460
- if (this.#enabled.includes(group)) {
461
- this.#enabled.splice(this.#enabled.indexOf(group), 1);
462
- }
463
- }
464
- enable(group) {
465
- if (!this.#enabled.includes(group)) {
466
- this.#enabled.push(group);
467
- }
468
- }
469
- isEnabled(group) {
470
- return this.#enabled.includes(group);
471
- }
472
- }
473
- function debug(...data) {
474
- reporter.isEnabled("debug") && console.debug(`\x1B[36m[debug]\x1B[39m`, ...data);
475
- }
476
- function error(...data) {
477
- reporter.isEnabled("error") && console.error(`\x1B[31m[error]\x1B[39m`, ...data);
478
- }
479
- function info(...data) {
480
- reporter.isEnabled("info") && console.info(`\x1B[32m[info]\x1B[39m`, ...data);
481
- }
482
- function net(...data) {
483
- reporter.isEnabled("net") && console.info(`\x1B[33m[net]\x1B[39m`, ...data);
484
- }
485
- function raw(...data) {
486
- reporter.isEnabled("raw") && console.log(`\x1B[34m[raw]\x1B[39m`, ...data);
487
- }
488
- function warn(...data) {
489
- reporter.isEnabled("warn") && console.warn(`\x1B[33m[warn]\x1B[39m`, ...data);
490
- }
491
- var reporter = new Reporter;
492
-
493
- // src/context.ts
494
- class Context {
495
- get deviceId() {
496
- return this.#deviceId;
497
- }
498
- get logger() {
499
- return this.#logger;
500
- }
501
- #deviceId;
502
- #logger;
503
- constructor(deviceId) {
504
- this.#deviceId = deviceId;
505
- this.#logger = new Logger(deviceId);
506
- }
507
- }
508
- // src/pairing.ts
509
- import { OPack, TLV8 } from "@basmilius/apple-encoding";
510
- import { Chacha20, Curve25519, Ed25519, hkdf } from "@basmilius/apple-encryption";
511
- import { SRP, SrpClient } from "fast-srp-hap";
512
- class BasePairing {
513
- get context() {
514
- return this.#context;
515
- }
516
- #context;
517
- constructor(context) {
518
- this.#context = context;
519
- }
520
- tlv(buffer) {
521
- const data = TLV8.decode(buffer);
522
- if (data.has(TLV8.Value.Error)) {
523
- TLV8.bail(data);
524
- }
525
- this.#context.logger.raw("Decoded TLV", data);
526
- return data;
527
- }
528
- }
529
-
530
- class AccessoryPair extends BasePairing {
531
- #name;
532
- #pairingId;
533
- #requestHandler;
534
- #publicKey;
535
- #secretKey;
536
- #srp;
537
- constructor(context, requestHandler) {
538
- super(context);
539
- this.#name = "basmilius/apple-protocols";
540
- this.#pairingId = Buffer.from(v4_default().toUpperCase());
541
- this.#requestHandler = requestHandler;
542
- }
543
- async start() {
544
- const keyPair = Ed25519.generateKeyPair();
545
- this.#publicKey = Buffer.from(keyPair.publicKey);
546
- this.#secretKey = Buffer.from(keyPair.secretKey);
547
- }
548
- async pin(askPin) {
549
- const m1 = await this.m1();
550
- const m2 = await this.m2(m1, await askPin());
551
- const m3 = await this.m3(m2);
552
- const m4 = await this.m4(m3);
553
- const m5 = await this.m5(m4);
554
- const m6 = await this.m6(m4, m5);
555
- if (!m6) {
556
- throw new Error("Pairing failed, could not get accessory keys.");
557
- }
558
- return m6;
559
- }
560
- async transient() {
561
- const m1 = await this.m1([[TLV8.Value.Flags, TLV8.Flags.TransientPairing]]);
562
- const m2 = await this.m2(m1);
563
- const m3 = await this.m3(m2);
564
- const m4 = await this.m4(m3);
565
- const accessoryToControllerKey = hkdf({
566
- hash: "sha512",
567
- key: m4.sharedSecret,
568
- length: 32,
569
- salt: Buffer.from("Control-Salt"),
570
- info: Buffer.from("Control-Read-Encryption-Key")
571
- });
572
- const controllerToAccessoryKey = hkdf({
573
- hash: "sha512",
574
- key: m4.sharedSecret,
575
- length: 32,
576
- salt: Buffer.from("Control-Salt"),
577
- info: Buffer.from("Control-Write-Encryption-Key")
578
- });
579
- return {
580
- pairingId: this.#pairingId,
581
- sharedSecret: m4.sharedSecret,
582
- accessoryToControllerKey,
583
- controllerToAccessoryKey
584
- };
585
- }
586
- async m1(additionalTlv = []) {
587
- const response = await this.#requestHandler("m1", TLV8.encode([
588
- [TLV8.Value.Method, TLV8.Method.PairSetup],
589
- [TLV8.Value.State, TLV8.State.M1],
590
- ...additionalTlv
591
- ]));
592
- const data = this.tlv(response);
593
- const publicKey = data.get(TLV8.Value.PublicKey);
594
- const salt = data.get(TLV8.Value.Salt);
595
- return { publicKey, salt };
596
- }
597
- async m2(m1, pin = AIRPLAY_TRANSIENT_PIN) {
598
- const srpKey = await SRP.genKey(32);
599
- this.#srp = new SrpClient(SRP.params.hap, m1.salt, Buffer.from("Pair-Setup"), Buffer.from(pin), srpKey, true);
600
- this.#srp.setB(m1.publicKey);
601
- const publicKey = this.#srp.computeA();
602
- const proof = this.#srp.computeM1();
603
- return { publicKey, proof };
604
- }
605
- async m3(m2) {
606
- const response = await this.#requestHandler("m3", TLV8.encode([
607
- [TLV8.Value.State, TLV8.State.M3],
608
- [TLV8.Value.PublicKey, m2.publicKey],
609
- [TLV8.Value.Proof, m2.proof]
610
- ]));
611
- const data = this.tlv(response);
612
- const serverProof = data.get(TLV8.Value.Proof);
613
- return { serverProof };
614
- }
615
- async m4(m3) {
616
- this.#srp.checkM2(m3.serverProof);
617
- const sharedSecret = this.#srp.computeK();
618
- return { sharedSecret };
619
- }
620
- async m5(m4) {
621
- const iosDeviceX = hkdf({
622
- hash: "sha512",
623
- key: m4.sharedSecret,
624
- length: 32,
625
- salt: Buffer.from("Pair-Setup-Controller-Sign-Salt", "utf8"),
626
- info: Buffer.from("Pair-Setup-Controller-Sign-Info", "utf8")
627
- });
628
- const sessionKey = hkdf({
629
- hash: "sha512",
630
- key: m4.sharedSecret,
631
- length: 32,
632
- salt: Buffer.from("Pair-Setup-Encrypt-Salt", "utf8"),
633
- info: Buffer.from("Pair-Setup-Encrypt-Info", "utf8")
634
- });
635
- const deviceInfo = Buffer.concat([
636
- iosDeviceX,
637
- this.#pairingId,
638
- this.#publicKey
639
- ]);
640
- const signature = Ed25519.sign(deviceInfo, this.#secretKey);
641
- const innerTlv = TLV8.encode([
642
- [TLV8.Value.Identifier, this.#pairingId],
643
- [TLV8.Value.PublicKey, this.#publicKey],
644
- [TLV8.Value.Signature, Buffer.from(signature)],
645
- [TLV8.Value.Name, OPack.encode({
646
- name: this.#name
647
- })]
648
- ]);
649
- const { authTag, ciphertext } = Chacha20.encrypt(sessionKey, Buffer.from("PS-Msg05"), null, innerTlv);
650
- const encrypted = Buffer.concat([ciphertext, authTag]);
651
- const response = await this.#requestHandler("m5", TLV8.encode([
652
- [TLV8.Value.State, TLV8.State.M5],
653
- [TLV8.Value.EncryptedData, encrypted]
654
- ]));
655
- const data = this.tlv(response);
656
- const encryptedDataRaw = data.get(TLV8.Value.EncryptedData);
657
- const encryptedData = encryptedDataRaw.subarray(0, -16);
658
- const encryptedTag = encryptedDataRaw.subarray(-16);
659
- return {
660
- authTag: encryptedTag,
661
- data: encryptedData,
662
- sessionKey
663
- };
664
- }
665
- async m6(m4, m5) {
666
- const data = Chacha20.decrypt(m5.sessionKey, Buffer.from("PS-Msg06"), null, m5.data, m5.authTag);
667
- const tlv = TLV8.decode(data);
668
- const accessoryIdentifier = tlv.get(TLV8.Value.Identifier);
669
- const accessoryLongTermPublicKey = tlv.get(TLV8.Value.PublicKey);
670
- const accessorySignature = tlv.get(TLV8.Value.Signature);
671
- const accessoryX = hkdf({
672
- hash: "sha512",
673
- key: m4.sharedSecret,
674
- length: 32,
675
- salt: Buffer.from("Pair-Setup-Accessory-Sign-Salt"),
676
- info: Buffer.from("Pair-Setup-Accessory-Sign-Info")
677
- });
678
- const accessoryInfo = Buffer.concat([
679
- accessoryX,
680
- accessoryIdentifier,
681
- accessoryLongTermPublicKey
682
- ]);
683
- if (!Ed25519.verify(accessoryInfo, accessorySignature, accessoryLongTermPublicKey)) {
684
- throw new Error("Invalid accessory signature.");
685
- }
686
- return {
687
- accessoryIdentifier: accessoryIdentifier.toString(),
688
- accessoryLongTermPublicKey,
689
- pairingId: this.#pairingId,
690
- publicKey: this.#publicKey,
691
- secretKey: this.#secretKey
692
- };
693
- }
694
- }
695
-
696
- class AccessoryVerify extends BasePairing {
697
- #ephemeralKeyPair;
698
- #requestHandler;
699
- constructor(context, requestHandler) {
700
- super(context);
701
- this.#ephemeralKeyPair = Curve25519.generateKeyPair();
702
- this.#requestHandler = requestHandler;
703
- }
704
- async start(credentials) {
705
- const m1 = await this.#m1();
706
- const m2 = await this.#m2(credentials.accessoryIdentifier, credentials.accessoryLongTermPublicKey, m1);
707
- await this.#m3(credentials.pairingId, credentials.secretKey, m2);
708
- return await this.#m4(m2, credentials.pairingId);
709
- }
710
- async#m1() {
711
- const response = await this.#requestHandler("m1", TLV8.encode([
712
- [TLV8.Value.State, TLV8.State.M1],
713
- [TLV8.Value.PublicKey, Buffer.from(this.#ephemeralKeyPair.publicKey)]
714
- ]));
715
- const data = this.tlv(response);
716
- const serverPublicKey = data.get(TLV8.Value.PublicKey);
717
- const encryptedData = data.get(TLV8.Value.EncryptedData);
718
- return {
719
- encryptedData,
720
- serverPublicKey
721
- };
722
- }
723
- async#m2(localAccessoryIdentifier, longTermPublicKey, m1) {
724
- const sharedSecret = Buffer.from(Curve25519.generateSharedSecKey(this.#ephemeralKeyPair.secretKey, m1.serverPublicKey));
725
- const sessionKey = hkdf({
726
- hash: "sha512",
727
- key: sharedSecret,
728
- length: 32,
729
- salt: Buffer.from("Pair-Verify-Encrypt-Salt"),
730
- info: Buffer.from("Pair-Verify-Encrypt-Info")
731
- });
732
- const encryptedData = m1.encryptedData.subarray(0, -16);
733
- const encryptedTag = m1.encryptedData.subarray(-16);
734
- const data = Chacha20.decrypt(sessionKey, Buffer.from("PV-Msg02"), null, encryptedData, encryptedTag);
735
- const tlv = TLV8.decode(data);
736
- const accessoryIdentifier = tlv.get(TLV8.Value.Identifier);
737
- const accessorySignature = tlv.get(TLV8.Value.Signature);
738
- if (accessoryIdentifier.toString() !== localAccessoryIdentifier) {
739
- throw new Error(`Invalid accessory identifier. Expected ${accessoryIdentifier.toString()} to be ${localAccessoryIdentifier}.`);
740
- }
741
- const accessoryInfo = Buffer.concat([
742
- m1.serverPublicKey,
743
- accessoryIdentifier,
744
- this.#ephemeralKeyPair.publicKey
745
- ]);
746
- if (!Ed25519.verify(accessoryInfo, accessorySignature, longTermPublicKey)) {
747
- throw new Error("Invalid accessory signature.");
748
- }
749
- return {
750
- serverEphemeralPublicKey: m1.serverPublicKey,
751
- sessionKey,
752
- sharedSecret
753
- };
754
- }
755
- async#m3(pairingId, secretKey, m2) {
756
- const iosDeviceInfo = Buffer.concat([
757
- this.#ephemeralKeyPair.publicKey,
758
- pairingId,
759
- m2.serverEphemeralPublicKey
760
- ]);
761
- const iosDeviceSignature = Buffer.from(Ed25519.sign(iosDeviceInfo, secretKey));
762
- const innerTlv = TLV8.encode([
763
- [TLV8.Value.Identifier, pairingId],
764
- [TLV8.Value.Signature, iosDeviceSignature]
765
- ]);
766
- const { authTag, ciphertext } = Chacha20.encrypt(m2.sessionKey, Buffer.from("PV-Msg03"), null, innerTlv);
767
- const encrypted = Buffer.concat([ciphertext, authTag]);
768
- await this.#requestHandler("m3", TLV8.encode([
769
- [TLV8.Value.State, TLV8.State.M3],
770
- [TLV8.Value.EncryptedData, encrypted]
771
- ]));
772
- return {};
773
- }
774
- async#m4(m2, pairingId) {
775
- return {
776
- accessoryToControllerKey: Buffer.alloc(0),
777
- controllerToAccessoryKey: Buffer.alloc(0),
778
- pairingId,
779
- sharedSecret: m2.sharedSecret
780
- };
781
- }
782
- }
783
- // src/timing.ts
784
- import { createSocket } from "node:dgram";
785
- import { NTP } from "@basmilius/apple-encoding";
786
- class TimingServer {
787
- get port() {
788
- return this.#port;
789
- }
790
- #logger;
791
- #socket;
792
- #port = 0;
793
- constructor() {
794
- this.#logger = new Logger("timing-server");
795
- this.#socket = createSocket("udp4");
796
- this.#socket.on("connect", this.#onConnect.bind(this));
797
- this.#socket.on("error", this.#onError.bind(this));
798
- this.#socket.on("message", this.#onMessage.bind(this));
799
- }
800
- async close() {
801
- this.#socket.close();
802
- this.#port = 0;
803
- }
804
- async listen() {
805
- return new Promise((resolve) => {
806
- this.#socket.once("listening", () => this.#onListening());
807
- this.#socket.bind(0, resolve);
808
- });
809
- }
810
- #onConnect() {
811
- this.#socket.setRecvBufferSize(16384);
812
- this.#socket.setSendBufferSize(16384);
813
- }
814
- async#onError(err) {
815
- this.#logger.error("Timing server error", err);
816
- }
817
- async#onListening() {
818
- const { port } = this.#socket.address();
819
- this.#port = port;
820
- }
821
- async#onMessage(data, info2) {
822
- try {
823
- const request = NTP.decode(data);
824
- const ntp = NTP.now();
825
- const [receivedSeconds, receivedFraction] = NTP.parts(ntp);
826
- this.#logger.info(`Timing server ntp=${ntp} receivedSeconds=${receivedSeconds} receivedFraction=${receivedFraction}`);
827
- const response = NTP.encode({
828
- proto: request.proto,
829
- type: 83 | 128,
830
- seqno: request.seqno,
831
- padding: 0,
832
- reftime_sec: request.sendtime_sec,
833
- reftime_frac: request.sendtime_frac,
834
- recvtime_sec: receivedSeconds,
835
- recvtime_frac: receivedFraction,
836
- sendtime_sec: receivedSeconds,
837
- sendtime_frac: receivedFraction
838
- });
839
- this.#socket.send(response, info2.port, info2.address);
840
- } catch (err) {
841
- this.#logger.warn(`Timing server received malformed packet (${data.length} bytes) from ${info2.address}:${info2.port}`, err);
842
- }
843
- }
844
- }
845
- // src/utils.ts
846
- import { randomBytes } from "node:crypto";
847
- import { networkInterfaces } from "node:os";
848
- function getLocalIP() {
849
- const interfaces = networkInterfaces();
850
- for (const iface of Object.values(interfaces)) {
851
- if (!iface) {
852
- continue;
853
- }
854
- for (const net2 of iface) {
855
- if (net2.internal || net2.family !== "IPv4") {
856
- continue;
857
- }
858
- if (net2.address && net2.address !== "127.0.0.1") {
859
- return net2.address;
860
- }
861
- }
862
- }
863
- return null;
864
- }
865
- function getMacAddress() {
866
- const interfaces = networkInterfaces();
867
- for (const iface of Object.values(interfaces)) {
868
- if (!iface) {
869
- continue;
870
- }
871
- for (const net2 of iface) {
872
- if (net2.internal || net2.family !== "IPv4") {
873
- continue;
874
- }
875
- if (net2.mac && net2.mac !== "00:00:00:00:00:00") {
876
- return net2.mac.toUpperCase();
877
- }
878
- }
879
- }
880
- return "00:00:00:00:00:00";
881
- }
882
- function randomInt32() {
883
- return randomBytes(4).readUInt32BE(0);
884
- }
885
- function randomInt64() {
886
- return randomBytes(8).readBigUint64LE(0);
887
- }
888
- function uint16ToBE(value) {
889
- const buffer = Buffer.allocUnsafe(2);
890
- buffer.writeUInt16BE(value, 0);
891
- return buffer;
892
- }
893
- function uint53ToLE(value) {
894
- const [upper, lower] = splitUInt53(value);
895
- const buffer = Buffer.allocUnsafe(8);
896
- buffer.writeUInt32LE(lower, 0);
897
- buffer.writeUInt32LE(upper, 4);
898
- return buffer;
899
- }
900
- function splitUInt53(number) {
901
- const MAX_UINT32 = 4294967295;
902
- const MAX_INT53 = 9007199254740991;
903
- if (number <= -1 || number > MAX_INT53) {
904
- throw new Error("Number out of range.");
905
- }
906
- if (Math.floor(number) !== number) {
907
- throw new Error("Number is not an integer.");
908
- }
909
- let upper = 0;
910
- const signbit = number & 4294967295;
911
- const lower = signbit < 0 ? (number & 2147483647) + 2147483648 : signbit;
912
- if (number > MAX_UINT32) {
913
- upper = (number - lower) / (MAX_UINT32 + 1);
914
- }
915
- return [upper, lower];
916
- }
917
- export {
918
- waitFor,
919
- v4_default as uuid,
920
- uint53ToLE,
921
- uint16ToBE,
922
- reporter,
923
- randomInt64,
924
- randomInt32,
925
- prompt,
926
- getMacAddress,
927
- getLocalIP,
928
- TimingServer,
929
- RAOP_SERVICE,
930
- HTTP_TIMEOUT,
931
- EncryptionState,
932
- EncryptionAwareConnection,
933
- ENCRYPTION,
934
- Discovery,
935
- Context,
936
- Connection,
937
- COMPANION_LINK_SERVICE,
938
- AccessoryVerify,
939
- AccessoryPair,
940
- AIRPLAY_TRANSIENT_PIN,
941
- AIRPLAY_SERVICE
942
- };