appium-ios-remotexpc 0.21.2 → 0.22.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 (92) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/build/src/lib/apple-tv/constants.d.ts +4 -3
  3. package/build/src/lib/apple-tv/constants.d.ts.map +1 -1
  4. package/build/src/lib/apple-tv/constants.js +10 -3
  5. package/build/src/lib/apple-tv/discovery/device-discovery.d.ts.map +1 -1
  6. package/build/src/lib/apple-tv/discovery/device-discovery.js +2 -2
  7. package/build/src/lib/apple-tv/encryption/index.d.ts +1 -0
  8. package/build/src/lib/apple-tv/encryption/index.d.ts.map +1 -1
  9. package/build/src/lib/apple-tv/encryption/index.js +1 -0
  10. package/build/src/lib/apple-tv/encryption/x25519.d.ts +8 -0
  11. package/build/src/lib/apple-tv/encryption/x25519.d.ts.map +1 -0
  12. package/build/src/lib/apple-tv/encryption/x25519.js +52 -0
  13. package/build/src/lib/apple-tv/index.d.ts +1 -0
  14. package/build/src/lib/apple-tv/index.d.ts.map +1 -1
  15. package/build/src/lib/apple-tv/index.js +1 -0
  16. package/build/src/lib/apple-tv/network/network-client.js +2 -2
  17. package/build/src/lib/apple-tv/pairing/user-input-service.d.ts.map +1 -1
  18. package/build/src/lib/apple-tv/pairing/user-input-service.js +2 -2
  19. package/build/src/lib/apple-tv/pairing-protocol/constants.d.ts +17 -0
  20. package/build/src/lib/apple-tv/pairing-protocol/constants.d.ts.map +1 -1
  21. package/build/src/lib/apple-tv/pairing-protocol/constants.js +25 -0
  22. package/build/src/lib/apple-tv/pairing-protocol/index.d.ts +2 -1
  23. package/build/src/lib/apple-tv/pairing-protocol/index.d.ts.map +1 -1
  24. package/build/src/lib/apple-tv/pairing-protocol/index.js +2 -1
  25. package/build/src/lib/apple-tv/pairing-protocol/pair-verification-protocol.d.ts +66 -0
  26. package/build/src/lib/apple-tv/pairing-protocol/pair-verification-protocol.d.ts.map +1 -0
  27. package/build/src/lib/apple-tv/pairing-protocol/pair-verification-protocol.js +178 -0
  28. package/build/src/lib/apple-tv/pairing-protocol/pairing-protocol.d.ts +35 -2
  29. package/build/src/lib/apple-tv/pairing-protocol/pairing-protocol.d.ts.map +1 -1
  30. package/build/src/lib/apple-tv/pairing-protocol/pairing-protocol.js +48 -14
  31. package/build/src/lib/apple-tv/storage/index.d.ts +1 -1
  32. package/build/src/lib/apple-tv/storage/index.d.ts.map +1 -1
  33. package/build/src/lib/apple-tv/storage/pairing-storage.d.ts +4 -1
  34. package/build/src/lib/apple-tv/storage/pairing-storage.d.ts.map +1 -1
  35. package/build/src/lib/apple-tv/storage/pairing-storage.js +59 -4
  36. package/build/src/lib/apple-tv/storage/types.d.ts +7 -1
  37. package/build/src/lib/apple-tv/storage/types.d.ts.map +1 -1
  38. package/build/src/lib/apple-tv/tunnel/index.d.ts +3 -0
  39. package/build/src/lib/apple-tv/tunnel/index.d.ts.map +1 -0
  40. package/build/src/lib/apple-tv/tunnel/index.js +1 -0
  41. package/build/src/lib/apple-tv/tunnel/tunnel-service.d.ts +57 -0
  42. package/build/src/lib/apple-tv/tunnel/tunnel-service.d.ts.map +1 -0
  43. package/build/src/lib/apple-tv/tunnel/tunnel-service.js +357 -0
  44. package/build/src/lib/apple-tv/tunnel/types.d.ts +22 -0
  45. package/build/src/lib/apple-tv/tunnel/types.d.ts.map +1 -0
  46. package/build/src/lib/apple-tv/tunnel/types.js +1 -0
  47. package/build/src/lib/bonjour/bonjour-discovery.d.ts.map +1 -1
  48. package/build/src/lib/bonjour/bonjour-discovery.js +3 -3
  49. package/build/src/lib/lockdown/index.d.ts.map +1 -1
  50. package/build/src/lib/plist/length-based-splitter.d.ts.map +1 -1
  51. package/build/src/lib/plist/length-based-splitter.js +0 -7
  52. package/build/src/lib/tunnel/index.d.ts +1 -0
  53. package/build/src/lib/tunnel/index.d.ts.map +1 -1
  54. package/build/src/lib/tunnel/packet-stream-server.d.ts.map +1 -1
  55. package/build/src/lib/tunnel/tunnel-registry-server.d.ts +1 -0
  56. package/build/src/lib/tunnel/tunnel-registry-server.d.ts.map +1 -1
  57. package/build/src/lib/tunnel/tunnel-registry-server.js +1 -1
  58. package/build/src/services/ios/afc/index.d.ts.map +1 -1
  59. package/build/src/services/ios/afc/index.js +1 -1
  60. package/build/src/services/ios/afc/stream-utils.d.ts.map +1 -1
  61. package/build/src/services/ios/afc/stream-utils.js +0 -2
  62. package/build/src/services/ios/mobile-config/index.js +2 -2
  63. package/package.json +2 -1
  64. package/scripts/pair-appletv.ts +2 -2
  65. package/scripts/start-appletv-tunnel.ts +178 -0
  66. package/scripts/test-tunnel-creation.ts +32 -23
  67. package/src/lib/apple-tv/constants.ts +11 -3
  68. package/src/lib/apple-tv/discovery/device-discovery.ts +2 -3
  69. package/src/lib/apple-tv/encryption/index.ts +6 -0
  70. package/src/lib/apple-tv/encryption/x25519.ts +79 -0
  71. package/src/lib/apple-tv/index.ts +1 -0
  72. package/src/lib/apple-tv/network/network-client.ts +2 -2
  73. package/src/lib/apple-tv/pairing/user-input-service.ts +2 -2
  74. package/src/lib/apple-tv/pairing-protocol/constants.ts +29 -0
  75. package/src/lib/apple-tv/pairing-protocol/index.ts +12 -1
  76. package/src/lib/apple-tv/pairing-protocol/pair-verification-protocol.ts +329 -0
  77. package/src/lib/apple-tv/pairing-protocol/pairing-protocol.ts +49 -19
  78. package/src/lib/apple-tv/storage/index.ts +1 -1
  79. package/src/lib/apple-tv/storage/pairing-storage.ts +73 -5
  80. package/src/lib/apple-tv/storage/types.ts +8 -1
  81. package/src/lib/apple-tv/tunnel/index.ts +2 -0
  82. package/src/lib/apple-tv/tunnel/tunnel-service.ts +543 -0
  83. package/src/lib/apple-tv/tunnel/types.ts +23 -0
  84. package/src/lib/bonjour/bonjour-discovery.ts +3 -5
  85. package/src/lib/lockdown/index.ts +0 -7
  86. package/src/lib/plist/length-based-splitter.ts +0 -22
  87. package/src/lib/tunnel/index.ts +2 -8
  88. package/src/lib/tunnel/packet-stream-server.ts +0 -8
  89. package/src/lib/tunnel/tunnel-registry-server.ts +1 -1
  90. package/src/services/ios/afc/index.ts +0 -2
  91. package/src/services/ios/afc/stream-utils.ts +0 -2
  92. package/src/services/ios/mobile-config/index.ts +2 -2
