]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSCKAccountStateTracker.m
Security-58286.230.21.tar.gz
[apple/security.git] / keychain / ckks / CKKSCKAccountStateTracker.m
1 /*
2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
11 * file.
12 *
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 #if OCTAGON
25
26 #include <dispatch/dispatch.h>
27 #include <utilities/debugging.h>
28 #include <Security/SecureObjectSync/SOSCloudCircle.h>
29 #include <Security/SecureObjectSync/SOSCloudCircleInternal.h>
30 #include <Security/SecureObjectSync/SOSInternal.h>
31 #include <notify.h>
32
33 #import "keychain/ckks/CKKS.h"
34 #import "keychain/ckks/CloudKitCategories.h"
35 #import "keychain/ckks/CKKSCKAccountStateTracker.h"
36 #import "keychain/ckks/CKKSAnalytics.h"
37 #import "keychain/categories/NSError+UsefulConstructors.h"
38
39 @interface CKKSCKAccountStateTracker ()
40 @property (readonly) Class<CKKSNSNotificationCenter> nsnotificationCenterClass;
41
42 @property CKKSAccountStatus currentComputedAccountStatus;
43 @property (nullable, atomic) NSError* currentAccountError;
44
45 @property dispatch_queue_t queue;
46
47 @property NSMapTable<dispatch_queue_t, id<CKKSAccountStateListener>>* changeListeners;
48 @property CKContainer* container; // used only for fetching the CKAccountStatus
49
50 /* We have initialization races. We should report CKKSAccountStatusUnknown until both of
51 * these are true, otherwise on a race, it looks like we logged out. */
52 @property bool firstCKAccountFetch;
53 @property bool firstSOSCircleFetch;
54 @end
55
56 @implementation CKKSCKAccountStateTracker
57
58 -(instancetype)init: (CKContainer*) container nsnotificationCenterClass: (Class<CKKSNSNotificationCenter>) nsnotificationCenterClass {
59 if((self = [super init])) {
60 _nsnotificationCenterClass = nsnotificationCenterClass;
61 _changeListeners = [NSMapTable strongToWeakObjectsMapTable]; // Backwards from how we'd like, but it's the best way to have weak pointers to CKKSAccountStateListener.
62 _currentCKAccountInfo = nil;
63 _currentCircleStatus = [[SOSAccountStatus alloc] init:kSOSCCError error:nil];
64
65 _currentComputedAccountStatus = CKKSAccountStatusUnknown;
66 _currentComputedAccountStatusValid = [[CKKSCondition alloc] init];
67
68 _container = container;
69
70 _queue = dispatch_queue_create("ck-account-state", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
71
72 _firstCKAccountFetch = false;
73 _firstSOSCircleFetch = false;
74
75 _finishedInitialDispatches = [[CKKSCondition alloc] init];
76 _ckdeviceIDInitialized = [[CKKSCondition alloc] init];
77
78 id<CKKSNSNotificationCenter> notificationCenter = [self.nsnotificationCenterClass defaultCenter];
79 secinfo("ckksaccount", "Registering with notification center %@", notificationCenter);
80 [notificationCenter addObserver:self selector:@selector(notifyCKAccountStatusChange:) name:CKAccountChangedNotification object:NULL];
81
82 __weak __typeof(self) weakSelf = self;
83
84 // If this is a live server, register with notify
85 if(!SecCKKSTestsEnabled()) {
86 int token = 0;
87 notify_register_dispatch(kSOSCCCircleChangedNotification, &token, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^(int t) {
88 [weakSelf notifyCircleChange:nil];
89 });
90 }
91
92 // Fire off a fetch of the account status. Do not go on our local queue, because notifyCKAccountStatusChange will attempt to go back on it for thread-safety.
93 dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
94 __strong __typeof(self) strongSelf = weakSelf;
95 if(!strongSelf) {
96 return;
97 }
98 @autoreleasepool {
99 [strongSelf notifyCKAccountStatusChange:nil];
100 [strongSelf notifyCircleChange:nil];
101 [strongSelf.finishedInitialDispatches fulfill];
102 }
103 });
104 }
105 return self;
106 }
107
108 -(void)dealloc {
109 id<CKKSNSNotificationCenter> notificationCenter = [self.nsnotificationCenterClass defaultCenter];
110 [notificationCenter removeObserver:self];
111 }
112
113 -(NSString*)descriptionInternal: (NSString*) selfString {
114 return [NSString stringWithFormat:@"<%@: %@ (%@ %@) %@>",
115 selfString,
116 [self currentStatus],
117 self.currentCKAccountInfo,
118 self.currentCircleStatus,
119 self.currentAccountError ?: @""];
120 }
121
122 -(NSString*)description {
123 return [self descriptionInternal: [[self class] description]];
124 }
125
126 -(NSString*)debugDescription {
127 return [self descriptionInternal: [super description]];
128 }
129
130 -(dispatch_semaphore_t)notifyOnAccountStatusChange:(id<CKKSAccountStateListener>)listener {
131 // signals when we've successfully delivered the first account status
132 dispatch_semaphore_t finishedSema = dispatch_semaphore_create(0);
133
134 dispatch_async(self.queue, ^{
135 bool alreadyRegisteredListener = false;
136 NSEnumerator *enumerator = [self.changeListeners objectEnumerator];
137 id<CKKSAccountStateListener> value;
138
139 while ((value = [enumerator nextObject])) {
140 // do pointer comparison
141 alreadyRegisteredListener |= (value == listener);
142 }
143
144 if(listener && !alreadyRegisteredListener) {
145 NSString* queueName = [NSString stringWithFormat: @"ck-account-state-%@", listener];
146
147 dispatch_queue_t objQueue = dispatch_queue_create([queueName UTF8String], DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
148 [self.changeListeners setObject: listener forKey: objQueue];
149
150 secinfo("ckksaccount", "adding a new listener: %@", listener);
151
152 // If we know the current account status, let this listener know
153 if(self.currentComputedAccountStatus != CKKSAccountStatusUnknown) {
154 secinfo("ckksaccount", "notifying new listener %@ of current state %d", listener, (int)self.currentComputedAccountStatus);
155
156 dispatch_group_t g = dispatch_group_create();
157 if(!g) {
158 secnotice("ckksaccount", "Unable to get dispatch group.");
159 return;
160 }
161
162 [self _onqueueDeliverCurrentState:listener listenerQueue:objQueue oldStatus:CKKSAccountStatusUnknown group:g];
163
164 dispatch_group_notify(g, self.queue, ^{
165 dispatch_semaphore_signal(finishedSema);
166 });
167 } else {
168 dispatch_semaphore_signal(finishedSema);
169 }
170 } else {
171 dispatch_semaphore_signal(finishedSema);
172 }
173 });
174
175 return finishedSema;
176 }
177
178 - (dispatch_semaphore_t)notifyCKAccountStatusChange:(__unused id)object {
179 // signals when this notify is Complete, including all downcalls.
180 dispatch_semaphore_t finishedSema = dispatch_semaphore_create(0);
181
182 [self.container accountInfoWithCompletionHandler:^(CKAccountInfo* ckAccountInfo, NSError * _Nullable error) {
183 if(error) {
184 secerror("ckksaccount: error getting account info: %@", error);
185 dispatch_semaphore_signal(finishedSema);
186 return;
187 }
188
189 dispatch_sync(self.queue, ^{
190 self.firstCKAccountFetch = true;
191 secnotice("ckksaccount", "received CK Account info: %@", ckAccountInfo);
192 [self _onqueueUpdateAccountState:ckAccountInfo circle:self.currentCircleStatus deliveredSemaphore:finishedSema];
193 });
194 }];
195
196 return finishedSema;
197 }
198
199 -(dispatch_semaphore_t)notifyCircleChange:(__unused id)object {
200 dispatch_semaphore_t finishedSema = dispatch_semaphore_create(0);
201
202 SOSAccountStatus* circleStatus = [CKKSCKAccountStateTracker getCircleStatus];
203 dispatch_sync(self.queue, ^{
204 self.firstSOSCircleFetch = true;
205
206 [self _onqueueUpdateAccountState:self.currentCKAccountInfo circle:circleStatus deliveredSemaphore:finishedSema];
207 });
208 return finishedSema;
209 }
210
211 // Takes the new ckAccountInfo we're moving to
212 -(void)_onqueueUpdateCKDeviceID: (CKAccountInfo*)ckAccountInfo {
213 dispatch_assert_queue(self.queue);
214 __weak __typeof(self) weakSelf = self;
215
216 // If we're in an account, opportunistically fill in the device id
217 if(ckAccountInfo.accountStatus == CKAccountStatusAvailable) {
218 [self.container fetchCurrentDeviceIDWithCompletionHandler:^(NSString* deviceID, NSError* ckerror) {
219 __strong __typeof(self) strongSelf = weakSelf;
220 if(!strongSelf) {
221 secerror("ckksaccount: Received fetchCurrentDeviceIDWithCompletionHandler callback with null AccountStateTracker");
222 return;
223 }
224
225 // Make sure you synchronize here; if we've logged out before the callback returns, don't record the result
226 dispatch_async(strongSelf.queue, ^{
227 __strong __typeof(self) innerStrongSelf = weakSelf;
228 if(innerStrongSelf.currentCKAccountInfo.accountStatus == CKAccountStatusAvailable) {
229 secnotice("ckksaccount", "CloudKit deviceID is: %@ %@", deviceID, ckerror);
230
231 innerStrongSelf.ckdeviceID = deviceID;
232 innerStrongSelf.ckdeviceIDError = ckerror;
233 [innerStrongSelf.ckdeviceIDInitialized fulfill];
234 } else {
235 // Logged out! No ckdeviceid.
236 secerror("ckksaccount: Logged back out but still received a fetchCurrentDeviceIDWithCompletionHandler callback");
237
238 innerStrongSelf.ckdeviceID = nil;
239 innerStrongSelf.ckdeviceIDError = nil;
240 // Don't touch the ckdeviceIDInitialized object; it should have been reset when the logout happened.
241 }
242 });
243 }];
244 } else {
245 // Logging out? no more device ID.
246 self.ckdeviceID = nil;
247 self.ckdeviceIDError = nil;
248 self.ckdeviceIDInitialized = [[CKKSCondition alloc] init];
249 }
250 }
251
252 // Pulled out for mocking purposes
253 +(void)fetchCirclePeerID:(void (^)(NSString* _Nullable peerID, NSError* _Nullable error))callback {
254 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
255 CFErrorRef cferror = nil;
256 SOSPeerInfoRef egoPeerInfo = SOSCCCopyMyPeerInfo(&cferror);
257 NSString* egoPeerID = egoPeerInfo ? (NSString*)CFBridgingRelease(CFRetainSafe(SOSPeerInfoGetPeerID(egoPeerInfo))) : nil;
258 CFReleaseNull(egoPeerInfo);
259
260 callback(egoPeerID, CFBridgingRelease(cferror));
261 });
262 }
263
264 // Takes the new ckAccountInfo we're moving to
265 -(void)_onqueueUpdateCirclePeerID: (SOSAccountStatus*)sosstatus {
266 dispatch_assert_queue(self.queue);
267 __weak __typeof(self) weakSelf = self;
268
269 // If we're in a circle, fetch the peer id
270 if(sosstatus.status == kSOSCCInCircle) {
271 [CKKSCKAccountStateTracker fetchCirclePeerID:^(NSString* peerID, NSError* error) {
272 __strong __typeof(self) strongSelf = weakSelf;
273 if(!strongSelf) {
274 secerror("ckksaccount: Received fetchCirclePeerID callback with null AccountStateTracker");
275 return;
276 }
277
278 dispatch_async(strongSelf.queue, ^{
279 __strong __typeof(self) innerstrongSelf = weakSelf;
280
281 if(innerstrongSelf.currentCircleStatus && innerstrongSelf.currentCircleStatus.status == kSOSCCInCircle) {
282 secnotice("ckksaccount", "Circle peerID is: %@ %@", peerID, error);
283 // Still in circle. Proceed.
284 innerstrongSelf.accountCirclePeerID = peerID;
285 innerstrongSelf.accountCirclePeerIDError = error;
286 [innerstrongSelf.accountCirclePeerIDInitialized fulfill];
287 } else {
288 secerror("ckksaccount: Out of circle but still received a fetchCirclePeerID callback");
289 // Not in-circle. Throw away circle id.
290 strongSelf.accountCirclePeerID = nil;
291 strongSelf.accountCirclePeerIDError = nil;
292 // Don't touch the accountCirclePeerIDInitialized object; it should have been reset when the logout happened.
293 }
294 });
295 }];
296 } else {
297 // Not in-circle, reset circle ID
298 secnotice("ckksaccount", "out of circle(%@): resetting peer ID", sosstatus);
299 self.accountCirclePeerID = nil;
300 self.accountCirclePeerIDError = nil;
301 self.accountCirclePeerIDInitialized = [[CKKSCondition alloc] init];
302 }
303 }
304
305 - (bool)_onqueueDetermineLoggedIn:(NSError**)error {
306 // We are logged in if we are:
307 // in CKAccountStatusAvailable
308 // and supportsDeviceToDeviceEncryption == true
309 // and the iCloud account is not in grey mode
310 // and in circle
311 dispatch_assert_queue(self.queue);
312 if(self.currentCKAccountInfo) {
313 if(self.currentCKAccountInfo.accountStatus != CKAccountStatusAvailable) {
314 if(error) {
315 *error = [NSError errorWithDomain:CKKSErrorDomain
316 code:CKKSNotLoggedIn
317 description:@"iCloud account is logged out"];
318 }
319 return false;
320 } else if(!self.currentCKAccountInfo.supportsDeviceToDeviceEncryption) {
321 if(error) {
322 *error = [NSError errorWithDomain:CKKSErrorDomain
323 code:CKKSNotHSA2
324 description:@"iCloud account is not HSA2"];
325 }
326 return false;
327 } else if(!self.currentCKAccountInfo.hasValidCredentials) {
328 if(error) {
329 *error = [NSError errorWithDomain:CKKSErrorDomain
330 code:CKKSiCloudGreyMode
331 description:@"iCloud account is in grey mode"];
332 }
333 return false;
334 }
335 } else {
336 if(error) {
337 *error = [NSError errorWithDomain:CKKSErrorDomain
338 code:CKKSNotLoggedIn
339 description:@"No current iCloud account status"];
340 }
341 return false;
342 }
343
344 if(self.currentCircleStatus.status != kSOSCCInCircle) {
345 if(error) {
346 *error = [NSError errorWithDomain:(__bridge NSString*)kSOSErrorDomain
347 code:kSOSErrorNotInCircle
348 description:@"Not in circle"];
349 }
350 return false;
351 }
352
353 return true;
354 }
355
356 -(void)_onqueueUpdateAccountState:(CKAccountInfo*)ckAccountInfo circle:(SOSAccountStatus*)sosstatus deliveredSemaphore:(dispatch_semaphore_t)finishedSema {
357 dispatch_assert_queue(self.queue);
358
359 if([self.currentCKAccountInfo isEqual: ckAccountInfo] && self.currentCircleStatus.status == sosstatus.status) {
360 // no-op.
361 secinfo("ckksaccount", "received another notification of CK Account State %@ and Circle status %d", ckAccountInfo, (int)sosstatus);
362 dispatch_semaphore_signal(finishedSema);
363 return;
364 }
365
366 if((self.currentCKAccountInfo == nil && ckAccountInfo != nil) ||
367 ![self.currentCKAccountInfo isEqual: ckAccountInfo]) {
368 secnotice("ckksaccount", "moving to CK Account info: %@", ckAccountInfo);
369 self.currentCKAccountInfo = ckAccountInfo;
370
371 [self _onqueueUpdateCKDeviceID: ckAccountInfo];
372 }
373 if(![self.currentCircleStatus isEqual:sosstatus]) {
374 secnotice("ckksaccount", "moving to circle status: %@", sosstatus);
375 self.currentCircleStatus = sosstatus;
376 if (sosstatus.status == kSOSCCInCircle) {
377 [[CKKSAnalytics logger] setDateProperty:[NSDate date] forKey:CKKSAnalyticsLastInCircle];
378 }
379 [self _onqueueUpdateCirclePeerID: sosstatus];
380 }
381
382 if(!self.firstSOSCircleFetch || !self.firstCKAccountFetch) {
383 secnotice("ckksaccount", "Haven't received updates from all sources; not passing update along: %@", self);
384 dispatch_semaphore_signal(finishedSema);
385 return;
386 }
387
388 if(self.currentCircleStatus.status == kSOSCCError &&
389 [self.currentCircleStatus.error.domain isEqualToString:(__bridge id)kSOSErrorDomain] &&
390 self.currentCircleStatus.error.code == kSOSErrorNotReady &&
391 self.currentComputedAccountStatus == CKKSAccountStatusUnknown) {
392 secnotice("ckksaccount", "Device not unlocked yet; can't determine account status. Not passing update along: %@", self);
393 dispatch_semaphore_signal(finishedSema);
394 return;
395 }
396
397 CKKSAccountStatus oldComputedStatus = self.currentComputedAccountStatus;
398
399 NSError* error = nil;
400 if([self _onqueueDetermineLoggedIn:&error]) {
401 self.currentComputedAccountStatus = CKKSAccountStatusAvailable;
402 self.currentAccountError = nil;
403 } else {
404 self.currentComputedAccountStatus = CKKSAccountStatusNoAccount;
405 self.currentAccountError = error;
406 }
407 [self.currentComputedAccountStatusValid fulfill];
408
409 if(oldComputedStatus == self.currentComputedAccountStatus) {
410 secnotice("ckksaccount", "No change in computed account status: %@ (%@ %@)",
411 [self currentStatus],
412 self.currentCKAccountInfo,
413 self.currentCircleStatus);
414 dispatch_semaphore_signal(finishedSema);
415 return;
416 }
417
418 secnotice("ckksaccount", "New computed account status: %@ (%@ %@)",
419 [self currentStatus],
420 self.currentCKAccountInfo,
421 self.currentCircleStatus);
422
423 [self _onqueueDeliverStateChanges:oldComputedStatus deliveredSemaphore:finishedSema];
424 }
425
426 -(void)_onqueueDeliverStateChanges:(CKKSAccountStatus)oldStatus deliveredSemaphore:(dispatch_semaphore_t)finishedSema {
427 dispatch_assert_queue(self.queue);
428
429 dispatch_group_t g = dispatch_group_create();
430 if(!g) {
431 secnotice("ckksaccount", "Unable to get dispatch group.");
432 return;
433 }
434
435 NSEnumerator *enumerator = [self.changeListeners keyEnumerator];
436 dispatch_queue_t dq;
437
438 // Queue up the changes for each listener.
439 while ((dq = [enumerator nextObject])) {
440 id<CKKSAccountStateListener> listener = [self.changeListeners objectForKey: dq];
441 [self _onqueueDeliverCurrentState:listener listenerQueue:dq oldStatus:oldStatus group:g];
442 }
443
444 dispatch_group_notify(g, self.queue, ^{
445 dispatch_semaphore_signal(finishedSema);
446 });
447 }
448
449 -(void)_onqueueDeliverCurrentState:(id<CKKSAccountStateListener>)listener listenerQueue:(dispatch_queue_t)listenerQueue oldStatus:(CKKSAccountStatus)oldStatus group:(dispatch_group_t)g {
450 dispatch_assert_queue(self.queue);
451
452 __weak __typeof(listener) weakListener = listener;
453
454 if(listener) {
455 dispatch_group_async(g, listenerQueue, ^{
456 [weakListener ckAccountStatusChange:oldStatus to:self.currentComputedAccountStatus];
457 });
458 }
459 }
460
461 -(void)notifyCKAccountStatusChangeAndWaitForSignal {
462 dispatch_semaphore_wait([self notifyCKAccountStatusChange: nil], DISPATCH_TIME_FOREVER);
463 }
464
465 -(void)notifyCircleStatusChangeAndWaitForSignal {
466 dispatch_semaphore_wait([self notifyCircleChange: nil], DISPATCH_TIME_FOREVER);
467 }
468
469 -(dispatch_group_t)checkForAllDeliveries {
470
471 dispatch_group_t g = dispatch_group_create();
472 if(!g) {
473 secnotice("ckksaccount", "Unable to get dispatch group.");
474 return nil;
475 }
476
477 dispatch_sync(self.queue, ^{
478 NSEnumerator *enumerator = [self.changeListeners keyEnumerator];
479 dispatch_queue_t dq;
480
481 // Queue up the changes for each listener.
482 while ((dq = [enumerator nextObject])) {
483 id<CKKSAccountStateListener> listener = [self.changeListeners objectForKey: dq];
484
485 secinfo("ckksaccountblock", "Starting blocking for listener %@", listener);
486 __weak __typeof(listener) weakListener = listener;
487 dispatch_group_async(g, dq, ^{
488 __strong __typeof(listener) strongListener = weakListener;
489 // Do nothing in particular. It's just important that this block runs.
490 secinfo("ckksaccountblock", "Done blocking for listener %@", strongListener);
491 });
492 }
493 });
494
495 return g;
496 }
497
498 // This is its own function to allow OCMock to swoop in and replace the result during testing.
499 + (SOSAccountStatus*)getCircleStatus {
500 CFErrorRef cferror = NULL;
501
502 SOSCCStatus status = SOSCCThisDeviceIsInCircle(&cferror);
503 if(cferror) {
504 secerror("ckksaccount: error getting circle status: %@", cferror);
505 return [[SOSAccountStatus alloc] init:kSOSCCError error:CFBridgingRelease(cferror)];
506 }
507
508 return [[SOSAccountStatus alloc] init:status error:nil];
509 }
510
511 -(NSString*)currentStatus {
512 return [CKKSCKAccountStateTracker stringFromAccountStatus:self.currentComputedAccountStatus];
513 }
514
515 +(NSString*)stringFromAccountStatus: (CKKSAccountStatus) status {
516 switch(status) {
517 case CKKSAccountStatusUnknown: return @"account state unknown";
518 case CKKSAccountStatusAvailable: return @"logged in";
519 case CKKSAccountStatusNoAccount: return @"no account";
520 }
521 }
522
523 @end
524
525 @implementation SOSAccountStatus
526 - (instancetype)init:(SOSCCStatus)status error:(NSError*)error
527 {
528 if((self = [super init])) {
529 _status = status;
530 _error = error;
531 }
532 return self;
533 }
534
535 - (BOOL)isEqual:(id)object
536 {
537 if(![object isKindOfClass:[SOSAccountStatus class]]) {
538 return NO;
539 }
540
541 if(object == nil) {
542 return NO;
543 }
544
545 SOSAccountStatus* obj = (SOSAccountStatus*) object;
546 return self.status == obj.status &&
547 ((self.error == nil && obj.error == nil) || [self.error isEqual:obj.error]);
548 }
549
550 - (NSString*)description
551 {
552 return [NSString stringWithFormat:@"<SOSStatus: %@ (%@)>", SOSCCGetStatusDescription(self.status), self.error];
553 }
554 @end
555
556 #endif // OCTAGON