appium-ios-remotexpc 0.0.5 → 0.1.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 (35) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/build/src/index.d.ts +1 -1
  3. package/build/src/index.d.ts.map +1 -1
  4. package/build/src/lib/apple-tv/srp/crypto-utils.d.ts +53 -0
  5. package/build/src/lib/apple-tv/srp/crypto-utils.d.ts.map +1 -0
  6. package/build/src/lib/apple-tv/srp/crypto-utils.js +128 -0
  7. package/build/src/lib/apple-tv/srp/index.d.ts +3 -0
  8. package/build/src/lib/apple-tv/srp/index.d.ts.map +1 -0
  9. package/build/src/lib/apple-tv/srp/index.js +2 -0
  10. package/build/src/lib/apple-tv/srp/srp-client.d.ts +130 -0
  11. package/build/src/lib/apple-tv/srp/srp-client.d.ts.map +1 -0
  12. package/build/src/lib/apple-tv/srp/srp-client.js +288 -0
  13. package/build/src/lib/remote-xpc/remote-xpc-connection.d.ts +1 -2
  14. package/build/src/lib/remote-xpc/remote-xpc-connection.d.ts.map +1 -1
  15. package/build/src/lib/remote-xpc/remote-xpc-connection.js +1 -2
  16. package/build/src/lib/tunnel/index.d.ts +3 -2
  17. package/build/src/lib/tunnel/index.d.ts.map +1 -1
  18. package/build/src/lib/tunnel/index.js +6 -3
  19. package/build/src/lib/types.d.ts +11 -0
  20. package/build/src/lib/types.d.ts.map +1 -1
  21. package/build/src/services/ios/diagnostic-service/index.d.ts.map +1 -1
  22. package/build/src/services/ios/diagnostic-service/index.js +0 -1
  23. package/build/src/services.d.ts +3 -3
  24. package/build/src/services.d.ts.map +1 -1
  25. package/build/src/services.js +8 -5
  26. package/package.json +1 -1
  27. package/src/index.ts +1 -0
  28. package/src/lib/apple-tv/srp/crypto-utils.ts +166 -0
  29. package/src/lib/apple-tv/srp/index.ts +8 -0
  30. package/src/lib/apple-tv/srp/srp-client.ts +387 -0
  31. package/src/lib/remote-xpc/remote-xpc-connection.ts +1 -2
  32. package/src/lib/tunnel/index.ts +7 -5
  33. package/src/lib/types.ts +12 -0
  34. package/src/services/ios/diagnostic-service/index.ts +0 -1
  35. package/src/services.ts +10 -7
