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