1c4ef02aeSEvan Bacon/**
2c4ef02aeSEvan Bacon * Copyright (c) 2021 Expo, Inc.
3c4ef02aeSEvan Bacon * Copyright (c) 2018 Drifty Co.
4c4ef02aeSEvan Bacon *
5c4ef02aeSEvan Bacon * This source code is licensed under the MIT license found in the
6c4ef02aeSEvan Bacon * LICENSE file in the root directory of this source tree.
7c4ef02aeSEvan Bacon */
8c4ef02aeSEvan Baconimport Debug from 'debug';
9c4ef02aeSEvan Baconimport { Socket } from 'net';
10c4ef02aeSEvan Baconimport * as tls from 'tls';
11c4ef02aeSEvan Bacon
12c4ef02aeSEvan Baconimport { ResponseError, ServiceClient } from './ServiceClient';
13c4ef02aeSEvan Baconimport { UsbmuxdPairRecord } from './UsbmuxdClient';
14*8a424bebSJames Ideimport { LockdownProtocolClient } from '../protocol/LockdownProtocol';
15c4ef02aeSEvan Bacon
16c4ef02aeSEvan Baconconst debug = Debug('expo:apple-device:client:lockdownd');
17c4ef02aeSEvan Bacon
18c4ef02aeSEvan Baconexport interface DeviceValues {
19c4ef02aeSEvan Bacon  BasebandCertId: number;
20c4ef02aeSEvan Bacon  BasebandKeyHashInformation: {
21c4ef02aeSEvan Bacon    AKeyStatus: number;
22c4ef02aeSEvan Bacon    SKeyHash: Buffer;
23c4ef02aeSEvan Bacon    SKeyStatus: number;
24c4ef02aeSEvan Bacon  };
25c4ef02aeSEvan Bacon  BasebandSerialNumber: Buffer;
26c4ef02aeSEvan Bacon  BasebandVersion: string;
27c4ef02aeSEvan Bacon  BoardId: number;
28c4ef02aeSEvan Bacon  BuildVersion: string;
29c4ef02aeSEvan Bacon  ChipID: number;
30bd710fa0SBartosz Kaszubowski  ConnectionType: 'USB' | 'Network';
31c4ef02aeSEvan Bacon  DeviceClass: string;
32c4ef02aeSEvan Bacon  DeviceColor: string;
33c4ef02aeSEvan Bacon  DeviceName: string;
34c4ef02aeSEvan Bacon  DieID: number;
35c4ef02aeSEvan Bacon  HardwareModel: string;
36c4ef02aeSEvan Bacon  HasSiDP: boolean;
37c4ef02aeSEvan Bacon  PartitionType: string;
38c4ef02aeSEvan Bacon  ProductName: string;
39c4ef02aeSEvan Bacon  ProductType: string;
40c4ef02aeSEvan Bacon  ProductVersion: string;
41c4ef02aeSEvan Bacon  ProductionSOC: boolean;
42c4ef02aeSEvan Bacon  ProtocolVersion: string;
43c4ef02aeSEvan Bacon  TelephonyCapability: boolean;
44c4ef02aeSEvan Bacon  UniqueChipID: number;
45c4ef02aeSEvan Bacon  UniqueDeviceID: string;
46c4ef02aeSEvan Bacon  WiFiAddress: string;
47c4ef02aeSEvan Bacon  [key: string]: any;
48c4ef02aeSEvan Bacon}
49c4ef02aeSEvan Bacon
50c4ef02aeSEvan Baconinterface LockdowndServiceResponse {
51c4ef02aeSEvan Bacon  Request: 'StartService';
52c4ef02aeSEvan Bacon  Service: string;
53c4ef02aeSEvan Bacon  Port: number;
54c4ef02aeSEvan Bacon  EnableServiceSSL?: boolean; // Only on iOS 13+
55c4ef02aeSEvan Bacon}
56c4ef02aeSEvan Bacon
57c4ef02aeSEvan Baconinterface LockdowndSessionResponse {
58c4ef02aeSEvan Bacon  Request: 'StartSession';
59c4ef02aeSEvan Bacon  EnableSessionSSL: boolean;
60c4ef02aeSEvan Bacon}
61c4ef02aeSEvan Bacon
62c4ef02aeSEvan Baconinterface LockdowndAllValuesResponse {
63c4ef02aeSEvan Bacon  Request: 'GetValue';
64c4ef02aeSEvan Bacon  Value: DeviceValues;
65c4ef02aeSEvan Bacon}
66c4ef02aeSEvan Bacon
67c4ef02aeSEvan Baconinterface LockdowndValueResponse {
68c4ef02aeSEvan Bacon  Request: 'GetValue';
69c4ef02aeSEvan Bacon  Key: string;
70c4ef02aeSEvan Bacon  Value: string;
71c4ef02aeSEvan Bacon}
72c4ef02aeSEvan Bacon
73c4ef02aeSEvan Baconinterface LockdowndQueryTypeResponse {
74c4ef02aeSEvan Bacon  Request: 'QueryType';
75c4ef02aeSEvan Bacon  Type: string;
76c4ef02aeSEvan Bacon}
77c4ef02aeSEvan Bacon
78c4ef02aeSEvan Baconfunction isLockdowndServiceResponse(resp: any): resp is LockdowndServiceResponse {
79c4ef02aeSEvan Bacon  return resp.Request === 'StartService' && resp.Service !== undefined && resp.Port !== undefined;
80c4ef02aeSEvan Bacon}
81c4ef02aeSEvan Bacon
82c4ef02aeSEvan Baconfunction isLockdowndSessionResponse(resp: any): resp is LockdowndSessionResponse {
83c4ef02aeSEvan Bacon  return resp.Request === 'StartSession';
84c4ef02aeSEvan Bacon}
85c4ef02aeSEvan Bacon
86c4ef02aeSEvan Baconfunction isLockdowndAllValuesResponse(resp: any): resp is LockdowndAllValuesResponse {
87c4ef02aeSEvan Bacon  return resp.Request === 'GetValue' && resp.Value !== undefined;
88c4ef02aeSEvan Bacon}
89c4ef02aeSEvan Bacon
90c4ef02aeSEvan Baconfunction isLockdowndValueResponse(resp: any): resp is LockdowndValueResponse {
91c4ef02aeSEvan Bacon  return resp.Request === 'GetValue' && resp.Key !== undefined && typeof resp.Value === 'string';
92c4ef02aeSEvan Bacon}
93c4ef02aeSEvan Bacon
94c4ef02aeSEvan Baconfunction isLockdowndQueryTypeResponse(resp: any): resp is LockdowndQueryTypeResponse {
95c4ef02aeSEvan Bacon  return resp.Request === 'QueryType' && resp.Type !== undefined;
96c4ef02aeSEvan Bacon}
97c4ef02aeSEvan Bacon
98c4ef02aeSEvan Baconexport class LockdowndClient extends ServiceClient<LockdownProtocolClient> {
99c4ef02aeSEvan Bacon  constructor(public socket: Socket) {
100c4ef02aeSEvan Bacon    super(socket, new LockdownProtocolClient(socket));
101c4ef02aeSEvan Bacon  }
102c4ef02aeSEvan Bacon
103c4ef02aeSEvan Bacon  async startService(name: string) {
104c4ef02aeSEvan Bacon    debug(`startService: ${name}`);
105c4ef02aeSEvan Bacon
106c4ef02aeSEvan Bacon    const resp = await this.protocolClient.sendMessage({
107c4ef02aeSEvan Bacon      Request: 'StartService',
108c4ef02aeSEvan Bacon      Service: name,
109c4ef02aeSEvan Bacon    });
110c4ef02aeSEvan Bacon
111c4ef02aeSEvan Bacon    if (isLockdowndServiceResponse(resp)) {
112c4ef02aeSEvan Bacon      return { port: resp.Port, enableServiceSSL: !!resp.EnableServiceSSL };
113c4ef02aeSEvan Bacon    } else {
114c4ef02aeSEvan Bacon      throw new ResponseError(`Error starting service ${name}`, resp);
115c4ef02aeSEvan Bacon    }
116c4ef02aeSEvan Bacon  }
117c4ef02aeSEvan Bacon
118c4ef02aeSEvan Bacon  async startSession(pairRecord: UsbmuxdPairRecord) {
119c4ef02aeSEvan Bacon    debug(`startSession: ${pairRecord}`);
120c4ef02aeSEvan Bacon
121c4ef02aeSEvan Bacon    const resp = await this.protocolClient.sendMessage({
122c4ef02aeSEvan Bacon      Request: 'StartSession',
123c4ef02aeSEvan Bacon      HostID: pairRecord.HostID,
124c4ef02aeSEvan Bacon      SystemBUID: pairRecord.SystemBUID,
125c4ef02aeSEvan Bacon    });
126c4ef02aeSEvan Bacon
127c4ef02aeSEvan Bacon    if (isLockdowndSessionResponse(resp)) {
128c4ef02aeSEvan Bacon      if (resp.EnableSessionSSL) {
129c4ef02aeSEvan Bacon        this.protocolClient.socket = new tls.TLSSocket(this.protocolClient.socket, {
130c4ef02aeSEvan Bacon          secureContext: tls.createSecureContext({
131eec3a87eSCedric van Putten            // Avoid using `secureProtocol` fixing the socket to a single TLS version.
132eec3a87eSCedric van Putten            // Newer Node versions might not support older TLS versions.
133eec3a87eSCedric van Putten            // By using the default `minVersion` and `maxVersion` options,
134eec3a87eSCedric van Putten            // The socket will automatically use the appropriate TLS version.
135eec3a87eSCedric van Putten            // See: https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions
136c4ef02aeSEvan Bacon            cert: pairRecord.RootCertificate,
137c4ef02aeSEvan Bacon            key: pairRecord.RootPrivateKey,
138c4ef02aeSEvan Bacon          }),
139c4ef02aeSEvan Bacon        });
140c4ef02aeSEvan Bacon        debug(`Socket upgraded to TLS connection`);
141c4ef02aeSEvan Bacon      }
142c4ef02aeSEvan Bacon      // TODO: save sessionID for StopSession?
143c4ef02aeSEvan Bacon    } else {
144c4ef02aeSEvan Bacon      throw new ResponseError('Error starting session', resp);
145c4ef02aeSEvan Bacon    }
146c4ef02aeSEvan Bacon  }
147c4ef02aeSEvan Bacon
148c4ef02aeSEvan Bacon  async getAllValues() {
149c4ef02aeSEvan Bacon    debug(`getAllValues`);
150c4ef02aeSEvan Bacon
151c4ef02aeSEvan Bacon    const resp = await this.protocolClient.sendMessage({ Request: 'GetValue' });
152c4ef02aeSEvan Bacon
153c4ef02aeSEvan Bacon    if (isLockdowndAllValuesResponse(resp)) {
154c4ef02aeSEvan Bacon      return resp.Value;
155c4ef02aeSEvan Bacon    } else {
156c4ef02aeSEvan Bacon      throw new ResponseError('Error getting lockdown value', resp);
157c4ef02aeSEvan Bacon    }
158c4ef02aeSEvan Bacon  }
159c4ef02aeSEvan Bacon
160c4ef02aeSEvan Bacon  async getValue(val: string) {
161c4ef02aeSEvan Bacon    debug(`getValue: ${val}`);
162c4ef02aeSEvan Bacon
163c4ef02aeSEvan Bacon    const resp = await this.protocolClient.sendMessage({
164c4ef02aeSEvan Bacon      Request: 'GetValue',
165c4ef02aeSEvan Bacon      Key: val,
166c4ef02aeSEvan Bacon    });
167c4ef02aeSEvan Bacon
168c4ef02aeSEvan Bacon    if (isLockdowndValueResponse(resp)) {
169c4ef02aeSEvan Bacon      return resp.Value;
170c4ef02aeSEvan Bacon    } else {
171c4ef02aeSEvan Bacon      throw new ResponseError('Error getting lockdown value', resp);
172c4ef02aeSEvan Bacon    }
173c4ef02aeSEvan Bacon  }
174c4ef02aeSEvan Bacon
175c4ef02aeSEvan Bacon  async queryType() {
176c4ef02aeSEvan Bacon    debug('queryType');
177c4ef02aeSEvan Bacon
178c4ef02aeSEvan Bacon    const resp = await this.protocolClient.sendMessage({
179c4ef02aeSEvan Bacon      Request: 'QueryType',
180c4ef02aeSEvan Bacon    });
181c4ef02aeSEvan Bacon
182c4ef02aeSEvan Bacon    if (isLockdowndQueryTypeResponse(resp)) {
183c4ef02aeSEvan Bacon      return resp.Type;
184c4ef02aeSEvan Bacon    } else {
185c4ef02aeSEvan Bacon      throw new ResponseError('Error getting lockdown query type', resp);
186c4ef02aeSEvan Bacon    }
187c4ef02aeSEvan Bacon  }
188c4ef02aeSEvan Bacon
189c4ef02aeSEvan Bacon  async doHandshake(pairRecord: UsbmuxdPairRecord) {
190c4ef02aeSEvan Bacon    debug('doHandshake');
191c4ef02aeSEvan Bacon
192c4ef02aeSEvan Bacon    // if (await this.lockdownQueryType() !== 'com.apple.mobile.lockdown') {
193c4ef02aeSEvan Bacon    //   throw new CommandError('Invalid type received from lockdown handshake');
194c4ef02aeSEvan Bacon    // }
195c4ef02aeSEvan Bacon    // await this.getLockdownValue('ProductVersion');
196c4ef02aeSEvan Bacon    // TODO: validate pair and pair
197c4ef02aeSEvan Bacon    await this.startSession(pairRecord);
198c4ef02aeSEvan Bacon  }
199c4ef02aeSEvan Bacon}
200