]> git.saurik.com Git - apple/security.git/blob - keychain/otpaird/OTPairingService.m
Security-59306.41.2.tar.gz
[apple/security.git] / keychain / otpaird / OTPairingService.m
1 #import <TargetConditionals.h>
2 #import <Foundation/Foundation.h>
3 #import <IDS/IDS.h>
4 #import <KeychainCircle/KeychainCircle.h>
5 #import <os/assumes.h>
6 #import <Security/SecXPCHelper.h>
7 #import <xpc/private.h>
8
9 #if TARGET_OS_WATCH
10 #import <Security/OTControl.h>
11 #endif /* TARGET_OS_WATCH */
12
13 #if !TARGET_OS_SIMULATOR
14 #import <MobileKeyBag/MobileKeyBag.h>
15 #endif /* !TARGET_OS_SIMULATOR */
16
17 #import "OTPairingService.h"
18 #import "OTPairingPacketContext.h"
19 #import "OTPairingSession.h"
20 #import "OTPairingConstants.h"
21
22 #import "keychain/categories/NSError+UsefulConstructors.h"
23 #import "keychain/ot/OTDeviceInformationAdapter.h"
24
25 #define WAIT_FOR_UNLOCK_DURATION (120ull)
26
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;
34 @end
35
36 @implementation OTPairingService
37
38 + (instancetype)sharedService
39 {
40 static dispatch_once_t once;
41 static OTPairingService *service;
42
43 dispatch_once(&once, ^{
44 service = [[OTPairingService alloc] init];
45 });
46
47 return service;
48 }
49
50 - (instancetype)init
51 {
52 self = [super init];
53 if (self != nil) {
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];
59 }
60 return self;
61 }
62
63 - (NSString *)pairedDeviceNotificationName
64 {
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];
69 break;
70 }
71 }
72 return result;
73 }
74
75 #if TARGET_OS_WATCH
76 - (void)initiatePairingWithCompletion:(OTPairingCompletionHandler)completionHandler
77 {
78 dispatch_assert_queue_not(self.queue);
79
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"]);
83 return;
84 }
85
86 dispatch_async(self.queue, ^{
87 if (self.session != nil) {
88 completionHandler(false, [NSError errorWithDomain:OTPairingErrorDomain code:OTPairingErrorTypeBusy description:@"pairing in progress"]);
89 return;
90 }
91
92 self.session = [[OTPairingSession alloc] initWithDeviceInfo:self.deviceInfo];
93 self.session.completionHandler = completionHandler;
94 [self sendReplyToPacket];
95 });
96 }
97 #endif /* TARGET_OS_WATCH */
98
99 // Should be a delegate method - future refactor
100 - (void)session:(__unused OTPairingSession *)session didCompleteWithSuccess:(bool)success error:(NSError *)error
101 {
102 os_assert(self.session == session);
103
104 #if TARGET_OS_WATCH
105 self.session.completionHandler(success, error);
106 #endif /* TARGET_OS_WATCH */
107
108 self.session = nil;
109 self.unlockTimer = nil;
110 }
111
112 #pragma mark -
113
114 - (void)sendReplyToPacket
115 {
116 dispatch_assert_queue(self.queue);
117
118 #if !TARGET_OS_SIMULATOR
119 NSDictionary *lockOptions;
120 CFErrorRef lockError = NULL;
121
122 lockOptions = @{
123 (__bridge NSString *)kMKBAssertionTypeKey : (__bridge NSString *)kMKBAssertionTypeOther,
124 (__bridge NSString *)kMKBAssertionTimeoutKey : @(60),
125 };
126 self.session.lockAssertion = MKBDeviceLockAssertion((__bridge CFDictionaryRef)lockOptions, &lockError);
127
128 if (self.session.lockAssertion) {
129 [self stopWaitingForDeviceUnlock];
130 [self exchangePacketAndReply];
131 } else {
132 os_log(OS_LOG_DEFAULT, "Failed to obtain lock assertion: %@", lockError);
133 if (lockError) {
134 CFRelease(lockError);
135 }
136 [self waitForDeviceUnlock];
137 }
138 #else /* TARGET_OS_SIMULATOR */
139 [self exchangePacketAndReply];
140 #endif /* !TARGET_OS_SIMULATOR */
141 }
142
143 - (void)deviceUnlockTimedOut
144 {
145 dispatch_assert_queue(self.queue);
146
147 /* Should not happen; be safe. */
148 if (self.session == nil) {
149 return;
150 }
151
152 #if TARGET_OS_IOS
153 OTPairingPacketContext *packet = self.session.packet;
154 self.session.packet = nil;
155
156 os_assert(packet != nil); // the acceptor always responds to a request packet, it's never initiating
157
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];
167
168 [self scheduleGizmoPoke];
169 #endif /* TARGET_OS_IOS */
170
171 [self session:self.session didCompleteWithSuccess:false error:[NSError errorWithDomain:OTPairingErrorDomain code:OTPairingErrorTypeLock description:@"timed out waiting for unlock"]];
172 }
173
174 - (void)exchangePacketAndReply
175 {
176 dispatch_assert_queue(self.queue);
177
178 OTPairingPacketContext *packet = self.session.packet;
179 self.session.packet = nil;
180
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) */
183
184 dispatch_async(self.queue, ^{
185 NSString *toID;
186 NSString *responseIdentifier;
187
188 os_log(OS_LOG_DEFAULT, "exchangePacket: complete=%d responsePacket=%@ channelError=%@", complete, responsePacket, channelError);
189
190 if (self.session == nil) {
191 os_log(OS_LOG_DEFAULT, "pairing session went away, dropping exchangePacket response");
192 return;
193 }
194
195 if (channelError != nil) {
196 #if TARGET_OS_IOS
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];
207 #endif
208
209 [self session:self.session didCompleteWithSuccess:false error:[NSError errorWithDomain:OTPairingErrorDomain code:OTPairingErrorTypeKCPairing description:@"exchangePacket" underlying:channelError]];
210
211 return;
212 }
213
214 if (responsePacket != nil) {
215 NSDictionary *message = @{
216 OTPairingIDSKeyMessageType : @(OTPairingIDSMessageTypePacket),
217 OTPairingIDSKeySession : self.session.identifier,
218 OTPairingIDSKeyPacket : responsePacket,
219 };
220 toID = packet ? packet.fromID : IDSDefaultPairedDevice;
221 responseIdentifier = packet ? packet.outgoingResponseIdentifier : nil;
222 [self _sendMessage:message to:toID identifier:responseIdentifier];
223 }
224
225 if (complete) {
226 [self session:self.session didCompleteWithSuccess:true error:nil];
227 }
228 });
229 }];
230 }
231
232 - (void)_sendMessage:(NSDictionary *)message to:(NSString *)toID identifier:(NSString *)responseIdentifier
233 {
234 [self _sendMessage:message to:toID identifier:responseIdentifier expectReply:YES];
235 }
236
237 - (void)_sendMessage:(NSDictionary *)message to:(NSString *)toID identifier:(NSString *)responseIdentifier expectReply:(BOOL)expectReply
238 {
239 dispatch_assert_queue(self.queue);
240
241 NSSet *destinations;
242 NSMutableDictionary *options;
243 NSString *identifier = nil;
244 NSError *error = nil;
245 BOOL sendResult = NO;
246
247 destinations = [NSSet setWithObject:toID];
248
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;
254 }
255
256 sendResult = [self.service sendMessage:message
257 toDestinations:destinations
258 priority:IDSMessagePriorityDefault
259 options:options
260 identifier:&identifier
261 error:&error];
262 if (sendResult) {
263 self.session.sentMessageIdentifier = identifier;
264 } else {
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]];
268 }
269 }
270
271 #pragma mark IDSServiceDelegate methods
272
273 - (void)service:(IDSService *)service account:(__unused IDSAccount *)account incomingMessage:(NSDictionary *)message fromID:(NSString *)fromID context:(IDSMessageContext *)context
274 {
275 dispatch_assert_queue(self.queue);
276
277 OTPairingPacketContext *packet = nil;
278 bool validateIdentifier = true;
279
280 os_log(OS_LOG_DEFAULT, "IDS message from %@: %@", fromID, message);
281
282 packet = [[OTPairingPacketContext alloc] initWithMessage:message fromID:fromID context:context];
283
284 if (packet.messageType == OTPairingIDSMessageTypePoke) {
285 #if TARGET_OS_WATCH
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) {
290 if (success) {
291 os_log(OS_LOG_DEFAULT, "companion-unlocked retry succeeded");
292 } else {
293 os_log(OS_LOG_DEFAULT, "companion-unlocked retry failed: %@", error);
294 }
295 }];
296 });
297 #endif /* TARGET_OS_WATCH */
298 return;
299 }
300
301 /*
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.
304 */
305 if (packet.sessionIdentifier == nil) {
306 os_log(OS_LOG_DEFAULT, "ignoring message with no session identifier (old build?)");
307 return;
308 } else if (![packet.sessionIdentifier isEqualToString:self.session.identifier]) {
309 #if TARGET_OS_WATCH
310 os_log(OS_LOG_DEFAULT, "unknown session identifier, dropping message");
311 return;
312 #elif TARGET_OS_IOS
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 */
317 }
318
319 if (validateIdentifier && ![self.session.sentMessageIdentifier isEqualToString:packet.incomingResponseIdentifier]) {
320 os_log(OS_LOG_DEFAULT, "ignoring message with unrecognized incomingResponseIdentifier");
321 return;
322 }
323
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]];
327 break;
328 case OTPairingIDSMessageTypePacket:
329 self.session.packet = packet;
330 [self sendReplyToPacket];
331 break;
332 case OTPairingIDSMessageTypePoke:
333 /*impossible*/
334 break;
335 }
336 }
337
338 - (void)service:(__unused IDSService *)service account:(__unused IDSAccount *)account identifier:(NSString *)identifier didSendWithSuccess:(BOOL)success error:(NSError *)error context:(__unused IDSMessageContext *)context
339 {
340 dispatch_assert_queue(self.queue);
341
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);
345 return;
346 }
347
348 if (!success) {
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]];
352 }
353 }
354
355 #pragma mark lock state handling
356
357 #if !TARGET_OS_SIMULATOR
358 - (void)waitForDeviceUnlock
359 {
360 dispatch_assert_queue(self.queue);
361
362 static dispatch_once_t once;
363 static dispatch_source_t lockStateCoalescingSource;
364 uint32_t notify_status;
365 int token;
366
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];
372 });
373 dispatch_activate(lockStateCoalescingSource);
374 });
375
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);
379 });
380 if (os_assumes_zero(notify_status) == NOTIFY_STATUS_OK) {
381 self.notifyToken = token;
382 }
383 }
384
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];
390 });
391 dispatch_activate(self.unlockTimer);
392
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];
398 });
399 }
400 }
401
402 - (void)stopWaitingForDeviceUnlock
403 {
404 dispatch_assert_queue(self.queue);
405
406 uint32_t notify_status;
407
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;
412 }
413
414 if (self.unlockTimer != nil) {
415 dispatch_source_cancel(self.unlockTimer);
416 self.unlockTimer = nil;
417 }
418 }
419 #endif /* !TARGET_OS_SIMULATOR */
420
421 #if TARGET_OS_IOS
422 - (void)scheduleGizmoPoke
423 {
424 xpc_object_t criteria;
425
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);
434
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),
443 };
444 [self _sendMessage:message to:IDSDefaultPairedDevice identifier:nil expectReply:NO];
445 });
446 }
447 });
448 }
449 #endif /* TARGET_OS_IOS */
450
451 #pragma mark Octagon Clique Status
452
453 #if TARGET_OS_WATCH
454 - (bool)_octagonInClique
455 {
456 __block bool result = false;
457 NSError *ctlError = nil;
458
459 OTControl *ctl = [OTControl controlObject:true error:&ctlError];
460 if (ctl != nil) {
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);
465 }];
466 } else {
467 os_log(OS_LOG_DEFAULT, "failed to acquire OTControl: %@", ctlError);
468 }
469
470 return result;
471 }
472 #endif /* TARGET_OS_WATCH */
473
474 @end