@@ -0,0 +1,329 @@
1
+ import { type KeyObject } from 'node:crypto';
2
+ import { hostname } from 'node:os';
3
+
4
+ import { getLogger } from '../../logger.js';
5
+ import { PairingDataComponentType } from '../constants.js';
6
+ import {
7
+ createEd25519Signature,
8
+ encryptChaCha20Poly1305,
9
+ generateX25519KeyPair,
10
+ hkdf,
11
+ performX25519DiffieHellman,
12
+ } from '../encryption/index.js';
13
+ import { PairingError } from '../errors.js';
14
+ import type { NetworkClientInterface } from '../network/types.js';
15
+ import type { PairRecord } from '../storage/types.js';
16
+ import { decodeTLV8ToDict, encodeTLV8 } from '../tlv/index.js';
17
+ import { generateHostId } from '../utils/uuid-generator.js';
18
+ import {
19
+ ENCRYPTION_MESSAGES,
20
+ PAIR_VERIFY_ERROR_DESCRIPTIONS,
21
+ PAIR_VERIFY_MESSAGES,
22
+ PAIR_VERIFY_STATES,
23
+ } from './constants.js';
24
+ import type { PairingRequest } from './types.js';
25
+
26
+ const log = getLogger('PairVerificationProtocol');
27
+
28
+ /**
29
+ * Implements the HomeKit Accessory Protocol (HAP) Pair-Verify process for Apple TV.
30
+ *
31
+ * Protocol Overview:
32
+ * This class implements the HAP Pair-Verify protocol, which establishes an encrypted
33
+ * session between a previously paired controller (this client) and an Apple TV accessory.
34
+ * Unlike Pair-Setup, Pair-Verify uses existing long-term keys to authenticate both parties
35
+ * and derive session-specific encryption keys without requiring user interaction.
36
+ *
37
+ * State Machine Flow (4 states):
38
+ * - STATE=1: Client sends ephemeral X25519 public key to device
39
+ * - STATE=2: Device responds with its ephemeral X25519 public key and encrypted proof
40
+ * - STATE=3: Client sends encrypted signature proving identity using Ed25519 private key
41
+ * - STATE=4: Device confirms verification success or returns error
42
+ *
43
+ * After successful verification, both parties derive shared session keys for encrypting
44
+ * all subsequent communication during this session.
45
+ *
46
+ * Technical Details:
47
+ * - Uses X25519 Elliptic Curve Diffie-Hellman for ephemeral key exchange (RFC 7748)
48
+ * - Employs Ed25519 signatures for authentication using long-term keys (RFC 8032)
49
+ * - Uses ChaCha20-Poly1305 for authenticated encryption (RFC 8439)
50
+ * - Derives session keys using HKDF with protocol-specific salt/info (RFC 5869)
51
+ * - Encodes messages in TLV8 (Type-Length-Value) format
52
+ *
53
+ * Security Properties:
54
+ * - Perfect Forward Secrecy: Each session uses unique ephemeral keys
55
+ * - Mutual Authentication: Both client and device prove their identities
56
+ * - Replay Protection: Ephemeral keys prevent replay attacks
57
+ *
58
+ * References:
59
+ * - HAP Specification: https://developer.apple.com/homekit/ (Apple Developer)
60
+ * - HAP-NodeJS (community implementation): https://github.com/homebridge/HAP-NodeJS
61
+ * - X25519 ECDH: https://datatracker.ietf.org/doc/html/rfc7748
62
+ * - Ed25519 Signatures: https://datatracker.ietf.org/doc/html/rfc8032
63
+ * - ChaCha20-Poly1305: https://datatracker.ietf.org/doc/html/rfc8439
64
+ * - HKDF: https://datatracker.ietf.org/doc/html/rfc5869
65
+ *
66
+ * @see PairingProtocol for the initial pairing process that generates the long-term keys
67
+ * @see PairRecord for the stored credentials used in verification
68
+ */
69
+
70
+ export interface VerificationKeys {
71
+ encryptionKey: Buffer;
72
+ clientEncryptionKey: Buffer;
73
+ serverEncryptionKey: Buffer;
74
+ }
75
+
76
+ export class PairVerificationProtocol {
77
+ private readonly hostIdentifier: string;
78
+ private sequenceNumber: number = 0;
79
+
80
+ constructor(private readonly networkClient: NetworkClientInterface) {
81
+ this.hostIdentifier = generateHostId(hostname());
82
+ }
83
+
84
+ async verify(
85
+ pairRecord: PairRecord,
86
+ deviceId: string,
87
+ ): Promise<VerificationKeys> {
88
+ log.debug('Starting pair verification (4-step process)');
89
+
90
+ const { publicKey, privateKey } = generateX25519KeyPair();
91
+
92
+ log.debug(' - STATE=1: Send X25519 public key to device');
93
+ await this.sendState1(publicKey);
94
+
95
+ const devicePublicKey = await this.processState2Response();
96
+
97
+ const sharedSecret = this.computeSharedSecret(privateKey, devicePublicKey);
98
+ const pairVerifyKey = this.derivePairVerifyKey(sharedSecret);
99
+
100
+ log.debug(
101
+ ' - STATE=3: Send encrypted signature using Ed25519 private key from pair record',
102
+ );
103
+ log.debug(` Using pair record: ${deviceId}`);
104
+
105
+ await this.sendState3(
106
+ pairRecord,
107
+ publicKey,
108
+ devicePublicKey,
109
+ pairVerifyKey,
110
+ );
111
+
112
+ await this.validateState4Response();
113
+
114
+ log.debug(' - STATE=4: Receive verification success from device');
115
+
116
+ return this.deriveEncryptionKeys(sharedSecret);
117
+ }
118
+
119
+ getSequenceNumber(): number {
120
+ return this.sequenceNumber;
121
+ }
122
+
123
+ setSequenceNumber(value: number): void {
124
+ this.sequenceNumber = value;
125
+ }
126
+
127
+ private async processState2Response(): Promise<Buffer> {
128
+ const state2Response = await this.networkClient.receiveResponse();
129
+
130
+ const pairingData =
131
+ state2Response.message?.plain?._0?.event?._0?.pairingData?._0?.data;
132
+ if (!pairingData) {
133
+ throw new PairingError(
134
+ 'No pairing data in STATE=2 response',
135
+ 'STATE_2_NO_DATA',
136
+ );
137
+ }
138
+
139
+ const tlvData = decodeTLV8ToDict(Buffer.from(pairingData, 'base64'));
140
+
141
+ if (tlvData[PairingDataComponentType.ERROR]) {
142
+ const errorCode = tlvData[PairingDataComponentType.ERROR] as Buffer;
143
+ const errorDecimal = errorCode[0];
144
+ log.error(
145
+ `Device returned error in STATE=2: ${errorCode.toString('hex')} (decimal: ${errorDecimal})`,
146
+ );
147
+ throw new PairingError(
148
+ `Authentication failed at STATE=2 (error: ${errorDecimal})`,
149
+ 'STATE_2_ERROR',
150
+ );
151
+ }
152
+
153
+ const devicePublicKey = tlvData[PairingDataComponentType.PUBLIC_KEY];
154
+ if (!devicePublicKey) {
155
+ throw new PairingError(
156
+ 'No device public key in STATE=2',
157
+ 'STATE_2_NO_PUBLIC_KEY',
158
+ );
159
+ }
160
+
161
+ log.debug(' - STATE=2: Receive devices X25519 public key + encrypted data');
162
+
163
+ return devicePublicKey;
164
+ }
165
+
166
+ private computeSharedSecret(
167
+ privateKey: KeyObject,
168
+ devicePublicKey: Buffer,
169
+ ): Buffer {
170
+ return performX25519DiffieHellman(privateKey, devicePublicKey);
171
+ }
172
+
173
+ private derivePairVerifyKey(sharedSecret: Buffer): Buffer {
174
+ return hkdf({
175
+ ikm: sharedSecret,
176
+ salt: Buffer.from(PAIR_VERIFY_MESSAGES.ENCRYPT_SALT),
177
+ info: Buffer.from(PAIR_VERIFY_MESSAGES.ENCRYPT_INFO),
178
+ length: 32,
179
+ });
180
+ }
181
+
182
+ private async validateState4Response(): Promise<void> {
183
+ const state4Response = await this.networkClient.receiveResponse();
184
+
185
+ const state4Data =
186
+ state4Response.message?.plain?._0?.event?._0?.pairingData?._0?.data;
187
+ if (!state4Data) {
188
+ return;
189
+ }
190
+
191
+ const state4TLV = decodeTLV8ToDict(Buffer.from(state4Data, 'base64'));
192
+
193
+ if (state4TLV[PairingDataComponentType.ERROR]) {
194
+ const errorCode = state4TLV[PairingDataComponentType.ERROR] as Buffer;
195
+ const errorDecimal = errorCode[0];
196
+
197
+ const errorDescription =
198
+ PAIR_VERIFY_ERROR_DESCRIPTIONS[errorDecimal] || 'Unknown error';
199
+
200
+ log.error(
201
+ `Device returned error in STATE=4: ${errorCode.toString('hex')} (decimal: ${errorDecimal})`,
202
+ );
203
+ log.error(`Error description: ${errorDescription}`);
204
+ throw new PairingError(
205
+ `Pair verification failed: ${errorDescription}`,
206
+ 'STATE_4_ERROR',
207
+ );
208
+ }
209
+ }
210
+
211
+ private createPairingPayload(
212
+ data: string,
213
+ startNewSession: boolean,
214
+ ): PairingRequest {
215
+ return {
216
+ message: {
217
+ plain: {
218
+ _0: {
219
+ event: {
220
+ _0: {
221
+ pairingData: {
222
+ _0: {
223
+ data,
224
+ kind: 'verifyManualPairing',
225
+ startNewSession,
226
+ },
227
+ },
228
+ },
229
+ },
230
+ },
231
+ },
232
+ },
233
+ originatedBy: 'host',
234
+ sequenceNumber: this.sequenceNumber++,
235
+ };
236
+ }
237
+
238
+ private async sendState1(x25519PublicKey: Buffer): Promise<void> {
239
+ const tlvData = encodeTLV8([
240
+ {
241
+ type: PairingDataComponentType.STATE,
242
+ data: Buffer.from([PAIR_VERIFY_STATES.STATE_01]),
243
+ },
244
+ { type: PairingDataComponentType.PUBLIC_KEY, data: x25519PublicKey },
245
+ ]);
246
+
247
+ const payload = this.createPairingPayload(tlvData.toString('base64'), true);
248
+
249
+ await this.networkClient.sendPacket(payload);
250
+ }
251
+
252
+ private async sendState3(
253
+ pairRecord: PairRecord,
254
+ x25519PublicKey: Buffer,
255
+ devicePublicKey: Buffer,
256
+ pairVerifyEncryptionKey: Buffer,
257
+ ): Promise<void> {
258
+ const signData = Buffer.concat([
259
+ x25519PublicKey,
260
+ Buffer.from(this.hostIdentifier, 'utf8'),
261
+ devicePublicKey,
262
+ ]);
263
+
264
+ const signature = createEd25519Signature(signData, pairRecord.privateKey);
265
+
266
+ const responseTLV = encodeTLV8([
267
+ {
268
+ type: PairingDataComponentType.IDENTIFIER,
269
+ data: Buffer.from(this.hostIdentifier, 'utf8'),
270
+ },
271
+ { type: PairingDataComponentType.SIGNATURE, data: signature },
272
+ ]);
273
+
274
+ const nonce = Buffer.concat([
275
+ Buffer.alloc(4),
276
+ Buffer.from(PAIR_VERIFY_MESSAGES.STATE_03_NONCE),
277
+ ]);
278
+
279
+ const encryptedResponse = encryptChaCha20Poly1305({
280
+ plaintext: responseTLV,
281
+ key: pairVerifyEncryptionKey,
282
+ nonce,
283
+ });
284
+
285
+ const finalTLV = encodeTLV8([
286
+ {
287
+ type: PairingDataComponentType.STATE,
288
+ data: Buffer.from([PAIR_VERIFY_STATES.STATE_03]),
289
+ },
290
+ {
291
+ type: PairingDataComponentType.ENCRYPTED_DATA,
292
+ data: encryptedResponse,
293
+ },
294
+ ]);
295
+
296
+ const payload = this.createPairingPayload(
297
+ finalTLV.toString('base64'),
298
+ false,
299
+ );
300
+
301
+ await this.networkClient.sendPacket(payload);
302
+ }
303
+
304
+ private deriveEncryptionKeys(sharedSecret: Buffer): VerificationKeys {
305
+ log.debug('Deriving main encryption keys');
306
+
307
+ const clientEncryptionKey = hkdf({
308
+ ikm: sharedSecret,
309
+ salt: null,
310
+ info: Buffer.from(ENCRYPTION_MESSAGES.CLIENT_ENCRYPT),
311
+ length: 32,
312
+ });
313
+
314
+ const serverEncryptionKey = hkdf({
315
+ ikm: sharedSecret,
316
+ salt: null,
317
+ info: Buffer.from(ENCRYPTION_MESSAGES.SERVER_ENCRYPT),
318
+ length: 32,
319
+ });
320
+
321
+ log.debug('Derived client/server encryption keys using HKDF');
322
+
323
+ return {
324
+ encryptionKey: sharedSecret,
325
+ clientEncryptionKey,
326
+ serverEncryptionKey,
327
+ };
328
+ }
329
+ }
@@ -1,8 +1,8 @@
1
- import { logger } from '@appium/support';
2
1
  import { randomBytes } from 'node:crypto';
