2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
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
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.
21 * @APPLE_LICENSE_HEADER_END@
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>
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"
39 @interface CKKSCKAccountStateTracker ()
40 @property (readonly) Class<CKKSNSNotificationCenter> nsnotificationCenterClass;
42 @property CKKSAccountStatus currentComputedAccountStatus;
43 @property (nullable, atomic) NSError* currentAccountError;
45 @property dispatch_queue_t queue;
47 @property NSMapTable<dispatch_queue_t, id<CKKSAccountStateListener>>* changeListeners;
48 @property CKContainer* container; // used only for fetching the CKAccountStatus
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;
56 @implementation CKKSCKAccountStateTracker
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];
65 _currentComputedAccountStatus = CKKSAccountStatusUnknown;
66 _currentComputedAccountStatusValid = [[CKKSCondition alloc] init];
68 _container = container;
70 _queue = dispatch_queue_create("ck-account-state", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
72 _firstCKAccountFetch = false;
73 _firstSOSCircleFetch = false;
75 _finishedInitialDispatches = [[CKKSCondition alloc] init];
76 _ckdeviceIDInitialized = [[CKKSCondition alloc] init];
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];
82 __weak __typeof(self) weakSelf = self;
84 // If this is a live server, register with notify
85 if(!SecCKKSTestsEnabled()) {
87 notify_register_dispatch(kSOSCCCircleChangedNotification, &token, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^(int t) {
88 [weakSelf notifyCircleChange:nil];
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;
99 [strongSelf notifyCKAccountStatusChange:nil];
100 [strongSelf notifyCircleChange:nil];
101 [strongSelf.finishedInitialDispatches fulfill];
109 id<CKKSNSNotificationCenter> notificationCenter = [self.nsnotificationCenterClass defaultCenter];
110 [notificationCenter removeObserver:self];
113 -(NSString*)descriptionInternal: (NSString*) selfString {
114 return [NSString stringWithFormat:@"<%@: %@ (%@ %@) %@>",
116 [self currentStatus],
117 self.currentCKAccountInfo,
118 self.currentCircleStatus,
119 self.currentAccountError ?: @""];
122 -(NSString*)description {
123 return [self descriptionInternal: [[self class] description]];
126 -(NSString*)debugDescription {
127 return [self descriptionInternal: [super description]];
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);
134 dispatch_async(self.queue, ^{
135 bool alreadyRegisteredListener = false;
136 NSEnumerator *enumerator = [self.changeListeners objectEnumerator];
137 id<CKKSAccountStateListener> value;
139 while ((value = [enumerator nextObject])) {
140 // do pointer comparison
141 alreadyRegisteredListener |= (value == listener);
144 if(listener && !alreadyRegisteredListener) {
145 NSString* queueName = [NSString stringWithFormat: @"ck-account-state-%@", listener];
147 dispatch_queue_t objQueue = dispatch_queue_create([queueName UTF8String], DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
148 [self.changeListeners setObject: listener forKey: objQueue];
150 secinfo("ckksaccount", "adding a new listener: %@", listener);
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);
156 dispatch_group_t g = dispatch_group_create();
158 secnotice("ckksaccount", "Unable to get dispatch group.");
162 [self _onqueueDeliverCurrentState:listener listenerQueue:objQueue oldStatus:CKKSAccountStatusUnknown group:g];
164 dispatch_group_notify(g, self.queue, ^{
165 dispatch_semaphore_signal(finishedSema);
168 dispatch_semaphore_signal(finishedSema);
171 dispatch_semaphore_signal(finishedSema);
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);
182 [self.container accountInfoWithCompletionHandler:^(CKAccountInfo* ckAccountInfo, NSError * _Nullable error) {
184 secerror("ckksaccount: error getting account info: %@", error);
185 dispatch_semaphore_signal(finishedSema);
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];
199 -(dispatch_semaphore_t)notifyCircleChange:(__unused id)object {
200 dispatch_semaphore_t finishedSema = dispatch_semaphore_create(0);
202 SOSAccountStatus* circleStatus = [CKKSCKAccountStateTracker getCircleStatus];
203 dispatch_sync(self.queue, ^{
204 self.firstSOSCircleFetch = true;
206 [self _onqueueUpdateAccountState:self.currentCKAccountInfo circle:circleStatus deliveredSemaphore:finishedSema];
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;
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;
221 secerror("ckksaccount: Received fetchCurrentDeviceIDWithCompletionHandler callback with null AccountStateTracker");
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);
231 innerStrongSelf.ckdeviceID = deviceID;
232 innerStrongSelf.ckdeviceIDError = ckerror;
233 [innerStrongSelf.ckdeviceIDInitialized fulfill];
235 // Logged out! No ckdeviceid.
236 secerror("ckksaccount: Logged back out but still received a fetchCurrentDeviceIDWithCompletionHandler callback");
238 innerStrongSelf.ckdeviceID = nil;
239 innerStrongSelf.ckdeviceIDError = nil;
240 // Don't touch the ckdeviceIDInitialized object; it should have been reset when the logout happened.
245 // Logging out? no more device ID.
246 self.ckdeviceID = nil;
247 self.ckdeviceIDError = nil;
248 self.ckdeviceIDInitialized = [[CKKSCondition alloc] init];
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);
260 callback(egoPeerID, CFBridgingRelease(cferror));
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;
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;
274 secerror("ckksaccount: Received fetchCirclePeerID callback with null AccountStateTracker");
278 dispatch_async(strongSelf.queue, ^{
279 __strong __typeof(self) innerstrongSelf = weakSelf;
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];
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.
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];
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
311 dispatch_assert_queue(self.queue);
312 if(self.currentCKAccountInfo) {
313 if(self.currentCKAccountInfo.accountStatus != CKAccountStatusAvailable) {
315 *error = [NSError errorWithDomain:CKKSErrorDomain
317 description:@"iCloud account is logged out"];
320 } else if(!self.currentCKAccountInfo.supportsDeviceToDeviceEncryption) {
322 *error = [NSError errorWithDomain:CKKSErrorDomain
324 description:@"iCloud account is not HSA2"];
327 } else if(!self.currentCKAccountInfo.hasValidCredentials) {
329 *error = [NSError errorWithDomain:CKKSErrorDomain
330 code:CKKSiCloudGreyMode
331 description:@"iCloud account is in grey mode"];
337 *error = [NSError errorWithDomain:CKKSErrorDomain
339 description:@"No current iCloud account status"];
344 if(self.currentCircleStatus.status != kSOSCCInCircle) {
346 *error = [NSError errorWithDomain:(__bridge NSString*)kSOSErrorDomain
347 code:kSOSErrorNotInCircle
348 description:@"Not in circle"];
356 -(void)_onqueueUpdateAccountState:(CKAccountInfo*)ckAccountInfo circle:(SOSAccountStatus*)sosstatus deliveredSemaphore:(dispatch_semaphore_t)finishedSema {
357 dispatch_assert_queue(self.queue);
359 if([self.currentCKAccountInfo isEqual: ckAccountInfo] && self.currentCircleStatus.status == sosstatus.status) {
361 secinfo("ckksaccount", "received another notification of CK Account State %@ and Circle status %d", ckAccountInfo, (int)sosstatus);
362 dispatch_semaphore_signal(finishedSema);
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;
371 [self _onqueueUpdateCKDeviceID: ckAccountInfo];
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];
379 [self _onqueueUpdateCirclePeerID: sosstatus];
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);
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);
397 CKKSAccountStatus oldComputedStatus = self.currentComputedAccountStatus;
399 NSError* error = nil;
400 if([self _onqueueDetermineLoggedIn:&error]) {
401 self.currentComputedAccountStatus = CKKSAccountStatusAvailable;
402 self.currentAccountError = nil;
404 self.currentComputedAccountStatus = CKKSAccountStatusNoAccount;
405 self.currentAccountError = error;
407 [self.currentComputedAccountStatusValid fulfill];
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);
418 secnotice("ckksaccount", "New computed account status: %@ (%@ %@)",
419 [self currentStatus],
420 self.currentCKAccountInfo,
421 self.currentCircleStatus);
423 [self _onqueueDeliverStateChanges:oldComputedStatus deliveredSemaphore:finishedSema];
426 -(void)_onqueueDeliverStateChanges:(CKKSAccountStatus)oldStatus deliveredSemaphore:(dispatch_semaphore_t)finishedSema {
427 dispatch_assert_queue(self.queue);
429 dispatch_group_t g = dispatch_group_create();
431 secnotice("ckksaccount", "Unable to get dispatch group.");
435 NSEnumerator *enumerator = [self.changeListeners keyEnumerator];
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];
444 dispatch_group_notify(g, self.queue, ^{
445 dispatch_semaphore_signal(finishedSema);
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);
452 __weak __typeof(listener) weakListener = listener;
455 dispatch_group_async(g, listenerQueue, ^{
456 [weakListener ckAccountStatusChange:oldStatus to:self.currentComputedAccountStatus];
461 -(void)notifyCKAccountStatusChangeAndWaitForSignal {
462 dispatch_semaphore_wait([self notifyCKAccountStatusChange: nil], DISPATCH_TIME_FOREVER);
465 -(void)notifyCircleStatusChangeAndWaitForSignal {
466 dispatch_semaphore_wait([self notifyCircleChange: nil], DISPATCH_TIME_FOREVER);
469 -(dispatch_group_t)checkForAllDeliveries {
471 dispatch_group_t g = dispatch_group_create();
473 secnotice("ckksaccount", "Unable to get dispatch group.");
477 dispatch_sync(self.queue, ^{
478 NSEnumerator *enumerator = [self.changeListeners keyEnumerator];
481 // Queue up the changes for each listener.
482 while ((dq = [enumerator nextObject])) {
483 id<CKKSAccountStateListener> listener = [self.changeListeners objectForKey: dq];
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);
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;
502 SOSCCStatus status = SOSCCThisDeviceIsInCircle(&cferror);
504 secerror("ckksaccount: error getting circle status: %@", cferror);
505 return [[SOSAccountStatus alloc] init:kSOSCCError error:CFBridgingRelease(cferror)];
508 return [[SOSAccountStatus alloc] init:status error:nil];
511 -(NSString*)currentStatus {
512 return [CKKSCKAccountStateTracker stringFromAccountStatus:self.currentComputedAccountStatus];
515 +(NSString*)stringFromAccountStatus: (CKKSAccountStatus) status {
517 case CKKSAccountStatusUnknown: return @"account state unknown";
518 case CKKSAccountStatusAvailable: return @"logged in";
519 case CKKSAccountStatusNoAccount: return @"no account";
525 @implementation SOSAccountStatus
526 - (instancetype)init:(SOSCCStatus)status error:(NSError*)error
528 if((self = [super init])) {
535 - (BOOL)isEqual:(id)object
537 if(![object isKindOfClass:[SOSAccountStatus class]]) {
545 SOSAccountStatus* obj = (SOSAccountStatus*) object;
546 return self.status == obj.status &&
547 ((self.error == nil && obj.error == nil) || [self.error isEqual:obj.error]);
550 - (NSString*)description
552 return [NSString stringWithFormat:@"<SOSStatus: %@ (%@)>", SOSCCGetStatusDescription(self.status), self.error];