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 { ResponseError, ServiceClient } from './ServiceClient'; 13import { UsbmuxdPairRecord } from './UsbmuxdClient'; 14import { LockdownProtocolClient } from '../protocol/LockdownProtocol'; 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