appium-ios-remotexpc 0.3.3 → 0.4.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.
@@ -0,0 +1,338 @@
1
+ import { logger } from '@appium/support';
2
+ import axios from 'axios';
3
+ import { randomUUID } from 'node:crypto';
4
+
5
+ import { createPlist, parsePlist } from '../plist/index.js';
6
+ import type { PlistDictionary } from '../types.js';
7
+
8
+ const log = logger.getLogger('TSSRequestor');
9
+
10
+ // TSS Constants
11
+ const TSS_CONTROLLER_ACTION_URL = 'http://gs.apple.com/TSS/controller?action=2';
12
+ const TSS_CLIENT_VERSION_STRING = 'libauthinstall-1033.80.3';
13
+ const TSS_SUCCESS_MESSAGE = 'SUCCESS';
14
+ const TSS_REQUEST_TIMEOUT = 10000; // 10 seconds
15
+ const TSS_RULE_IGNORE_VALUE = 255;
16
+
17
+ export class TSSError extends Error {
18
+ constructor(message: string) {
19
+ super(message);
20
+ this.name = 'TSSError';
21
+ }
22
+ }
23
+
24
+ export class BuildIdentityNotFoundError extends TSSError {
25
+ constructor(message: string) {
26
+ super(message);
27
+ this.name = 'BuildIdentityNotFoundError';
28
+ }
29
+ }
30
+
31
+ export interface TSSResponse {
32
+ [key: string]: any;
33
+ ApImg4Ticket?: Buffer;
34
+ }
35
+
36
+ export interface RestoreRequestRule {
37
+ Conditions?: {
38
+ ApRawProductionMode?: boolean;
39
+ ApCurrentProductionMode?: boolean;
40
+ ApRawSecurityMode?: boolean;
41
+ ApRequiresImage4?: boolean;
42
+ ApDemotionPolicyOverride?: string;
43
+ ApInRomDFU?: boolean;
44
+ [key: string]: any;
45
+ };
46
+ Actions?: {
47
+ [key: string]: any;
48
+ };
49
+ }
50
+
51
+ export interface ManifestEntry {
52
+ Info?: {
53
+ RestoreRequestRules?: RestoreRequestRule[];
54
+ [key: string]: any;
55
+ };
56
+ Digest?: Buffer;
57
+ Trusted?: boolean;
58
+ [key: string]: any;
59
+ }
60
+
61
+ export interface BuildManifest {
62
+ LoadableTrustCache?: ManifestEntry;
63
+ PersonalizedDMG?: ManifestEntry;
64
+ [key: string]: ManifestEntry | undefined;
65
+ }
66
+
67
+ export class TSSRequest {
68
+ private _request: PlistDictionary;
69
+
70
+ constructor() {
71
+ this._request = {
72
+ '@HostPlatformInfo': 'mac',
73
+ '@VersionInfo': TSS_CLIENT_VERSION_STRING,
74
+ '@UUID': randomUUID().toUpperCase(),
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Apply restore request rules to TSS entry
80
+ * @param tssEntry The TSS entry to modify
81
+ * @param parameters The parameters for rule evaluation
82
+ * @param rules The rules to apply
83
+ * @returns Modified TSS entry
84
+ */
85
+ static applyRestoreRequestRules(
86
+ tssEntry: PlistDictionary,
87
+ parameters: PlistDictionary,
88
+ rules: RestoreRequestRule[],
89
+ ): PlistDictionary {
90
+ for (const rule of rules) {
91
+ let conditionsFulfilled = true;
92
+ const conditions = rule.Conditions || {};
93
+
94
+ for (const [key, value] of Object.entries(conditions)) {
95
+ if (!conditionsFulfilled) {
96
+ break;
97
+ }
98
+
99
+ let value2: any;
100
+ switch (key) {
101
+ case 'ApRawProductionMode':
102
+ case 'ApCurrentProductionMode':
103
+ value2 = parameters.ApProductionMode;
104
+ break;
105
+ case 'ApRawSecurityMode':
106
+ value2 = parameters.ApSecurityMode;
107
+ break;
108
+ case 'ApRequiresImage4':
109
+ value2 = parameters.ApSupportsImg4;
110
+ break;
111
+ case 'ApDemotionPolicyOverride':
112
+ value2 = parameters.DemotionPolicy;
113
+ break;
114
+ case 'ApInRomDFU':
115
+ value2 = parameters.ApInRomDFU;
116
+ break;
117
+ default:
118
+ log.error(
119
+ `Unhandled condition ${key} while parsing RestoreRequestRules`,
120
+ );
121
+ value2 = null;
122
+ }
123
+
124
+ if (value2 !== null && value2 !== undefined) {
125
+ conditionsFulfilled = value === value2;
126
+ } else {
127
+ conditionsFulfilled = false;
128
+ }
129
+ }
130
+
131
+ if (!conditionsFulfilled) {
132
+ continue;
133
+ }
134
+
135
+ const actions = rule.Actions || {};
136
+ for (const [key, value] of Object.entries(actions)) {
137
+ if (value !== TSS_RULE_IGNORE_VALUE) {
138
+ const value2 = tssEntry[key];
139
+ if (value2) {
140
+ delete tssEntry[key];
141
+ }
142
+ log.debug(`Adding ${key}=${value} to TSS entry`);
143
+ tssEntry[key] = value as any;
144
+ }
145
+ }
146
+ }
147
+ return tssEntry;
148
+ }
149
+
150
+ /**
151
+ * Update the TSS request with additional options
152
+ * @param options The options to add to the request
153
+ */
154
+ update(options: PlistDictionary): void {
155
+ Object.assign(this._request, options);
156
+ }
157
+
158
+ /**
159
+ * Send the TSS request to Apple's servers and receive the response
160
+ * @returns Promise resolving to TSS response
161
+ */
162
+ async sendReceive(): Promise<TSSResponse> {
163
+ const headers = {
164
+ 'Cache-Control': 'no-cache',
165
+ 'Content-Type': 'text/xml; charset="utf-8"',
166
+ 'User-Agent': 'InetURL/1.0',
167
+ Expect: '',
168
+ };
169
+
170
+ log.info('Sending TSS request...');
171
+ log.debug('TSS Request:', this._request);
172
+
173
+ try {
174
+ const requestDataStr = createPlist(this._request);
175
+ const requestData =
176
+ typeof requestDataStr === 'string'
177
+ ? Buffer.from(requestDataStr, 'utf8')
178
+ : requestDataStr;
179
+
180
+ const res = await axios.post(TSS_CONTROLLER_ACTION_URL, requestData, {
181
+ headers,
182
+ timeout: TSS_REQUEST_TIMEOUT,
183
+ responseType: 'text',
184
+ });
185
+
186
+ const response = res.data;
187
+ log.debug(`TSS response status: ${res.status}`);
188
+
189
+ if (response.includes('MESSAGE=SUCCESS')) {
190
+ log.debug('TSS response successfully received');
191
+ } else {
192
+ log.warn('TSS response does not contain MESSAGE=SUCCESS');
193
+ }
194
+
195
+ const [, messagePart] = response.split('MESSAGE=');
196
+ if (!messagePart) {
197
+ throw new TSSError('Invalid TSS response format');
198
+ }
199
+
200
+ const [message] = messagePart.split('&');
201
+ log.debug(`TSS server message: ${message}`);
202
+
203
+ if (message !== TSS_SUCCESS_MESSAGE) {
204
+ throw new TSSError(`TSS server replied: ${message}`);
205
+ }
206
+
207
+ const [, requestStringPart] = response.split('REQUEST_STRING=');
208
+ if (!requestStringPart) {
209
+ throw new TSSError('No REQUEST_STRING in TSS response');
210
+ }
211
+
212
+ return parsePlist(requestStringPart) as TSSResponse;
213
+ } catch (error) {
214
+ log.error('TSS request failed:', error);
215
+ throw error;
216
+ }
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Get manifest from Apple's TSS (Ticket Signing Server)
222
+ * @param ecid The device ECID
223
+ * @param buildManifest The build manifest dictionary
224
+ * @param queryPersonalizationIdentifiers Function to query personalization identifiers
225
+ * @param queryNonce Function to query nonce
226
+ * @returns Promise resolving to the manifest bytes
227
+ */
228
+ export async function getManifestFromTSS(
229
+ ecid: number,
230
+ buildManifest: PlistDictionary,
231
+ queryPersonalizationIdentifiers: () => Promise<PlistDictionary>,
232
+ queryNonce: (personalizedImageType: string) => Promise<Buffer>,
233
+ ): Promise<Buffer> {
234
+ log.debug('Starting TSS manifest generation process');
235
+
236
+ const request = new TSSRequest();
237
+
238
+ const personalizationIdentifiers = await queryPersonalizationIdentifiers();
239
+ for (const [key, value] of Object.entries(personalizationIdentifiers)) {
240
+ if (key.startsWith('Ap,')) {
241
+ request.update({ [key]: value });
242
+ }
243
+ }
244
+
245
+ const boardId = personalizationIdentifiers.BoardId as number;
246
+ const chipId = personalizationIdentifiers.ChipID as number;
247
+
248
+ let buildIdentity: any = null;
249
+ const buildIdentities = buildManifest.BuildIdentities as any[];
250
+
251
+ for (const tmpBuildIdentity of buildIdentities) {
252
+ // ApBoardID and ApChipID are hex strings, so parse with radix 16
253
+ const apBoardId = parseInt(tmpBuildIdentity.ApBoardID, 16);
254
+ const apChipId = parseInt(tmpBuildIdentity.ApChipID, 16);
255
+
256
+ if (apBoardId === boardId && apChipId === chipId) {
257
+ buildIdentity = tmpBuildIdentity;
258
+ break;
259
+ }
260
+ }
261
+
262
+ if (!buildIdentity) {
263
+ throw new BuildIdentityNotFoundError(
264
+ `Could not find the manifest for board ${boardId} and chip ${chipId}`,
265
+ );
266
+ }
267
+
268
+ const manifest = buildIdentity.Manifest as BuildManifest;
269
+
270
+ const parameters = {
271
+ ApProductionMode: true,
272
+ ApSecurityDomain: 1,
273
+ ApSecurityMode: true,
274
+ ApSupportsImg4: true,
275
+ ApCurrentProductionMode: true,
276
+ ApRequiresImage4: true,
277
+ ApDemotionPolicyOverride: 'Demote',
278
+ ApInRomDFU: true,
279
+ ApRawSecurityMode: true,
280
+ };
281
+
282
+ const apNonce = await queryNonce('DeveloperDiskImage');
283
+
284
+ request.update({
285
+ '@ApImg4Ticket': true,
286
+ '@BBTicket': true,
287
+ ApBoardID: boardId,
288
+ ApChipID: chipId,
289
+ ApECID: ecid,
290
+ ApNonce: apNonce,
291
+ ApProductionMode: true,
292
+ ApSecurityDomain: 1,
293
+ ApSecurityMode: true,
294
+ SepNonce: Buffer.alloc(20, 0), // 20 bytes of zeros
295
+ UID_MODE: false,
296
+ });
297
+
298
+ for (const [key, manifestEntry] of Object.entries(manifest)) {
299
+ if (!manifestEntry?.Info) {
300
+ continue;
301
+ }
302
+
303
+ if (!manifestEntry.Trusted) {
304
+ log.debug(`Skipping ${key} as it is not trusted`);
305
+ continue;
306
+ }
307
+
308
+ log.debug(`Processing manifest entry: ${key}`);
309
+
310
+ const tssEntry: PlistDictionary = {
311
+ Digest: manifestEntry.Digest || Buffer.alloc(0),
312
+ Trusted: manifestEntry.Trusted || false,
313
+ };
314
+
315
+ if (key === 'PersonalizedDMG') {
316
+ tssEntry.Name = 'DeveloperDiskImage';
317
+ }
318
+
319
+ const loadableTrustCache = manifest.LoadableTrustCache;
320
+ if (loadableTrustCache?.Info?.RestoreRequestRules) {
321
+ const rules = loadableTrustCache.Info.RestoreRequestRules;
322
+ if (rules.length > 0) {
323
+ log.debug(`Applying restore request rules for entry ${key}`);
324
+ TSSRequest.applyRestoreRequestRules(tssEntry, parameters, rules);
325
+ }
326
+ }
327
+
328
+ request.update({ [key]: tssEntry });
329
+ }
330
+
331
+ const response = await request.sendReceive();
332
+
333
+ if (!response.ApImg4Ticket) {
334
+ throw new TSSError('TSS response does not contain ApImg4Ticket');
335
+ }
336
+
337
+ return response.ApImg4Ticket;
338
+ }
package/src/lib/types.ts CHANGED
@@ -348,3 +348,101 @@ export interface SyslogServiceConstructor {
348
348
  */
349
349
  new (address: [string, number]): SyslogService;
350
350
  }
351
+
352
+ /**
353
+ * Represents the instance side of MobileImageMounterService
354
+ */
355
+ export interface MobileImageMounterService extends BaseService {
356
+ /**
357
+ * Lookup for mounted images by type
358
+ * @param imageType Type of image, 'Personalized' by default
359
+ * @returns Promise resolving to array of signatures of mounted images
360
+ */
361
+ lookup(imageType?: string): Promise<Buffer[]>;
362
+
363
+ /**
364
+ * Check if personalized image is mounted
365
+ * @returns Promise resolving to boolean indicating if personalized image is mounted
366
+ */
367
+ isPersonalizedImageMounted(): Promise<boolean>;
368
+
369
+ /**
370
+ * Mount personalized image for device (iOS 17+)
371
+ * @param imageFilePath The file path of the image (.dmg)
372
+ * @param buildManifestFilePath The build manifest file path (.plist)
373
+ * @param trustCacheFilePath The trust cache file path (.trustcache)
374
+ */
375
+ mount(
376
+ imageFilePath: string,
377
+ buildManifestFilePath: string,
378
+ trustCacheFilePath: string,
379
+ ): Promise<void>;
380
+
381
+ /**
382
+ * Unmount image from device
383
+ * @param mountPath The mount path to unmount, defaults to '/System/Developer'
384
+ */
385
+ unmountImage(mountPath?: string): Promise<void>;
386
+
387
+ /**
388
+ * Query developer mode status (iOS 16+)
389
+ * @returns Promise resolving to boolean indicating if developer mode is enabled
390
+ */
391
+ queryDeveloperModeStatus(): Promise<boolean>;
392
+
393
+ /**
394
+ * Query personalization nonce (for personalized images)
395
+ * @param personalizedImageType Optional personalized image type
396
+ * @returns Promise resolving to personalization nonce
397
+ */
398
+ queryNonce(personalizedImageType?: string): Promise<Buffer>;
399
+
400
+ /**
401
+ * Query personalization identifiers from the device
402
+ * @returns Promise resolving to personalization identifiers
403
+ */
404
+ queryPersonalizationIdentifiers(): Promise<PlistDictionary>;
405
+
406
+ /**
407
+ * Copy devices list
408
+ * @returns Promise resolving to array of mounted devices
409
+ */
410
+ copyDevices(): Promise<any[]>;
411
+
412
+ /**
413
+ * Query personalization manifest for a specific image
414
+ * @param imageType The image type (e.g., 'DeveloperDiskImage')
415
+ * @param signature The image signature/hash
416
+ * @returns Promise resolving to personalization manifest
417
+ */
418
+ queryPersonalizationManifest(
419
+ imageType: string,
420
+ signature: Buffer,
421
+ ): Promise<Buffer>;
422
+ }
423
+
424
+ /**
425
+ * Represents the static side of MobileImageMounterService
426
+ */
427
+ export interface MobileImageMounterServiceConstructor {
428
+ /**
429
+ * RSD service name for the mobile image mounter service
430
+ */
431
+ readonly RSD_SERVICE_NAME: string;
432
+
433
+ /**
434
+ * Creates a new MobileImageMounterService instance
435
+ * @param address Tuple containing [host, port]
436
+ */
437
+ new (address: [string, number]): MobileImageMounterService;
438
+ }
439
+
440
+ /**
441
+ * Represents a MobileImageMounterService instance with its associated RemoteXPC connection
442
+ */
443
+ export interface MobileImageMounterServiceWithConnection {
444
+ /** The MobileImageMounterService instance */
445
+ mobileImageMounterService: MobileImageMounterService;
446
+ /** The RemoteXPC connection for service management */
447
+ remoteXPC: RemoteXpcConnection;
448
+ }
@@ -3,11 +3,13 @@ import {
3
3
  startTunnelRegistryServer,
4
4
  } from '../lib/tunnel/tunnel-registry-server.js';
5
5
  import * as diagnostics from './ios/diagnostic-service/index.js';
6
+ import * as mobileImageMounter from './ios/mobile-image-mounter/index.js';
6
7
  import * as syslog from './ios/syslog-service/index.js';
7
8
  import * as tunnel from './ios/tunnel-service/index.js';
8
9
 
9
10
  export {
10
11
  diagnostics,
12
+ mobileImageMounter,
11
13
  syslog,
12
14
  tunnel,
13
15
  TunnelRegistryServer,