1 #import <TargetConditionals.h>
2 #import <Foundation/Foundation.h>
4 #import <KeychainCircle/KeychainCircle.h>
6 #import <Security/SecXPCHelper.h>
7 #import <xpc/private.h>
10 #import <Security/OTControl.h>
11 #endif /* TARGET_OS_WATCH */
13 #if !TARGET_OS_SIMULATOR
14 #import <MobileKeyBag/MobileKeyBag.h>
15 #endif /* !TARGET_OS_SIMULATOR */
17 #import "OTPairingService.h"
18 #import "OTPairingPacketContext.h"
19 #import "OTPairingSession.h"
20 #import "OTPairingConstants.h"
22 #import "keychain/categories/NSError+UsefulConstructors.h"
23 #import "keychain/ot/OTDeviceInformationAdapter.h"
25 #define WAIT_FOR_UNLOCK_DURATION (120ull)
27 @interface OTPairingService ()
28 @property dispatch_queue_t queue;
29 @property IDSService *service;
30 @property dispatch_source_t unlockTimer;
31 @property int notifyToken;
32 @property (nonatomic, strong) OTDeviceInformationActualAdapter *deviceInfo;
33 @property OTPairingSession *session;
36 @implementation OTPairingService
38 + (instancetype)sharedService
40 static dispatch_once_t once;
41 static OTPairingService *service;
43 dispatch_once(&once, ^{
44 service = [[OTPairingService alloc] init];
54 self.queue = dispatch_queue_create("com.apple.security.otpaird", DISPATCH_QUEUE_SERIAL);
55 self.service = [[IDSService alloc] initWithService:OTPairingIDSServiceName];
56 [self.service addDelegate:self queue:self.queue];
57 self.notifyToken = NOTIFY_TOKEN_INVALID;
58 self.deviceInfo = [[OTDeviceInformationActualAdapter alloc] init];
63 - (NSString *)pairedDeviceNotificationName
65 NSString *result = nil;
66 for (IDSDevice *device in self.service.devices) {
67 if (device.isDefaultPairedDevice) {
68 result = [NSString stringWithFormat:@"ids-device-state-%@", device.uniqueIDOverride];
76 - (void)initiatePairingWithCompletion:(OTPairingCompletionHandler)completionHandler
78 dispatch_assert_queue_not(self.queue);
80 if ([self _octagonInClique]) {
81 os_log(OS_LOG_DEFAULT, "already in octagon, bailing");
82 completionHandler(false, [NSError errorWithDomain:OTPairingErrorDomain code:OTPairingErrorTypeAlreadyIn description:@"already in octagon"]);
86 dispatch_async(self.queue, ^{
87 if (self.session != nil) {
88 completionHandler(false, [NSError errorWithDomain:OTPairingErrorDomain code:OTPairingErrorTypeBusy description:@"pairing in progress"]);
92 self.session = [[OTPairingSession alloc] initWithDeviceInfo:self.deviceInfo];
93 self.session.completionHandler = completionHandler;
94 [self sendReplyToPacket];
97 #endif /* TARGET_OS_WATCH */
99 // Should be a delegate method - future refactor
100 - (void)session:(__unused OTPairingSession *)session didCompleteWithSuccess:(bool)success error:(NSError *)error
102 os_assert(self.session == session);
105 self.session.completionHandler(success, error);
106 #endif /* TARGET_OS_WATCH */
109 self.unlockTimer = nil;
114 - (void)sendReplyToPacket
116 dispatch_assert_queue(self.queue);
118 #if !TARGET_OS_SIMULATOR
119 NSDictionary *lockOptions;
120 CFErrorRef lockError = NULL;
123 (__bridge NSString *)kMKBAssertionTypeKey : (__bridge NSString *)kMKBAssertionTypeOther,
124 (__bridge NSString *)kMKBAssertionTimeoutKey : @(60),
126 self.session.lockAssertion = MKBDeviceLockAssertion((__bridge CFDictionaryRef)lockOptions, &lockError);
128 if (self.session.lockAssertion) {
129 [self stopWaitingForDeviceUnlock];
130 [self exchangePacketAndReply];
132 os_log(OS_LOG_DEFAULT, "Failed to obtain lock assertion: %@", lockError);
134 CFRelease(lockError);
136 [self waitForDeviceUnlock];
138 #else /* TARGET_OS_SIMULATOR */
139 [self exchangePacketAndReply];
140 #endif /* !TARGET_OS_SIMULATOR */
143 - (void)deviceUnlockTimedOut
145 dispatch_assert_queue(self.queue);
147 /* Should not happen; be safe. */
148 if (self.session == nil) {
153 OTPairingPacketContext *packet = self.session.packet;
154 self.session.packet = nil;
156 os_assert(packet != nil); // the acceptor always responds to a request packet, it's never initiating
158 NSError *unlockError = [NSError errorWithDomain:OTPairingErrorDomain code:OTPairingErrorTypeLock description:@"timed out waiting for companion unlock"];
159 NSMutableDictionary *message = [[NSMutableDictionary alloc] init];
160 message[OTPairingIDSKeyMessageType] = @(OTPairingIDSMessageTypeError);
161 message[OTPairingIDSKeySession] = self.session.identifier;
162 message[OTPairingIDSKeyError] = [SecXPCHelper encodedDataFromError:unlockError];
163 message[OTPairingIDSKeyErrorDeprecated] = unlockError.localizedDescription; // For older watchOS builds; remove soon
164 NSString *toID = packet.fromID;
165 NSString *responseIdentifier = packet.outgoingResponseIdentifier;
166 [self _sendMessage:message to:toID identifier:responseIdentifier];
168 [self scheduleGizmoPoke];
169 #endif /* TARGET_OS_IOS */
171 [self session:self.session didCompleteWithSuccess:false error:[NSError errorWithDomain:OTPairingErrorDomain code:OTPairingErrorTypeLock description:@"timed out waiting for unlock"]];
174 - (void)exchangePacketAndReply
176 dispatch_assert_queue(self.queue);
178 OTPairingPacketContext *packet = self.session.packet;
179 self.session.packet = nil;
181 [self.session.channel exchangePacket:packet.packetData complete:^(BOOL complete, NSData *responsePacket, NSError *channelError) {
182 /* this runs on a variety of different queues depending on the step (caller's queue, or an NSXPC queue) */
184 dispatch_async(self.queue, ^{
186 NSString *responseIdentifier;
188 os_log(OS_LOG_DEFAULT, "exchangePacket: complete=%d responsePacket=%@ channelError=%@", complete, responsePacket, channelError);
190 if (self.session == nil) {
191 os_log(OS_LOG_DEFAULT, "pairing session went away, dropping exchangePacket response");
195 if (channelError != nil) {
197 NSError *cleansedError = [SecXPCHelper cleanseErrorForXPC:channelError];
198 NSMutableDictionary *message = [[NSMutableDictionary alloc] init];
199 message[OTPairingIDSKeyMessageType] = @(OTPairingIDSMessageTypeError);
200 message[OTPairingIDSKeySession] = self.session.identifier;
201 message[OTPairingIDSKeyError] = cleansedError ? [SecXPCHelper encodedDataFromError:cleansedError] : nil;
202 message[OTPairingIDSKeyErrorDeprecated] = channelError.description;
203 os_assert(packet != nil); // the acceptor always responds to a request packet, it's never initiating
204 toID = packet.fromID;
205 responseIdentifier = packet.outgoingResponseIdentifier;
206 [self _sendMessage:message to:toID identifier:responseIdentifier];
209 [self session:self.session didCompleteWithSuccess:false error:[NSError errorWithDomain:OTPairingErrorDomain code:OTPairingErrorTypeKCPairing description:@"exchangePacket" underlying:channelError]];
214 if (responsePacket != nil) {
215 NSDictionary *message = @{
216 OTPairingIDSKeyMessageType : @(OTPairingIDSMessageTypePacket),
217 OTPairingIDSKeySession : self.session.identifier,
218 OTPairingIDSKeyPacket : responsePacket,
220 toID = packet ? packet.fromID : IDSDefaultPairedDevice;
221 responseIdentifier = packet ? packet.outgoingResponseIdentifier : nil;
222 [self _sendMessage:message to:toID identifier:responseIdentifier];
226 [self session:self.session didCompleteWithSuccess:true error:nil];
232 - (void)_sendMessage:(NSDictionary *)message to:(NSString *)toID identifier:(NSString *)responseIdentifier
234 [self _sendMessage:message to:toID identifier:responseIdentifier expectReply:YES];
237 - (void)_sendMessage:(NSDictionary *)message to:(NSString *)toID identifier:(NSString *)responseIdentifier expectReply:(BOOL)expectReply
239 dispatch_assert_queue(self.queue);
242 NSMutableDictionary *options;
243 NSString *identifier = nil;
244 NSError *error = nil;
245 BOOL sendResult = NO;
247 destinations = [NSSet setWithObject:toID];
249 options = [NSMutableDictionary new];
250 options[IDSSendMessageOptionForceLocalDeliveryKey] = @YES;
251 options[IDSSendMessageOptionExpectsPeerResponseKey] = @(expectReply); // TODO: when are we complete??
252 if (responseIdentifier != nil) {
253 options[IDSSendMessageOptionPeerResponseIdentifierKey] = responseIdentifier;
256 sendResult = [self.service sendMessage:message
257 toDestinations:destinations
258 priority:IDSMessagePriorityDefault
260 identifier:&identifier
263 self.session.sentMessageIdentifier = identifier;
265 os_log(OS_LOG_DEFAULT, "send message failed (%@): %@", identifier, error);
266 // On iOS, do nothing; watch will time out waiting for response.
267 [self session:self.session didCompleteWithSuccess:false error:[NSError errorWithDomain:OTPairingErrorDomain code:OTPairingErrorTypeIDS description:@"IDS message send failure" underlying:error]];
271 #pragma mark IDSServiceDelegate methods
273 - (void)service:(IDSService *)service account:(__unused IDSAccount *)account incomingMessage:(NSDictionary *)message fromID:(NSString *)fromID context:(IDSMessageContext *)context
275 dispatch_assert_queue(self.queue);
277 OTPairingPacketContext *packet = nil;
278 bool validateIdentifier = true;
280 os_log(OS_LOG_DEFAULT, "IDS message from %@: %@", fromID, message);
282 packet = [[OTPairingPacketContext alloc] initWithMessage:message fromID:fromID context:context];
284 if (packet.messageType == OTPairingIDSMessageTypePoke) {
286 // on self.queue now, but initiatePairingWithCompletion: must _not_ be on self.queue
287 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
288 os_log(OS_LOG_DEFAULT, "companion claims to be unlocked, retrying");
289 [self initiatePairingWithCompletion:^(bool success, NSError *error) {
291 os_log(OS_LOG_DEFAULT, "companion-unlocked retry succeeded");
293 os_log(OS_LOG_DEFAULT, "companion-unlocked retry failed: %@", error);
297 #endif /* TARGET_OS_WATCH */
302 * Check for missing/invalid session identifier. Since the watch is the initiator,
303 * iOS responds to an invalid session identifier by resetting state and starting over.
305 if (packet.sessionIdentifier == nil) {
306 os_log(OS_LOG_DEFAULT, "ignoring message with no session identifier (old build?)");
308 } else if (![packet.sessionIdentifier isEqualToString:self.session.identifier]) {
310 os_log(OS_LOG_DEFAULT, "unknown session identifier, dropping message");
313 os_log(OS_LOG_DEFAULT, "unknown session identifier %@, creating new session object", packet.sessionIdentifier);
314 self.session = [[OTPairingSession alloc] initWithDeviceInfo:self.deviceInfo identifier:packet.sessionIdentifier];
315 validateIdentifier = false;
316 #endif /* TARGET_OS_IOS */
319 if (validateIdentifier && ![self.session.sentMessageIdentifier isEqualToString:packet.incomingResponseIdentifier]) {
320 os_log(OS_LOG_DEFAULT, "ignoring message with unrecognized incomingResponseIdentifier");
324 switch (packet.messageType) {
325 case OTPairingIDSMessageTypeError:
326 [self session:self.session didCompleteWithSuccess:false error:[NSError errorWithDomain:OTPairingErrorDomain code:OTPairingErrorTypeRemote description:@"companion error" underlying:packet.error]];
328 case OTPairingIDSMessageTypePacket:
329 self.session.packet = packet;
330 [self sendReplyToPacket];
332 case OTPairingIDSMessageTypePoke:
338 - (void)service:(__unused IDSService *)service account:(__unused IDSAccount *)account identifier:(NSString *)identifier didSendWithSuccess:(BOOL)success error:(NSError *)error context:(__unused IDSMessageContext *)context
340 dispatch_assert_queue(self.queue);
342 /* Only accept callback if it is for the recently-sent message. */
343 if (![self.session.sentMessageIdentifier isEqualToString:identifier]) {
344 os_log(OS_LOG_DEFAULT, "ignoring didSendWithSuccess callback for unexpected identifier: %@", identifier);
349 os_log(OS_LOG_DEFAULT, "unsuccessfully sent message (%@): %@", identifier, error);
350 // On iOS, do nothing; watch will time out waiting for response.
351 [self session:self.session didCompleteWithSuccess:false error:[NSError errorWithDomain:OTPairingErrorDomain code:OTPairingErrorTypeIDS description:@"IDS message failed to send" underlying:error]];
355 #pragma mark lock state handling
357 #if !TARGET_OS_SIMULATOR
358 - (void)waitForDeviceUnlock
360 dispatch_assert_queue(self.queue);
362 static dispatch_once_t once;
363 static dispatch_source_t lockStateCoalescingSource;
364 uint32_t notify_status;
367 dispatch_once(&once, ^{
368 // Everything here is on one queue, so we sometimes get several notifications while busy doing something else.
369 lockStateCoalescingSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_OR, 0, 0, self.queue);
370 dispatch_source_set_event_handler(lockStateCoalescingSource, ^{
371 [self sendReplyToPacket];
373 dispatch_activate(lockStateCoalescingSource);
376 if (self.notifyToken == NOTIFY_TOKEN_INVALID) {
377 notify_status = notify_register_dispatch(kMobileKeyBagLockStatusNotificationID, &token, self.queue, ^(__unused int t) {
378 dispatch_source_merge_data(lockStateCoalescingSource, 1);
380 if (os_assumes_zero(notify_status) == NOTIFY_STATUS_OK) {
381 self.notifyToken = token;
385 self.unlockTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.queue);
386 dispatch_source_set_timer(self.unlockTimer, dispatch_time(DISPATCH_TIME_NOW, WAIT_FOR_UNLOCK_DURATION * NSEC_PER_SEC), DISPATCH_TIME_FOREVER, 0);
387 dispatch_source_set_event_handler(self.unlockTimer, ^{
388 [self stopWaitingForDeviceUnlock];
389 [self deviceUnlockTimedOut];
391 dispatch_activate(self.unlockTimer);
393 // double-check to prevent race condition, try again soon
394 if (MKBGetDeviceLockState(NULL) == kMobileKeyBagDeviceIsUnlocked) {
395 [self stopWaitingForDeviceUnlock];
396 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 5ull * NSEC_PER_SEC), self.queue, ^{
397 [self sendReplyToPacket];
402 - (void)stopWaitingForDeviceUnlock
404 dispatch_assert_queue(self.queue);
406 uint32_t notify_status;
408 if (self.notifyToken != NOTIFY_TOKEN_INVALID) {
409 notify_status = notify_cancel(self.notifyToken);
410 os_assumes_zero(notify_status);
411 self.notifyToken = NOTIFY_TOKEN_INVALID;
414 if (self.unlockTimer != nil) {
415 dispatch_source_cancel(self.unlockTimer);
416 self.unlockTimer = nil;
419 #endif /* !TARGET_OS_SIMULATOR */
422 - (void)scheduleGizmoPoke
424 xpc_object_t criteria;
426 criteria = xpc_dictionary_create(NULL, NULL, 0);
427 xpc_dictionary_set_string(criteria, XPC_ACTIVITY_PRIORITY, XPC_ACTIVITY_PRIORITY_MAINTENANCE);
428 xpc_dictionary_set_int64(criteria, XPC_ACTIVITY_DELAY, 0ll);
429 xpc_dictionary_set_int64(criteria, XPC_ACTIVITY_GRACE_PERIOD, 0ll);
430 xpc_dictionary_set_bool(criteria, XPC_ACTIVITY_REPEATING, false);
431 xpc_dictionary_set_bool(criteria, XPC_ACTIVITY_ALLOW_BATTERY, true);
432 xpc_dictionary_set_bool(criteria, XPC_ACTIVITY_REQUIRES_CLASS_A, true);
433 xpc_dictionary_set_bool(criteria, XPC_ACTIVITY_COMMUNICATES_WITH_PAIRED_DEVICE, true);
435 os_log(OS_LOG_DEFAULT, "scheduling XPC Activity to inform gizmo of companion unlock");
436 xpc_activity_register(OTPairingXPCActivityPoke, criteria, ^(xpc_activity_t activity) {
437 xpc_activity_state_t state = xpc_activity_get_state(activity);
438 if (state == XPC_ACTIVITY_STATE_RUN) {
439 dispatch_sync(self.queue, ^{
440 os_log(OS_LOG_DEFAULT, "poking gizmo now");
441 NSDictionary *message = @{
442 OTPairingIDSKeyMessageType : @(OTPairingIDSMessageTypePoke),
444 [self _sendMessage:message to:IDSDefaultPairedDevice identifier:nil expectReply:NO];
449 #endif /* TARGET_OS_IOS */
451 #pragma mark Octagon Clique Status
454 - (bool)_octagonInClique
456 __block bool result = false;
457 NSError *ctlError = nil;
459 OTControl *ctl = [OTControl controlObject:true error:&ctlError];
461 OTOperationConfiguration *config = [OTOperationConfiguration new];
462 config.useCachedAccountStatus = true;
463 [ctl fetchCliqueStatus:nil context:OTDefaultContext configuration:config reply:^(CliqueStatus cliqueStatus, NSError * _Nullable error) {
464 result = (cliqueStatus == CliqueStatusIn);
467 os_log(OS_LOG_DEFAULT, "failed to acquire OTControl: %@", ctlError);
472 #endif /* TARGET_OS_WATCH */