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  DeviceClass: string;
31  DeviceColor: string;
32  DeviceName: string;
33  DieID: number;
34  HardwareModel: string;
35  HasSiDP: boolean;
36  PartitionType: string;
37  ProductName: string;
38  ProductType: string;
39  ProductVersion: string;
40  ProductionSOC: boolean;
41  ProtocolVersion: string;
42  TelephonyCapability: boolean;
43  UniqueChipID: number;
44  UniqueDeviceID: string;
45  WiFiAddress: string;
46  [key: string]: any;
47}
48
49interface LockdowndServiceResponse {
50  Request: 'StartService';
51  Service: string;
52  Port: number;
53  EnableServiceSSL?: boolean; // Only on iOS 13+
54}
55
56interface LockdowndSessionResponse {
57  Request: 'StartSession';
58  EnableSessionSSL: boolean;
59}
60
61interface LockdowndAllValuesResponse {
62  Request: 'GetValue';
63  Value: DeviceValues;
64}
65
66interface LockdowndValueResponse {
67  Request: 'GetValue';
68  Key: string;
69  Value: string;
70}
71
72interface LockdowndQueryTypeResponse {
73  Request: 'QueryType';
74  Type: string;
75}
76
77function isLockdowndServiceResponse(resp: any): resp is LockdowndServiceResponse {
78  return resp.Request === 'StartService' && resp.Service !== undefined && resp.Port !== undefined;
79}
80
81function isLockdowndSessionResponse(resp: any): resp is LockdowndSessionResponse {
82  return resp.Request === 'StartSession';
83}
84
85function isLockdowndAllValuesResponse(resp: any): resp is LockdowndAllValuesResponse {
86  return resp.Request === 'GetValue' && resp.Value !== undefined;
87}
88
89function isLockdowndValueResponse(resp: any): resp is LockdowndValueResponse {
90  return resp.Request === 'GetValue' && resp.Key !== undefined && typeof resp.Value === 'string';
91}
92
93function isLockdowndQueryTypeResponse(resp: any): resp is LockdowndQueryTypeResponse {
94  return resp.Request === 'QueryType' && resp.Type !== undefined;
95}
96
97export class LockdowndClient extends ServiceClient<LockdownProtocolClient> {
98  constructor(public socket: Socket) {
99    super(socket, new LockdownProtocolClient(socket));
100  }
101
102  async startService(name: string) {
103    debug(`startService: ${name}`);
104
105    const resp = await this.protocolClient.sendMessage({
106      Request: 'StartService',
107      Service: name,
108    });
109
110    if (isLockdowndServiceResponse(resp)) {
111      return { port: resp.Port, enableServiceSSL: !!resp.EnableServiceSSL };
112    } else {
113      throw new ResponseError(`Error starting service ${name}`, resp);
114    }
115  }
116
117  async startSession(pairRecord: UsbmuxdPairRecord) {
118    debug(`startSession: ${pairRecord}`);
119
120    const resp = await this.protocolClient.sendMessage({
121      Request: 'StartSession',
122      HostID: pairRecord.HostID,
123      SystemBUID: pairRecord.SystemBUID,
124    });
125
126    if (isLockdowndSessionResponse(resp)) {
127      if (resp.EnableSessionSSL) {
128        this.protocolClient.socket = new tls.TLSSocket(this.protocolClient.socket, {
129          secureContext: tls.createSecureContext({
130            secureProtocol: 'TLSv1_method',
131            cert: pairRecord.RootCertificate,
132            key: pairRecord.RootPrivateKey,
133          }),
134        });
135        debug(`Socket upgraded to TLS connection`);
136      }
137      // TODO: save sessionID for StopSession?
138    } else {
139      throw new ResponseError('Error starting session', resp);
140    }
141  }
142
143  async getAllValues() {
144    debug(`getAllValues`);
145
146    const resp = await this.protocolClient.sendMessage({ Request: 'GetValue' });
147
148    if (isLockdowndAllValuesResponse(resp)) {
149      return resp.Value;
150    } else {
151      throw new ResponseError('Error getting lockdown value', resp);
152    }
153  }
154
155  async getValue(val: string) {
156    debug(`getValue: ${val}`);
157
158    const resp = await this.protocolClient.sendMessage({
159      Request: 'GetValue',
160      Key: val,
161    });
162
163    if (isLockdowndValueResponse(resp)) {
164      return resp.Value;
165    } else {
166      throw new ResponseError('Error getting lockdown value', resp);
167    }
168  }
169
170  async queryType() {
171    debug('queryType');
172
173    const resp = await this.protocolClient.sendMessage({
174      Request: 'QueryType',
175    });
176
177    if (isLockdowndQueryTypeResponse(resp)) {
178      return resp.Type;
179    } else {
180      throw new ResponseError('Error getting lockdown query type', resp);
181    }
182  }
183
184  async doHandshake(pairRecord: UsbmuxdPairRecord) {
185    debug('doHandshake');
186
187    // if (await this.lockdownQueryType() !== 'com.apple.mobile.lockdown') {
188    //   throw new CommandError('Invalid type received from lockdown handshake');
189    // }
190    // await this.getLockdownValue('ProductVersion');
191    // TODO: validate pair and pair
192    await this.startSession(pairRecord);
193  }
194}
195