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