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