@@ -0,0 +1,387 @@
1
+ import { logger } from '@appium/support';
2
+ import { randomBytes } from 'node:crypto';
3
+
4
+ import {
5
+ SRP_GENERATOR,
6
+ SRP_KEY_LENGTH_BYTES,
7
+ SRP_PRIME_3072,
8
+ SRP_PRIVATE_KEY_BITS,
9
+ SRP_USERNAME,
10
+ } from '../constants.js';
11
+ import { SRPError } from '../errors.js';
12
+ import {
13
+ bigIntToBuffer,
14
+ bufferToBigInt,
15
+ modPow,
16
+ } from '../utils/buffer-utils.js';
17
+ import {
18
+ calculateK,
19
+ calculateM1,
20
+ calculateU,
21
+ calculateX,
22
+ hash,
23
+ } from './crypto-utils.js';
24
+
25
+ const log = logger.getLogger('SRPClient');
26
+
27
+ /**
28
+ * SRP (Secure Remote Password) client implementation following RFC 5054.
29
+ *
30
+ * This class handles the client-side operations of the SRP protocol,
31
+ * including key generation, authentication proof computation, and
32
+ * session key derivation.
33
+ */
34
+ export class SRPClient {
35
+ // Constants
36
+ private static readonly ZERO = BigInt(0);
37
+ private static readonly ONE = BigInt(1);
38
+ private static readonly MAX_KEY_GENERATION_ATTEMPTS = 100;
39
+
40
+ private readonly N = SRP_PRIME_3072;
41
+ private readonly g = SRP_GENERATOR;
42
+ private readonly k: bigint;
43
+ private readonly N_MINUS_ONE: bigint;
44
+
45
+ private username: string;
46
+ private password: string;
47
+ private _salt: Buffer | null = null;
48
+ private _a: bigint = SRPClient.ZERO;
49
+ private _A: bigint = SRPClient.ZERO;
50
+ private _B: bigint | null = null;
51
+ private _S: bigint | null = null;
52
+ private _K: Buffer | null = null;
53
+
54
+ // State tracking
55
+ private keysGenerated = false;
56
+ private disposed = false;
57
+
58
+ constructor() {
59
+ this.k = calculateK(this.N, this.g, SRP_KEY_LENGTH_BYTES);
60
+ this.N_MINUS_ONE = this.N - SRPClient.ONE;
61
+ this.username = SRP_USERNAME;
62
+ this.password = '';
63
+
64
+ log.debug('Initialized SRP client with k value');
65
+ }
66
+
67
+ /**
68
+ * Sets the user identity credentials.
69
+ * Note: Username is set to SRP_USERNAME constant, but can be overridden.
70
+ *
71
+ * @param username - The username for authentication
72
+ * @param password - The password for authentication
73
+ * @throws {SRPError} If username or password is empty
74
+ */
75
+ public setIdentity(username: string, password: string): void {
76
+ this.throwIfDisposed();
77
+
78
+ if (!username?.trim()) {
79
+ throw new SRPError('Username cannot be empty');
80
+ }
81
+ if (!password) {
82
+ throw new SRPError('Password cannot be empty');
83
+ }
84
+
85
+ this.username = username.trim();
86
+ this.password = password;
87
+
88
+ log.debug('Identity set successfully');
89
+ }
90
+
91
+ /**
92
+ * Gets the salt value received from the server.
93
+ *
94
+ * @returns The salt buffer or null if not set
95
+ */
96
+ get salt(): Buffer | null {
97
+ return this._salt;
98
+ }
99
+
100
+ /**
101
+ * Sets the salt value received from the server.
102
+ *
103
+ * @param value - The salt buffer from the server
104
+ * @throws {SRPError} If salt is empty or client is disposed
105
+ */
106
+ set salt(value: Buffer) {
107
+ this.throwIfDisposed();
108
+
109
+ if (!value || value.length === 0) {
110
+ throw new SRPError('Salt cannot be empty');
111
+ }
112
+
113
+ this._salt = value;
114
+ this.generateClientKeysIfReady();
115
+
116
+ log.debug('Salt set successfully');
117
+ }
118
+
119
+ /**
120
+ * Gets the server's public key B.
121
+ *
122
+ * @returns The server's public key as a Buffer or null if not set
123
+ */
124
+ get serverPublicKey(): Buffer | null {
125
+ return this._B ? bigIntToBuffer(this._B, SRP_KEY_LENGTH_BYTES) : null;
126
+ }
127
+
128
+ /**
129
+ * Sets the server's public key B.
130
+ *
131
+ * @param value - The server's public key as a Buffer
132
+ * @throws {SRPError} If the server public key is invalid or client is disposed
133
+ */
134
+ set serverPublicKey(value: Buffer) {
135
+ this.throwIfDisposed();
136
+
137
+ if (!value || value.length !== SRP_KEY_LENGTH_BYTES) {
138
+ throw new SRPError(
139
+ `Server public key must be ${SRP_KEY_LENGTH_BYTES} bytes, got ${value?.length || 0}`,
140
+ );
141
+ }
142
+
143
+ this._B = bufferToBigInt(value);
144
+
145
+ if (this._B <= SRPClient.ONE || this._B >= this.N_MINUS_ONE) {
146
+ throw new SRPError(
147
+ 'Invalid server public key B: must be in range (1, N-1)',
148
+ );
149
+ }
150
+
151
+ // Additional security check
152
+ if (this._B % this.N === SRPClient.ZERO) {
153
+ throw new SRPError('Invalid server public key B: divisible by N');
154
+ }
155
+
156
+ this.generateClientKeysIfReady();
157
+ log.debug('Server public key set successfully');
158
+ }
159
+
160
+ /**
161
+ * Gets the client's public key A.
162
+ *
163
+ * @returns The client's public key as a Buffer
164
+ * @throws {SRPError} If keys are not generated yet or client is disposed
165
+ */
166
+ get publicKey(): Buffer {
167
+ this.throwIfDisposed();
168
+
169
+ if (this._A === SRPClient.ZERO) {
170
+ throw new SRPError(
171
+ 'Client keys not generated yet. Set salt and serverPublicKey properties first.',
172
+ );
173
+ }
174
+
175
+ return bigIntToBuffer(this._A, SRP_KEY_LENGTH_BYTES);
176
+ }
177
+
178
+ /**
179
+ * Computes the authentication proof M1.
180
+ *
181
+ * @returns The authentication proof as a Buffer
182
+ * @throws {SRPError} If required parameters are not set or client is disposed
183
+ */
184
+ public computeProof(): Buffer {
185
+ this.throwIfDisposed();
186
+ this.validateIdentitySet();
187
+
188
+ if (!this._K) {
189
+ this.computeSharedSecret();
190
+ }
191
+
192
+ if (!this._salt || !this._K || !this._B) {
193
+ throw new SRPError(
194
+ 'Cannot compute proof: salt, session key, and server public key must be set',
195
+ );
196
+ }
197
+
198
+ return calculateM1(
199
+ this.N,
200
+ this.g,
201
+ this.username,
202
+ this._salt,
203
+ this._A,
204
+ this._B,
205
+ this._K,
206
+ );
207
+ }
208
+
209
+ /**
210
+ * Gets the computed session key K.
211
+ *
212
+ * @returns The session key as a Buffer
213
+ * @throws {SRPError} If session key is not computed or client is disposed
214
+ */
215
+ get sessionKey(): Buffer {
216
+ this.throwIfDisposed();
217
+ this.validateIdentitySet();
218
+
219
+ if (!this._K) {
220
+ this.computeSharedSecret();
221
+ }
222
+
223
+ if (!this._K) {
224
+ throw new SRPError('Session key not computed');
225
+ }
226
+
227
+ return this._K;
228
+ }
229
+
230
+ /**
231
+ * Checks if the client is ready to perform operations.
232
+ *
233
+ * @returns True if salt and server public key are set
234
+ */
235
+ public isReady(): boolean {
236
+ return !this.disposed && !!(this._salt && this._B && this.keysGenerated);
237
+ }
238
+
239
+ /**
240
+ * Checks if session key has been computed.
241
+ *
242
+ * @returns True if a session key is available
243
+ */
244
+ public hasSessionKey(): boolean {
245
+ return !this.disposed && !!this._K;
246
+ }
247
+
248
+ /**
249
+ * Clears sensitive data and disposes the client.
250
+ * After calling this method, the client instance should not be used.
251
+ */
252
+ public dispose(): void {
253
+ if (this.disposed) {
254
+ return;
255
+ }
256
+
257
+ // Clear sensitive data
258
+ this.password = '';
259
+ this._a = SRPClient.ZERO;
260
+
261
+ if (this._K) {
262
+ this._K.fill(0);
263
+ }
264
+
265
+ this._salt = null;
266
+ this._S = null;
267
+ this._B = null;
268
+ this.disposed = true;
269
+
270
+ log.debug('SRP client disposed and sensitive data cleared');
271
+ }
272
+
273
+ /**
274
+ * Generates client keys if both salt and server public key are available.
275
+ * This method ensures keys are generated only once.
276
+ */
277
+ private generateClientKeysIfReady(): void {
278
+ if (this._salt && this._B && !this.keysGenerated) {
279
+ this.generateClientKeys();
280
+ this.keysGenerated = true;
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Generates the client's private and public keys using cryptographically secure methods.
286
+ *
287
+ * @throws {SRPError} If generated public key is invalid or key generation fails
288
+ */
289
+ private generateClientKeys(): void {
290
+ this.validateIdentitySet();
291
+
292
+ let attempts = 0;
293
+
294
+ while (attempts < SRPClient.MAX_KEY_GENERATION_ATTEMPTS) {
295
+ const randomBits = randomBytes(SRP_PRIVATE_KEY_BITS / 8);
296
+ this._a = bufferToBigInt(randomBits);
297
+
298
+ // Ensure key is in valid range without introducing bias
299
+ if (this._a >= this.N) {
300
+ attempts++;
301
+ continue;
302
+ }
303
+
304
+ if (this._a === SRPClient.ZERO) {
305
+ attempts++;
306
+ continue;
307
+ }
308
+
309
+ this._A = modPow(this.g, this._a, this.N);
310
+
311
+ if (this._A <= SRPClient.ONE || this._A >= this.N_MINUS_ONE) {
312
+ attempts++;
313
+ continue;
314
+ }
315
+
316
+ // Successfully generated valid keys
317
+ log.debug('Generated client keys successfully');
318
+ return;
319
+ }
320
+
321
+ throw new SRPError(
322
+ `Failed to generate secure client keys after ${SRPClient.MAX_KEY_GENERATION_ATTEMPTS} attempts`,
323
+ );
324
+ }
325
+
326
+ /**
327
+ * Computes the shared secret S and derives the session key K.
328
+ *
329
+ * @throws {SRPError} If required parameters are not set
330
+ */
331
+ private computeSharedSecret(): void {
332
+ this.validateIdentitySet();
333
+
334
+ if (!this._salt || !this._B) {
335
+ throw new SRPError('Salt and server public key must be set first');
336
+ }
337
+
338
+ if (this._A === SRPClient.ZERO) {
339
+ throw new SRPError('Client keys not generated');
340
+ }
341
+
342
+ const u = calculateU(this._A, this._B, SRP_KEY_LENGTH_BYTES);
343
+ log.debug('Calculated u value');
344
+
345
+ const x = calculateX(this._salt, this.username, this.password);
346
+ log.debug('Calculated x value');
347
+
348
+ const gx = modPow(this.g, x, this.N);
349
+ const kgx = (this.k * gx) % this.N;
350
+
351
+ // Fix negative modulo operation
352
+ let base = this._B - kgx;
353
+ base = ((base % this.N) + this.N) % this.N;
354
+
355
+ const exponent = this._a + u * x;
356
+ this._S = modPow(base, exponent, this.N);
357
+ log.debug('Calculated shared secret S');
358
+
359
+ const SBuffer = bigIntToBuffer(this._S, SRP_KEY_LENGTH_BYTES);
360
+ this._K = hash(SBuffer);
361
+ log.debug('Calculated session key K');
362
+ }
363
+
364
+ /**
365
+ * Validates that identity has been set.
366
+ *
367
+ * @throws {SRPError} If password is not set (username is set by default)
368
+ */
369
+ private validateIdentitySet(): void {
370
+ if (!this.password) {
371
+ throw new SRPError(
372
+ 'Password must be set before performing operations. Call setIdentity() first.',
373
+ );
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Throws an error if the client has been disposed.
379
+ *
380
+ * @throws {SRPError} If client is disposed
381
+ */
382
+ private throwIfDisposed(): void {
383
+ if (this.disposed) {
384
+ throw new SRPError('SRP client has been disposed');
385
+ }
386
+ }
387
+ }
@@ -457,5 +457,4 @@ function extractServices(response: string): ServicesResponse {
457
457
  return { services };
458
458
  }
459
459
 
460
- export default RemoteXpcConnection;
461
- export { type Service, type ServicesResponse };
460
+ export { RemoteXpcConnection, type Service, type ServicesResponse };
@@ -2,7 +2,7 @@ import { logger } from '@appium/support';
2
2
  import type { TLSSocket } from 'tls';
3
3
  import { type TunnelConnection, connectToTunnelLockdown } from 'tuntap-bridge';
4
4
 
5
- import RemoteXpcConnection from '../remote-xpc/remote-xpc-connection.js';
5
+ import { RemoteXpcConnection } from '../remote-xpc/remote-xpc-connection.js';
6
6
 
7
7
  const log = logger.getLogger('TunnelManager');
8
8
 
@@ -64,9 +64,12 @@ class TunnelManagerService {
64
64
  async createRemoteXPCConnection(
65
65
  address: string,
66
66
  rsdPort: number,
67
- ): Promise<any> {
67
+ ): Promise<RemoteXpcConnection> {
68
68
  try {
69
- const remoteXPC = new RemoteXpcConnection([address, rsdPort]);
69
+ const remoteXPC: RemoteXpcConnection = new RemoteXpcConnection([
70
+ address,
71
+ rsdPort,
72
+ ]);
70
73
 
71
74
  // Connect to RemoteXPC with delay between retries
72
75
  let retries = 3;
@@ -75,7 +78,6 @@ class TunnelManagerService {
75
78
  while (retries > 0) {
76
79
  try {
77
80
  await remoteXPC.connect();
78
-
79
81
  // Update the registry entry with the RemoteXPC connection
80
82
  const entry = this.tunnelRegistry.get(address);
81
83
  if (entry) {
@@ -249,5 +251,5 @@ class TunnelManagerService {
249
251
  // Create and export the singleton instance
250
252
  export const TunnelManager = new TunnelManagerService();
251
253
  // Export packet streaming IPC functionality
252
- export { PacketStreamServer } from './packet-stream-server.js';
253
254
  export { PacketStreamClient } from './packet-stream-client.js';
255
+ export { PacketStreamServer } from './packet-stream-server.js';
package/src/lib/types.ts CHANGED
@@ -5,6 +5,7 @@ import { EventEmitter } from 'events';
5
5
  import type { PacketData } from 'tuntap-bridge';
6
6
 
7
7
  import type { BaseService, Service } from '../services/ios/base-service.js';
8
+ import type { RemoteXpcConnection } from './remote-xpc/remote-xpc-connection.js';
8
9
  import type { Device } from './usbmux/index.js';
9
10
 
10
11
  /**
@@ -190,6 +191,17 @@ export interface DiagnosticsServiceConstructor {
190
191
  new (address: [string, number]): DiagnosticsService;
191
192
  }
192
193
 
194
+ /**
195
+ * Represents a DiagnosticsService instance with its associated RemoteXPC connection
196
+ * This allows callers to properly manage the connection lifecycle
197
+ */
198
+ export interface DiagnosticsServiceWithConnection {
199
+ /** The DiagnosticsService instance */
200
+ diagnosticsService: DiagnosticsService;
201
+ /** The RemoteXPC connection that can be used to close the connection */
202
+ remoteXPC: RemoteXpcConnection;
203
+ }
204
+
193
205
  /**
194
206
  * Options for configuring syslog capture
195
207
  */
@@ -223,7 +223,6 @@ class DiagnosticsService
223
223
  emptyRequest,
224
224
  timeout,
225
225
  );
226
- log.debug('Additional response: ', additionalResponse);
227
226
  const hasDiagnostics =
228
227
  'Diagnostics' in additionalResponse &&
229
228
  typeof additionalResponse.Diagnostics === 'object' &&
package/src/services.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { strongbox } from '@appium/strongbox';
2
2
 
3
- import RemoteXpcConnection from './lib/remote-xpc/remote-xpc-connection.js';
3
+ import { RemoteXpcConnection } from './lib/remote-xpc/remote-xpc-connection.js';
4
4
  import { TunnelManager } from './lib/tunnel/index.js';
5
5
  import { TunnelApiClient } from './lib/tunnel/tunnel-api-client.js';
6
6
  import type {
7
- DiagnosticsService as DiagnosticsServiceType,
7
+ DiagnosticsServiceWithConnection,
8
8
  SyslogService as SyslogServiceType,
9
9
  } from './lib/types.js';
10
10
  import DiagnosticsService from './services/ios/diagnostic-service/index.js';
@@ -15,15 +15,18 @@ const TUNNEL_REGISTRY_PORT = 'tunnelRegistryPort';
15
15
 
16
16
  export async function startDiagnosticsService(
17
17
  udid: string,
18
- ): Promise<DiagnosticsServiceType> {
18
+ ): Promise<DiagnosticsServiceWithConnection> {
19
19
  const { remoteXPC, tunnelConnection } = await createRemoteXPCConnection(udid);
20
20
  const diagnosticsService = remoteXPC.findService(
21
21
  DiagnosticsService.RSD_SERVICE_NAME,
22
22
  );
23
- return new DiagnosticsService([
24
- tunnelConnection.host,
25
- parseInt(diagnosticsService.port, 10),
26
- ]);
23
+ return {
24
+ remoteXPC: remoteXPC as RemoteXpcConnection,
25
+ diagnosticsService: new DiagnosticsService([
26
+ tunnelConnection.host,
27
+ parseInt(diagnosticsService.port, 10),
28
+ ]),
29
+ };
27
30
  }
28
31
 
29
32
  export async function startSyslogService(