1 #import <TargetConditionals.h>
2 #import <Foundation/Foundation.h>
4 #import <KeychainCircle/KeychainCircle.h>
6 #import <xpc/private.h>
9 #import <Security/OTControl.h>
10 #endif /* TARGET_OS_WATCH */
12 #if !TARGET_OS_SIMULATOR
13 #import <MobileKeyBag/MobileKeyBag.h>
14 #endif /* !TARGET_OS_SIMULATOR */
16 #import "OTPairingService.h"
17 #import "OTPairingPacketContext.h"
18 #import "OTPairingSession.h"
19 #import "OTPairingConstants.h"
21 #import "keychain/categories/NSError+UsefulConstructors.h"
22 #import "keychain/ot/OTDeviceInformationAdapter.h"
24 #define WAIT_FOR_UNLOCK_DURATION (120ull)
26 @interface OTPairingService ()
27 @property dispatch_queue_t queue;
28 @property IDSService *service;
29 @property dispatch_source_t unlockTimer;
30 @property int notifyToken;
31 @property (nonatomic, strong) OTDeviceInformationActualAdapter *deviceInfo;
32 @property OTPairingSession *session;
35 @implementation OTPairingService
37 + (instancetype)sharedService
39 static dispatch_once_t once;
40 static OTPairingService *service;
42 dispatch_once(&once, ^{
43 service = [[OTPairingService alloc] init];
51 if ((self = [super init])) {
52 self.queue = dispatch_queue_create("com.apple.security.otpaird", DISPATCH_QUEUE_SERIAL);
53 self.service = [[IDSService alloc] initWithService:OTPairingIDSServiceName];
54 [self.service addDelegate:self queue:self.queue];
55 self.notifyToken = NOTIFY_TOKEN_INVALID;
56 self.deviceInfo = [[OTDeviceInformationActualAdapter alloc] init];
61 - (NSString *)pairedDeviceNotificationName
63 NSString *result = nil;
64 for (IDSDevice *device in self.service.devices) {
65 if (device.isDefaultPairedDevice) {
66 result = [NSString stringWithFormat:@"ids-device-state-%@", device.uniqueIDOverride];
74 - (void)initiatePairingWithCompletion:(OTPairingCompletionHandler)completionHandler
76 dispatch_assert_queue_not(self.queue);
78 if ([self _octagonInClique]) {
79 os_log(OS_LOG_DEFAULT, "already in octagon, bailing");
80 completionHandler(false, [NSError errorWithDomain:OTPairingErrorDomain code:OTPairingErrorTypeAlreadyIn description:@"already in octagon"]);
84 dispatch_async(self.queue, ^{
85 if (self.session != nil) {
86 completionHandler(false, [NSError errorWithDomain:OTPairingErrorDomain code:OTPairingErrorTypeBusy description:@"pairing in progress"]);
90 self.session = [[OTPairingSession alloc] initWithDeviceInfo:self.deviceInfo];
91 self.session.completionHandler = completionHandler;
92 [self sendReplyToPacket];
95 #endif /* TARGET_OS_WATCH */
97 // Should be a delegate method - future refactor
98 - (void)session:(__unused OTPairingSession *)session didCompleteWithSuccess:(bool)success error:(NSError *)error
100 os_assert(self.session == session);
103 self.session.completionHandler(success, error);
104 #endif /* TARGET_OS_WATCH */
107 self.unlockTimer = nil;
112 - (void)sendReplyToPacket
114 dispatch_assert_queue(self.queue);
116 #if !TARGET_OS_SIMULATOR
117 NSDictionary *lockOptions;
118 CFErrorRef lockError = NULL;
121 (__bridge NSString *)kMKBAssertionTypeKey : (__bridge NSString *)kMKBAssertionTypeOther,
122 (__bridge NSString *)kMKBAssertionTimeoutKey : @(60),
124 self.session.lockAssertion = MKBDeviceLockAssertion((__bridge CFDictionaryRef)lockOptions, &lockError);
126 if (self.session.lockAssertion) {
127 [self stopWaitingForDeviceUnlock];
128 [self exchangePacketAndReply];
130 os_log(OS_LOG_DEFAULT, "Failed to obtain lock assertion: %@", lockError);
132 CFRelease(lockError);
134 [self waitForDeviceUnlock];
136 #else /* TARGET_OS_SIMULATOR */
137 [self exchangePacketAndReply];
138 #endif /* !TARGET_OS_SIMULATOR */
141 - (void)deviceUnlockTimedOut
143 dispatch_assert_queue(self.queue);
145 /* Should not happen; be safe. */
146 if (self.session == nil) {
151 OTPairingPacketContext *packet = self.session.packet;
152 self.session.packet = nil;
154 os_assert(packet != nil); // the acceptor always responds to a request packet, it's never initiating
156 NSError *unlockError = [NSError errorWithDomain:OTPairingErrorDomain code:OTPairingErrorTypeLock description:@"timed out waiting for companion unlock"];
157 NSMutableDictionary *message = [[NSMutableDictionary alloc] init];
158 message[OTPairingIDSKeyMessageType] = @(OTPairingIDSMessageTypeError);
159 message[OTPairingIDSKeySession] = self.session.identifier;
160 message[OTPairingIDSKeyErrorDescription] = unlockError.description;
161 NSString *toID = packet.fromID;
162 NSString *responseIdentifier = packet.outgoingResponseIdentifier;
163 [self _sendMessage:message to:toID identifier:responseIdentifier];
165 [self scheduleGizmoPoke];
166 #endif /* TARGET_OS_IOS */
168 [self session:self.session didCompleteWithSuccess:false error:[NSError errorWithDomain:OTPairingErrorDomain code:OTPairingErrorTypeLock description:@"timed out waiting for unlock"]];
171 - (void)exchangePacketAndReply
173 dispatch_assert_queue(self.queue);
175 OTPairingPacketContext *packet = self.session.packet;
176 self.session.packet = nil;
178 [self.session.channel exchangePacket:packet.packetData complete:^(BOOL complete, NSData *responsePacket, NSError *channelError) {
179 /* this runs on a variety of different queues depending on the step (caller's queue, or an NSXPC queue) */
181 dispatch_async(self.queue, ^{
183 NSString *responseIdentifier;
185 os_log(OS_LOG_DEFAULT, "exchangePacket: complete=%d responsePacket=%@ channelError=%@", complete, responsePacket, channelError);
187 if (self.session == nil) {
188 os_log(OS_LOG_DEFAULT, "pairing session went away, dropping exchangePacket response");
192 if (channelError != nil) {
194 NSMutableDictionary *message = [[NSMutableDictionary alloc] init];
195 message[OTPairingIDSKeyMessageType] = @(OTPairingIDSMessageTypeError);
196 message[OTPairingIDSKeySession] = self.session.identifier;
197 message[OTPairingIDSKeyErrorDescription] = channelError.description;
198 os_assert(packet != nil); // the acceptor always responds to a request packet, it's never initiating
199 toID = packet.fromID;
200 responseIdentifier = packet.outgoingResponseIdentifier;
201 [self _sendMessage:message to:toID identifier:responseIdentifier];
204 [self session:self.session didCompleteWithSuccess:false error:[NSError errorWithDomain:OTPairingErrorDomain code:OTPairingErrorTypeKCPairing description:@"exchangePacket" underlying:channelError]];
209 if (responsePacket != nil) {
210 NSDictionary *message = @{
211 OTPairingIDSKeyMessageType : @(OTPairingIDSMessageTypePacket),
212 OTPairingIDSKeySession : self.session.identifier,
213 OTPairingIDSKeyPacket : responsePacket,
215 toID = packet ? packet.fromID : IDSDefaultPairedDevice;
216 responseIdentifier = packet ? packet.outgoingResponseIdentifier : nil;
217 [self _sendMessage:message to:toID identifier:responseIdentifier];
221 [self session:self.session didCompleteWithSuccess:true error:nil];
227 - (void)_sendMessage:(NSDictionary *)message to:(NSString *)toID identifier:(NSString *)responseIdentifier
229 [self _sendMessage:message to:toID identifier:responseIdentifier expectReply:YES];
232 - (void)_sendMessage:(NSDictionary *)message to:(NSString *)toID identifier:(NSString *)responseIdentifier expectReply:(BOOL)expectReply
234 dispatch_assert_queue(self.queue);
237 NSMutableDictionary *options;
238 NSString *identifier = nil;
239 NSError *error = nil;
240 BOOL sendResult = NO;
242 destinations = [NSSet setWithObject:toID];
244 options = [NSMutableDictionary new];
245 options[IDSSendMessageOptionForceLocalDeliveryKey] = @YES;
246 options[IDSSendMessageOptionExpectsPeerResponseKey] = @(expectReply); // TODO: when are we complete??
247 if (responseIdentifier != nil) {
248 options[IDSSendMessageOptionPeerResponseIdentifierKey] = responseIdentifier;
251 sendResult = [self.service sendMessage:message
252 toDestinations:destinations
253 priority:IDSMessagePriorityDefault
255 identifier:&identifier
258 /* sentMessageIdentifier is used to validate the next reply; do not set if no reply is expected. */
260 self.session.sentMessageIdentifier = identifier;
263 os_log(OS_LOG_DEFAULT, "send message failed (%@): %@", identifier, error);
264 // On iOS, do nothing; watch will time out waiting for response.
265 [self session:self.session didCompleteWithSuccess:false error:[NSError errorWithDomain:OTPairingErrorDomain code:OTPairingErrorTypeIDS description:@"IDS message send failure" underlying:error]];
269 #pragma mark IDSServiceDelegate methods
271 - (void)service:(IDSService *)service account:(__unused IDSAccount *)account incomingMessage:(NSDictionary *)message fromID:(NSString *)fromID context:(IDSMessageContext *)context
273 dispatch_assert_queue(self.queue);
275 OTPairingPacketContext *packet = nil;
276 bool validateIdentifier = true;
278 os_log(OS_LOG_DEFAULT, "IDS message from %@: %@", fromID, message);
280 packet = [[OTPairingPacketContext alloc] initWithMessage:message fromID:fromID context:context];
282 if (packet.messageType == OTPairingIDSMessageTypePoke) {
284 // on self.queue now, but initiatePairingWithCompletion: must _not_ be on self.queue
285 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
286 os_log(OS_LOG_DEFAULT, "companion claims to be unlocked, retrying");
287 [self initiatePairingWithCompletion:^(bool success, NSError *error) {
289 os_log(OS_LOG_DEFAULT, "companion-unlocked retry succeeded");
291 os_log(OS_LOG_DEFAULT, "companion-unlocked retry failed: %@", error);
295 #endif /* TARGET_OS_WATCH */
300 * Check for missing/invalid session identifier. Since the watch is the initiator,
301 * iOS responds to an invalid session identifier by resetting state and starting over.
303 if (packet.sessionIdentifier == nil) {
304 os_log(OS_LOG_DEFAULT, "ignoring message with no session identifier (old build?)");
306 } else if (![packet.sessionIdentifier isEqualToString:self.session.identifier]) {
308 os_log(OS_LOG_DEFAULT, "unknown session identifier, dropping message");
311 os_log(OS_LOG_DEFAULT, "unknown session identifier %@, creating new session object", packet.sessionIdentifier);
312 self.session = [[OTPairingSession alloc] initWithDeviceInfo:self.deviceInfo identifier:packet.sessionIdentifier];
313 validateIdentifier = false;
314 #endif /* TARGET_OS_IOS */
317 if (validateIdentifier && ![self.session.sentMessageIdentifier isEqualToString:packet.incomingResponseIdentifier]) {
318 os_log(OS_LOG_DEFAULT, "ignoring message with unrecognized incomingResponseIdentifier");
322 switch (packet.messageType) {
323 case OTPairingIDSMessageTypeError:
324 [self session:self.session didCompleteWithSuccess:false error:[NSError errorWithDomain:OTPairingErrorDomain code:OTPairingErrorTypeRemote description:@"companion error" underlying:packet.error]];
326 case OTPairingIDSMessageTypePacket:
327 self.session.packet = packet;
328 [self sendReplyToPacket];
330 case OTPairingIDSMessageTypePoke:
336 - (void)service:(__unused IDSService *)service account:(__unused IDSAccount *)account identifier:(NSString *)identifier didSendWithSuccess:(BOOL)success error:(NSError *)error context:(__unused IDSMessageContext *)context
338 dispatch_assert_queue(self.queue);
340 /* Only accept callback if it is for the recently-sent message. */
341 if (![self.session.sentMessageIdentifier isEqualToString:identifier]) {
342 os_log(OS_LOG_DEFAULT, "ignoring didSendWithSuccess callback for unexpected identifier: %@", identifier);
347 os_log(OS_LOG_DEFAULT, "unsuccessfully sent message (%@): %@", identifier, error);
348 // On iOS, do nothing; watch will time out waiting for response.
349 [self session:self.session didCompleteWithSuccess:false error:[NSError errorWithDomain:OTPairingErrorDomain code:OTPairingErrorTypeIDS description:@"IDS message failed to send" underlying:error]];
353 #pragma mark lock state handling
355 #if !TARGET_OS_SIMULATOR
356 - (void)waitForDeviceUnlock
358 dispatch_assert_queue(self.queue);
360 static dispatch_once_t once;
361 static dispatch_source_t lockStateCoalescingSource;
362 uint32_t notify_status;
365 dispatch_once(&once, ^{
366 // Everything here is on one queue, so we sometimes get several notifications while busy doing something else.
367 lockStateCoalescingSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_OR, 0, 0, self.queue);
368 dispatch_source_set_event_handler(lockStateCoalescingSource, ^{
369 [self sendReplyToPacket];
371 dispatch_activate(lockStateCoalescingSource);
374 if (self.notifyToken == NOTIFY_TOKEN_INVALID) {
375 notify_status = notify_register_dispatch(kMobileKeyBagLockStatusNotificationID, &token, self.queue, ^(__unused int t) {
376 dispatch_source_merge_data(lockStateCoalescingSource, 1);
378 if (os_assumes_zero(notify_status) == NOTIFY_STATUS_OK) {
379 self.notifyToken = token;
383 self.unlockTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.queue);
384 dispatch_source_set_timer(self.unlockTimer, dispatch_time(DISPATCH_TIME_NOW, WAIT_FOR_UNLOCK_DURATION * NSEC_PER_SEC), DISPATCH_TIME_FOREVER, 0);
385 dispatch_source_set_event_handler(self.unlockTimer, ^{
386 [self stopWaitingForDeviceUnlock];
387 [self deviceUnlockTimedOut];
389 dispatch_activate(self.unlockTimer);
391 // double-check to prevent race condition, try again soon
392 if (MKBGetDeviceLockState(NULL) == kMobileKeyBagDeviceIsUnlocked) {
393 [self stopWaitingForDeviceUnlock];
394 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 5ull * NSEC_PER_SEC), self.queue, ^{
395 [self sendReplyToPacket];
400 - (void)stopWaitingForDeviceUnlock
402 dispatch_assert_queue(self.queue);
404 uint32_t notify_status;
406 if (self.notifyToken != NOTIFY_TOKEN_INVALID) {
407 notify_status = notify_cancel(self.notifyToken);
408 os_assumes_zero(notify_status);
409 self.notifyToken = NOTIFY_TOKEN_INVALID;
412 if (self.unlockTimer != nil) {
413 dispatch_source_cancel(self.unlockTimer);
414 self.unlockTimer = nil;
417 #endif /* !TARGET_OS_SIMULATOR */
420 - (void)scheduleGizmoPoke
422 xpc_object_t criteria;
424 criteria = xpc_dictionary_create(NULL, NULL, 0);
425 xpc_dictionary_set_string(criteria, XPC_ACTIVITY_PRIORITY, XPC_ACTIVITY_PRIORITY_MAINTENANCE);
426 xpc_dictionary_set_int64(criteria, XPC_ACTIVITY_DELAY, 0ll);
427 xpc_dictionary_set_int64(criteria, XPC_ACTIVITY_GRACE_PERIOD, 0ll);
428 xpc_dictionary_set_bool(criteria, XPC_ACTIVITY_REPEATING, false);
429 xpc_dictionary_set_bool(criteria, XPC_ACTIVITY_ALLOW_BATTERY, true);
430 xpc_dictionary_set_bool(criteria, XPC_ACTIVITY_REQUIRES_CLASS_A, true);
431 xpc_dictionary_set_bool(criteria, XPC_ACTIVITY_COMMUNICATES_WITH_PAIRED_DEVICE, true);
433 os_log(OS_LOG_DEFAULT, "scheduling XPC Activity to inform gizmo of companion unlock");
434 xpc_activity_register(OTPairingXPCActivityPoke, criteria, ^(xpc_activity_t activity) {
435 xpc_activity_state_t state = xpc_activity_get_state(activity);
436 if (state == XPC_ACTIVITY_STATE_RUN) {
437 dispatch_sync(self.queue, ^{
438 os_log(OS_LOG_DEFAULT, "poking gizmo now");
439 NSDictionary *message = @{
440 OTPairingIDSKeyMessageType : @(OTPairingIDSMessageTypePoke),
442 [self _sendMessage:message to:IDSDefaultPairedDevice identifier:nil expectReply:NO];
447 #endif /* TARGET_OS_IOS */
449 #pragma mark Octagon Clique Status
452 - (bool)_octagonInClique
454 __block bool result = false;
455 NSError *ctlError = nil;
457 OTControl *ctl = [OTControl controlObject:true error:&ctlError];
459 OTOperationConfiguration *config = [OTOperationConfiguration new];
460 config.useCachedAccountStatus = true;
461 [ctl fetchCliqueStatus:nil context:OTDefaultContext configuration:config reply:^(CliqueStatus cliqueStatus, NSError * _Nullable error) {
462 result = (cliqueStatus == CliqueStatusIn);
465 os_log(OS_LOG_DEFAULT, "failed to acquire OTControl: %@", ctlError);
470 #endif /* TARGET_OS_WATCH */