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            secureProtocol: 'TLSv1_method',
132            cert: pairRecord.RootCertificate,
133            key: pairRecord.RootPrivateKey,
134          }),
135        });
136        debug(`Socket upgraded to TLS connection`);
137      }
138      // TODO: save sessionID for StopSession?
139    } else {
140      throw new ResponseError('Error starting session', resp);
141    }
142  }
143
144  async getAllValues() {
145    debug(`getAllValues`);
146
147    const resp = await this.protocolClient.sendMessage({ Request: 'GetValue' });
148
149    if (isLockdowndAllValuesResponse(resp)) {
150      return resp.Value;
151    } else {
152      throw new ResponseError('Error getting lockdown value', resp);
153    }
154  }
155
156  async getValue(val: string) {
157    debug(`getValue: ${val}`);
158
159    const resp = await this.protocolClient.sendMessage({
160      Request: 'GetValue',
161      Key: val,
162    });
163
164    if (isLockdowndValueResponse(resp)) {
165      return resp.Value;
166    } else {
167      throw new ResponseError('Error getting lockdown value', resp);
168    }
169  }
170
171  async queryType() {
172    debug('queryType');
173
174    const resp = await this.protocolClient.sendMessage({
175      Request: 'QueryType',
176    });
177
178    if (isLockdowndQueryTypeResponse(resp)) {
179      return resp.Type;
180    } else {
181      throw new ResponseError('Error getting lockdown query type', resp);
182    }
183  }
184
185  async doHandshake(pairRecord: UsbmuxdPairRecord) {
186    debug('doHandshake');
187
188    // if (await this.lockdownQueryType() !== 'com.apple.mobile.lockdown') {
189    //   throw new CommandError('Invalid type received from lockdown handshake');
190    // }
191    // await this.getLockdownValue('ProductVersion');
192    // TODO: validate pair and pair
193    await this.startSession(pairRecord);
194  }
195}
196