3
2
  import { hostname } from 'node:os';
4
3
 
5
4
  import type { AppleTVDevice } from '../../bonjour/bonjour-discovery.js';
5
+ import { getLogger } from '../../logger.js';
6
6
  import {
7
7
  DEFAULT_PAIRING_CONFIG,
8
8
  PairingDataComponentType,
@@ -36,9 +36,44 @@ import type {
36
36
  UserInputInterface,
37
37
  } from './types.js';
38
38
 
39
- /** Implements the Apple TV pairing protocol including SRP authentication and key exchange */
39
+ const log = getLogger('PairingProtocol');
40
+
41
+ /**
42
+ * Implements the HomeKit Accessory Protocol (HAP) Pair-Setup process for Apple TV.
43
+ *
44
+ * Protocol Overview:
45
+ * This class implements the HAP Pair-Setup protocol, which establishes a secure pairing
46
+ * between a controller (this client) and an Apple TV accessory. The protocol uses SRP
47
+ * (Secure Remote Password) authentication to verify a user-provided PIN without transmitting
48
+ * the PIN itself over the network.
49
+ *
50
+ * Message Exchange Flow (M1-M6):
51
+ * - M1/M2: Initial setup request and SRP challenge (salt + server public key)
52
+ * - M3/M4: Client sends SRP proof, server validates and responds
53
+ * - M5/M6: Exchange of long-term public keys and signatures (encrypted)
54
+ *
55
+ * After successful pairing, the generated Ed25519 key pair is stored and used for
56
+ * subsequent Pair-Verify operations to establish encrypted sessions.
57
+ *
58
+ * Technical Details:
59
+ * - Uses SRP-6a protocol for password-authenticated key exchange (RFC 5054)
60
+ * - Employs Ed25519 for long-term identity keys (RFC 8032)
61
+ * - Uses ChaCha20-Poly1305 for authenticated encryption (RFC 8439)
62
+ * - Derives session keys using HKDF (RFC 5869)
63
+ * - Encodes messages in TLV8 (Type-Length-Value) format
64
+ *
65
+ * References:
66
+ * - HAP Specification: https://developer.apple.com/homekit/ (Apple Developer)
67
+ * - HAP-NodeJS (community implementation): https://github.com/homebridge/HAP-NodeJS
68
+ * - SRP Protocol: https://datatracker.ietf.org/doc/html/rfc5054
69
+ * - Ed25519: https://datatracker.ietf.org/doc/html/rfc8032
70
+ * - ChaCha20-Poly1305: https://datatracker.ietf.org/doc/html/rfc8439
71
+ * - HKDF: https://datatracker.ietf.org/doc/html/rfc5869
72
+ *
73
+ * @see PairVerificationProtocol for the verification protocol used after pairing
74
+ * @see SRPClient for SRP authentication implementation
75
+ */
40
76
  export class PairingProtocol implements PairingProtocolInterface {
41
- private static readonly log = logger.getLogger('PairingProtocol');
42
77
  private _sequenceNumber = 0;
43
78
 
44
79
  constructor(
@@ -81,7 +116,7 @@ export class PairingProtocol implements PairingProtocolInterface {
81
116
 
82
117
  return this.createPairingResult(device, ltpk, ltsk);
83
118
  } catch (error) {
84
- PairingProtocol.log.error('Pairing flow failed:', error);
119
+ log.error('Pairing flow failed:', error);
85
120
  throw error;
86
121
  }
87
122
  }
@@ -136,7 +171,7 @@ export class PairingProtocol implements PairingProtocolInterface {
136
171
  const request = this.createHandshakeRequest();
137
172
  await this.networkClient.sendPacket(request);
138
173
  await this.networkClient.receiveResponse();
139
- PairingProtocol.log.debug('Handshake completed');
174
+ log.debug('Handshake completed');
140
175
  }
141
176
 
142
177
  /**
@@ -150,7 +185,7 @@ export class PairingProtocol implements PairingProtocolInterface {
150
185
 
151
186
  const failedRequest = this.createPairVerifyFailedRequest();
152
187
  await this.networkClient.sendPacket(failedRequest);
153
- PairingProtocol.log.debug('Pair verification attempt completed');
188
+ log.debug('Pair verification attempt completed');
154
189
  }
155
190
 
156
191
  /**
@@ -162,7 +197,7 @@ export class PairingProtocol implements PairingProtocolInterface {
162
197
  const request = this.createSetupManualPairingRequest();
163
198
  await this.networkClient.sendPacket(request);
164
199
  const response = await this.networkClient.receiveResponse();
165
- PairingProtocol.log.debug('Manual pairing setup completed');
200
+ log.debug('Manual pairing setup completed');
166
201
  return response;
167
202
  }
168
203
 
@@ -201,7 +236,7 @@ export class PairingProtocol implements PairingProtocolInterface {
201
236
  const response = await this.networkClient.receiveResponse();
202
237
  this.validateSRPProofResponse(response);
203
238
 
204
- PairingProtocol.log.debug('SRP authentication completed');
239
+ log.debug('SRP authentication completed');
205
240
  return srpClient;
206
241
  }
207
242
 
@@ -212,12 +247,12 @@ export class PairingProtocol implements PairingProtocolInterface {
212
247
  */
213
248
  private async receiveM6Completion(decryptKey: Buffer): Promise<void> {
214
249
  const m6Response = await this.networkClient.receiveResponse();
215
- PairingProtocol.log.info('M6 Response received');
250
+ log.info('M6 Response received');
216
251
 
217
252
  try {
218
253
  this.processM6Response(m6Response, decryptKey);
219
254
  } catch (error) {
220
- PairingProtocol.log.warn(
255
+ log.warn(
221
256
  'M6 decryption failed - but pairing may still be successful:',
222
257
  (error as Error).message,
223
258
  );
@@ -568,16 +603,14 @@ export class PairingProtocol implements PairingProtocolInterface {
568
603
  const m6TLVBuffer = Buffer.from(m6DataBase64, 'base64');
569
604
  const m6Parsed = decodeTLV8ToDict(m6TLVBuffer);
570
605
 
571
- PairingProtocol.log.debug(
606
+ log.debug(
572
607
  'M6 TLV types received:',
573
608
  Object.keys(m6Parsed).map((k) => `0x${Number(k).toString(16)}`),
574
609
  );
575
610
 
576
611
  const stateData = m6Parsed[PairingDataComponentType.STATE];
577
612
  if (stateData && stateData[0] === PAIRING_STATES.M6) {
578
- PairingProtocol.log.info(
579
- '✅ Pairing completed successfully (STATE=0x06)',
580
- );
613
+ log.info('✅ Pairing completed successfully (STATE=0x06)');
581
614
  }
582
615
 
583
616
  const encryptedData = m6Parsed[PairingDataComponentType.ENCRYPTED_DATA];
@@ -589,10 +622,7 @@ export class PairingProtocol implements PairingProtocolInterface {
589
622
  nonce,
590
623
  });
591
624
  const decryptedTLV = decodeTLV8ToDict(decrypted);
592
- PairingProtocol.log.debug(
593
- 'M6 decrypted content types:',
594
- Object.keys(decryptedTLV),
595
- );
625
+ log.debug('M6 decrypted content types:', Object.keys(decryptedTLV));
596
626
  }
597
627
  }
598
628
 
@@ -610,7 +640,7 @@ export class PairingProtocol implements PairingProtocolInterface {
610
640
  length: 32,
611
641
  });
612
642
 
613
- PairingProtocol.log.debug('Derived encryption keys');
643
+ log.debug('Derived encryption keys');
614
644
  return {
615
645
  encryptKey: sharedKey,
616
646
  decryptKey: sharedKey,
@@ -1,2 +1,2 @@
1
1
  export { PairingStorage } from './pairing-storage.js';
2
- export type { PairingStorageInterface } from './types.js';
2
+ export type { PairingStorageInterface, PairRecord } from './types.js';
@@ -1,16 +1,20 @@
1
1
  import { strongbox } from '@appium/strongbox';
2
- import { logger } from '@appium/support';
2
+ import { readdir } from 'node:fs/promises';
3
+ import { dirname } from 'node:path';
3
4
 
4
5
  import { STRONGBOX_CONTAINER_NAME } from '../../../constants.js';
5
- import { createXmlPlist } from '../../plist/index.js';
6
+ import { getLogger } from '../../logger.js';
7
+ import { createXmlPlist, parseXmlPlist } from '../../plist/index.js';
8
+ import { APPLETV_PAIRING_PREFIX } from '../constants.js';
6
9
  import { PairingError } from '../errors.js';
7
10
  import type { PairingConfig } from '../types.js';
8
- import type { PairingStorageInterface } from './types.js';
11
+ import type { PairRecord, PairingStorageInterface } from './types.js';
9
12
 
10
13
  /** Manages persistent storage of pairing credentials as plist files */
11
14
  export class PairingStorage implements PairingStorageInterface {
12
- private readonly log = logger.getLogger('PairingStorage');
15
+ private readonly log = getLogger('PairingStorage');
13
16
  private readonly box;
17
+ private strongboxDir?: string;
14
18
 
15
19
  constructor(private readonly config: PairingConfig) {
16
20
  this.box = strongbox(STRONGBOX_CONTAINER_NAME);
@@ -23,7 +27,7 @@ export class PairingStorage implements PairingStorageInterface {
23
27
  remoteUnlockHostKey = '',
24
28
  ): Promise<string> {
25
29
  try {
26
- const itemName = `appletv_pairing_${deviceId}`;
30
+ const itemName = `${APPLETV_PAIRING_PREFIX}${deviceId}`;
27
31
  const plistContent = this.createPlistContent(
28
32
  ltpk,
29
33
  ltsk,
@@ -46,6 +50,70 @@ export class PairingStorage implements PairingStorageInterface {
46
50
  }
47
51
  }
48
52
 
53
+ async load(deviceId: string): Promise<PairRecord | null> {
54
+ const itemName = `${APPLETV_PAIRING_PREFIX}${deviceId}`;
55
+
56
+ try {
57
+ const item =
58
+ this.box.getItem(itemName) ?? (await this.box.createItem(itemName));
59
+
60
+ const pairingData = await item.read();
61
+
62
+ if (!pairingData) {
63
+ this.log.debug(`No pair record found for device ${deviceId}`);
64
+ return null;
65
+ }
66
+
67
+ const parsed = parseXmlPlist(pairingData);
68
+
69
+ if (!parsed.private_key || !parsed.public_key) {
70
+ throw new Error('Could not parse pairing record keys');
71
+ }
72
+
73
+ const privateKey = Buffer.isBuffer(parsed.private_key)
74
+ ? parsed.private_key
75
+ : Buffer.from(parsed.private_key as string, 'base64');
76
+ const publicKey = Buffer.isBuffer(parsed.public_key)
77
+ ? parsed.public_key
78
+ : Buffer.from(parsed.public_key as string, 'base64');
79
+
80
+ this.log.debug(`Loaded pair record for ${deviceId}`);
81
+
82
+ return {
83
+ privateKey,
84
+ publicKey,
85
+ remoteUnlockHostKey: (parsed.remote_unlock_host_key as string) || '',
86
+ };
87
+ } catch (error) {
88
+ this.log.error(`Failed to load pair record for ${deviceId}:`, error);
89
+ return null;
90
+ }
91
+ }
92
+
93
+ async getAvailableDeviceIds(): Promise<string[]> {
94
+ try {
95
+ if (!this.strongboxDir) {
96
+ const dummyItem = await this.box.createItem('_temp');
97
+ this.strongboxDir = dirname(dummyItem.id);
98
+ // Clean up the temporary item after extracting the directory path
99
+ await dummyItem.clear();
100
+ }
101
+
102
+ const files = await readdir(this.strongboxDir);
103
+ const deviceIds = files
104
+ .filter((file: string) => file.startsWith(APPLETV_PAIRING_PREFIX))
105
+ .map((file: string) => file.replace(APPLETV_PAIRING_PREFIX, ''));
106
+
107
+ this.log.debug(
108
+ `Found ${deviceIds.length} pair record(s): ${deviceIds.join(', ')}`,
109
+ );
110
+ return deviceIds;
111
+ } catch (error) {
112
+ this.log.debug('Error getting available device IDs:', error);
113
+ return [];
114
+ }
115
+ }
116
+
49
117
  private createPlistContent(
50
118
  publicKey: Buffer,
51
119
  privateKey: Buffer,
@@ -1,4 +1,9 @@
1
- /** Interface for storing pairing credentials to disk */
1
+ export interface PairRecord {
2
+ publicKey: Buffer;
3
+ privateKey: Buffer;
4
+ remoteUnlockHostKey: string;
5
+ }
6
+
2
7
  export interface PairingStorageInterface {
3
8
  save(
4
9
  deviceId: string,
@@ -6,4 +11,6 @@ export interface PairingStorageInterface {
6
11
  ltsk: Buffer,
7
12
  remoteUnlockHostKey?: string,
8
13
  ): Promise<string>;
14
+ load(deviceId: string): Promise<PairRecord | null>;
15
+ getAvailableDeviceIds(): Promise<string[]>;
9
16
  }
@@ -0,0 +1,2 @@
1
+ export { TunnelService, AppleTVTunnelService } from './tunnel-service.js';
2
+ export type { TcpListenerInfo } from './types.js';