FBDeviceControl/Commands/FBDeviceActivationCommands.m (270 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "FBDeviceActivationCommands.h" #import "FBDevice.h" #import "FBAMDServiceConnection.h" static NSString *const DefaultDRMHandshakeURL = @"https://albert.apple.com/deviceservices/drmHandshake"; static NSString *const DefaultDeviceActivationURL = @"https://albert.apple.com/deviceservices/deviceActivation"; @interface FBDeviceActivationCommands () @property (nonatomic, weak, readonly) FBDevice *device; @end @implementation FBDeviceActivationCommands #pragma mark Initializers + (instancetype)commandsWithTarget:(FBDevice *)target { return [[self alloc] initWithDevice:target]; } - (instancetype)initWithDevice:(FBDevice *)device { self = [super init]; if (!self) { return nil; } _device = device; return self; } #pragma mark FBDeviceActivationCommands Implementation - (FBFuture<NSNull *> *)activate { id<FBControlCoreLogger> logger = self.device.logger; return [[self activationState] onQueue:self.device.asyncQueue fmap:^ FBFuture<NSNull *> * (FBDeviceActivationState activationState) { if ([activationState isEqualToString:FBDeviceActivationStateActivated]) { [logger logFormat:@"Device is already activated, nothing to activate"]; return FBFuture.empty; } if ([activationState isEqualToString:FBDeviceActivationStateUnactivated]) { [logger logFormat:@"Device is not activated, starting activation"]; return [self performActivation]; } return [[FBControlCoreError describeFormat:@"%@ is not a valid activation state", activationState] failFuture]; }]; } #pragma mark Private - (FBFuture<NSNull *> *)confirmActivationState:(FBDeviceActivationState)activationState { return [[self activationState] onQueue:self.device.asyncQueue fmap:^ FBFuture<NSNull *> * (FBDeviceActivationState actualActivationState) { if (![activationState isEqualToString:actualActivationState]) { return [[FBControlCoreError describeFormat:@"Activation State %@ is not equal to actual activation state %@", activationState, actualActivationState] failFuture]; } return FBFuture.empty; }]; } - (FBFuture<NSNull *> *)performActivation { id<FBControlCoreLogger> logger = self.device.logger; return [[[[[self confirmActivationState:FBDeviceActivationStateUnactivated] onQueue:self.device.workQueue fmap:^(id _) { [logger logFormat:@"Building DRM Handshake Payload"]; return [self buildDRMHandshakePayload]; }] onQueue:self.device.workQueue fmap:^(NSData *drmHandhakePayload) { [logger logFormat:@"Obtaining Activation record from DRM Handshake Payload"]; return [self activationRecordFromDRMHandshakePayload:drmHandhakePayload]; }] onQueue:self.device.workQueue fmap:^(NSData *activationRecordPayload) { [logger logFormat:@"Performing activation from activation record"]; return [self activateFromActivationRecord:activationRecordPayload]; }] onQueue:self.device.workQueue fmap:^(id _) { [logger logFormat:@"Confirming activation state is Activated"]; return [self confirmActivationState:FBDeviceActivationStateActivated]; }]; } - (FBFutureContext<FBAMDServiceConnection *> *)mobileActivationService { return [self.device startService:@"com.apple.mobileactivationd"]; } - (FBFuture<FBDeviceActivationState> *)activationState { return [[self mobileActivationService] onQueue:self.device.workQueue pop:^ FBFuture<NSData *> * (FBAMDServiceConnection *connection) { NSError *error = nil; id response = [connection sendAndReceiveMessage:@{@"Command": @"GetActivationStateRequest"} error:&error]; if (!response) { return [FBFuture futureWithError:error]; } NSString *activationState = response[@"Value"]; if (![activationState isKindOfClass:NSString.class]) { return [[FBControlCoreError describeFormat:@"No Activation State in %@", response] failFuture]; } return [FBFuture futureWithResult:FBDeviceActivationStateCoerceFromString(activationState)]; }]; } - (FBFuture<NSData *> *)buildDRMHandshakePayload { return [[self mobileActivationService] onQueue:self.device.workQueue pop:^ FBFuture<NSData *> * (FBAMDServiceConnection *connection) { NSError *error = nil; id response = [connection sendAndReceiveMessage:@{@"Command": @"CreateTunnel1SessionInfoRequest"} error:&error]; if (!response) { return [FBFuture futureWithError:error]; } id responsePayload = response[@"Value"]; if (!responsePayload) { return [[FBControlCoreError describeFormat:@"No 'Value' in %@", response] failFuture]; } return [FBDeviceActivationCommands mobileActivationRequestForRequestPayload:responsePayload queue:self.device.workQueue]; }]; } - (FBFuture<NSData *> *)activationRecordFromDRMHandshakePayload:(NSData *)handshakePayload { return [[self mobileActivationService] onQueue:self.device.workQueue pop:^ FBFuture<NSData *> * (FBAMDServiceConnection *connection) { NSError *error = nil; id response = [connection sendAndReceiveMessage:@{@"Command": @"CreateTunnel1ActivationInfoRequest", @"Value": handshakePayload} error:&error]; if (!response) { return [FBFuture futureWithError:error]; } NSDictionary<NSString *, id> *responsePayload = response[@"Value"]; if (!responsePayload) { return [[FBControlCoreError describeFormat:@"No 'Value' in %@", response] failFuture]; } return [FBDeviceActivationCommands mobileActivationActivateForRequestPayload:responsePayload queue:self.device.workQueue]; }]; } - (FBFuture<NSNull *> *)activateFromActivationRecord:(NSData *)activationRecord { return [[self mobileActivationService] onQueue:self.device.workQueue pop:^ FBFuture<NSNull *> * (FBAMDServiceConnection *connection) { NSError *error = nil; id response = [connection sendAndReceiveMessage:@{@"Command": @"HandleActivationInfoWithSessionRequest", @"Value": activationRecord} error:&error]; if (!response) { return [FBFuture futureWithError:error]; } return FBFuture.empty; }]; } + (FBFuture<NSData *> *)mobileActivationRequestForRequestPayload:(NSDictionary<NSString *, id> *)requestPayload queue:(dispatch_queue_t)queue { NSError *error = nil; NSData *body = [NSPropertyListSerialization dataWithPropertyList:requestPayload format:NSPropertyListXMLFormat_v1_0 options:0 error:&error]; if (!body) { return [FBFuture futureWithError:error]; } NSURL *url = [NSURL URLWithString:NSProcessInfo.processInfo.environment[@"IDB_DRM_HANDSHAKE_URL"] ?: DefaultDRMHandshakeURL]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; request.HTTPMethod = @"POST"; request.HTTPBody = body; [request setValue:@"application/x-apple-plist" forHTTPHeaderField:@"Content-Type"]; [request setValue:@"application/xml" forHTTPHeaderField:@"Accept"]; [request setValue:@"idb (https://github.com/facebook/idb/blob/main/FBDeviceControl/Commands/FBDeviceActivationCommands.m)" forHTTPHeaderField:@"User-Agent"]; return [[self responseForRequest:request] onQueue:queue fmap:^(NSArray<id> *result) { NSHTTPURLResponse *httpResponse = result[0]; if (httpResponse.statusCode != 200) { return [[FBControlCoreError describeFormat:@"%@ no 200", httpResponse] failFuture]; } NSData *responseData = result[1]; NSError *innerError = nil; NSDictionary<NSString *, id> *response = [NSPropertyListSerialization propertyListWithData:responseData options:0 format:nil error:&innerError]; if (!response) { return [FBFuture futureWithError:innerError]; } return [FBFuture futureWithResult:responseData]; }]; } + (FBFuture<NSData *> *)mobileActivationActivateForRequestPayload:(NSDictionary<NSString *, id> *)requestPayload queue:(dispatch_queue_t)queue { NSError *error = nil; NSData *payloadData = [NSPropertyListSerialization dataWithPropertyList:requestPayload format:NSPropertyListXMLFormat_v1_0 options:0 error:&error]; if (!payloadData) { return [FBFuture futureWithError:error]; } // Multipart info NSString *boundaryConstant = NSUUID.UUID.UUIDString; NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundaryConstant]; NSURL *url = [NSURL URLWithString:NSProcessInfo.processInfo.environment[@"IDB_ACTIVATION_URL"] ?: DefaultDeviceActivationURL]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; request.HTTPMethod = @"POST"; request.HTTPBody = [self multipartDataFromRequestPayload:payloadData key:@"activation-info" boundary:boundaryConstant]; [request setValue:contentType forHTTPHeaderField:@"Content-Type"]; [request setValue:@"idb (https://github.com/facebook/idb/blob/main/FBDeviceControl/Commands/FBDeviceActivationCommands.m)" forHTTPHeaderField:@"User-Agent"]; return [[self responseForRequest:request] onQueue:queue fmap:^(NSArray<id> *result) { NSHTTPURLResponse *httpResponse = result[0]; if (httpResponse.statusCode != 200) { return [[FBControlCoreError describeFormat:@"%@ no 200", httpResponse] failFuture]; } NSData *responseData = result[1]; NSError *innerError = nil; id response = [NSPropertyListSerialization propertyListWithData:responseData options:0 format:nil error:&innerError]; if (!response) { return [FBFuture futureWithError:innerError]; } id activationRecord = response[@"ActivationRecord"]; if (!activationRecord) { return [[FBControlCoreError describeFormat:@"No 'ActivationRecord' in %@", activationRecord] failFuture]; } NSData *activationRecordData = [NSPropertyListSerialization dataWithPropertyList:activationRecord format:NSPropertyListXMLFormat_v1_0 options:0 error:&innerError]; if (!activationRecordData) { return [FBFuture futureWithError:innerError]; } return [FBFuture futureWithResult:activationRecordData]; }]; } + (FBFuture<id> *)responseForRequest:(NSURLRequest *)request { NSURLSession *session = NSURLSession.sharedSession; FBMutableFuture<id> *future = FBMutableFuture.future; NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *responseData, NSURLResponse *response, NSError *error) { if (error) { [future resolveWithError:error]; return; } if (responseData == nil) { [future resolveWithError:[[FBControlCoreError describeFormat:@"No response data in response %@", response] build]]; return; } [future resolveWithResult:@[response, responseData]]; }]; [task resume]; return future; } + (NSData *)multipartDataFromRequestPayload:(NSData *)payload key:(NSString *)key boundary:(NSString *)boundary { NSData *dashesData = [@"--" dataUsingEncoding:NSUTF8StringEncoding]; NSData *newlineData = [@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]; NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding]; NSData *boundaryData = [boundary dataUsingEncoding:NSUTF8StringEncoding]; NSData *valueHeaderData = [@"Content-Disposition: form-data; name=" dataUsingEncoding:NSUTF8StringEncoding]; NSMutableData *data = NSMutableData.data; // Header prefixed with dashes. [data appendData:dashesData]; [data appendData:boundaryData]; [data appendData:newlineData]; // Then the key-value [data appendData:valueHeaderData]; [data appendData:keyData]; [data appendData:newlineData]; [data appendData:newlineData]; [data appendData:payload]; [data appendData:newlineData]; // Then the trailer, suffixed with dashes [data appendData:dashesData]; [data appendData:boundaryData]; [data appendData:dashesData]; [data appendData:newlineData]; return data; } @end