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