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"
38 @interface CKKSCKAccountStateTracker ()
39 @property (readonly) Class<CKKSNSNotificationCenter> nsnotificationCenterClass;
41 @property CKKSAccountStatus currentComputedAccountStatus;
42 @property (nullable, atomic) NSError* currentAccountError;
44 @property dispatch_queue_t queue;
46 @property NSMapTable<dispatch_queue_t, id<CKKSAccountStateListener>>* changeListeners;
47 @property CKContainer* container; // used only for fetching the CKAccountStatus
49 /* We have initialization races. We should report CKKSAccountStatusUnknown until both of
50 * these are true, otherwise on a race, it looks like we logged out. */
51 @property bool firstCKAccountFetch;
52 @property bool firstSOSCircleFetch;
55 @implementation CKKSCKAccountStateTracker
57 -(instancetype)init: (CKContainer*) container nsnotificationCenterClass: (Class<CKKSNSNotificationCenter>) nsnotificationCenterClass {
58 if((self = [super init])) {
59 _nsnotificationCenterClass = nsnotificationCenterClass;
60 _changeListeners = [NSMapTable strongToWeakObjectsMapTable]; // Backwards from how we'd like, but it's the best way to have weak pointers to CKKSAccountStateListener.
61 _currentCKAccountInfo = nil;
62 _currentCircleStatus = kSOSCCError;
64 _currentComputedAccountStatus = CKKSAccountStatusUnknown;
65 _currentComputedAccountStatusValid = [[CKKSCondition alloc] init];
67 _container = container;
69 _queue = dispatch_queue_create("ck-account-state", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
71 _firstCKAccountFetch = false;
72 _firstSOSCircleFetch = false;
74 _finishedInitialDispatches = [[CKKSCondition alloc] init];
75 _ckdeviceIDInitialized = [[CKKSCondition alloc] init];
77 id<CKKSNSNotificationCenter> notificationCenter = [self.nsnotificationCenterClass defaultCenter];
78 secinfo("ckksaccount", "Registering with notification center %@", notificationCenter);
79 [notificationCenter addObserver:self selector:@selector(notifyCKAccountStatusChange:) name:CKAccountChangedNotification object:NULL];
81 __weak __typeof(self) weakSelf = self;
83 // If this is a live server, register with notify
84 if(!SecCKKSTestsEnabled()) {
86 notify_register_dispatch(kSOSCCCircleChangedNotification, &token, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^(int t) {
87 [weakSelf notifyCircleChange:nil];
91 // 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.
92 dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
93 __strong __typeof(self) strongSelf = weakSelf;
98 [strongSelf notifyCKAccountStatusChange:nil];
99 [strongSelf notifyCircleChange:nil];
100 [strongSelf.finishedInitialDispatches fulfill];
108 id<CKKSNSNotificationCenter> notificationCenter = [self.nsnotificationCenterClass defaultCenter];
109 [notificationCenter removeObserver:self];
112 -(NSString*)descriptionInternal: (NSString*) selfString {
113 return [NSString stringWithFormat:@"<%@: %@ (%@ %@) %@>",
115 [self currentStatus],
116 self.currentCKAccountInfo,
117 SOSCCGetStatusDescription(self.currentCircleStatus),
118 self.currentAccountError ?: @""];
121 -(NSString*)description {
122 return [self descriptionInternal: [[self class] description]];
125 -(NSString*)debugDescription {
126 return [self descriptionInternal: [super description]];
129 -(dispatch_semaphore_t)notifyOnAccountStatusChange:(id<CKKSAccountStateListener>)listener {
130 // signals when we've successfully delivered the first account status
131 dispatch_semaphore_t finishedSema = dispatch_semaphore_create(0);
133 dispatch_async(self.queue, ^{
134 bool alreadyRegisteredListener = false;
135 NSEnumerator *enumerator = [self.changeListeners objectEnumerator];
136 id<CKKSAccountStateListener> value;
138 while ((value = [enumerator nextObject])) {
139 // do pointer comparison
140 alreadyRegisteredListener |= (value == listener);
143 if(listener && !alreadyRegisteredListener) {
144 NSString* queueName = [NSString stringWithFormat: @"ck-account-state-%@", listener];
146 dispatch_queue_t objQueue = dispatch_queue_create([queueName UTF8String], DISPATCH_QUEUE_SERIAL);
147 [self.changeListeners setObject: listener forKey: objQueue];
149 secinfo("ckksaccount", "adding a new listener: %@", listener);
151 // If we know the current account status, let this listener know
152 if(self.currentComputedAccountStatus != CKKSAccountStatusUnknown) {
153 secinfo("ckksaccount", "notifying new listener %@ of current state %d", listener, (int)self.currentComputedAccountStatus);
155 dispatch_group_t g = dispatch_group_create();
157 secnotice("ckksaccount", "Unable to get dispatch group.");
161 [self _onqueueDeliverCurrentState:listener listenerQueue:objQueue oldStatus:CKKSAccountStatusUnknown group:g];
163 dispatch_group_notify(g, self.queue, ^{
164 dispatch_semaphore_signal(finishedSema);
167 dispatch_semaphore_signal(finishedSema);
170 dispatch_semaphore_signal(finishedSema);
177 - (dispatch_semaphore_t)notifyCKAccountStatusChange:(__unused id)object {
178 // signals when this notify is Complete, including all downcalls.
179 dispatch_semaphore_t finishedSema = dispatch_semaphore_create(0);
181 [self.container accountInfoWithCompletionHandler:^(CKAccountInfo* ckAccountInfo, NSError * _Nullable error) {
183 secerror("ckksaccount: error getting account info: %@", error);
184 dispatch_semaphore_signal(finishedSema);
188 dispatch_sync(self.queue, ^{
189 self.firstCKAccountFetch = true;
190 [self _onqueueUpdateAccountState:ckAccountInfo circle:self.currentCircleStatus deliveredSemaphore:finishedSema];
197 -(dispatch_semaphore_t)notifyCircleChange:(__unused id)object {
198 dispatch_semaphore_t finishedSema = dispatch_semaphore_create(0);
200 SOSCCStatus circleStatus = [CKKSCKAccountStateTracker getCircleStatus];
201 dispatch_sync(self.queue, ^{
202 self.firstSOSCircleFetch = true;
204 [self _onqueueUpdateAccountState:self.currentCKAccountInfo circle:circleStatus deliveredSemaphore:finishedSema];
209 // Takes the new ckAccountInfo we're moving to
210 -(void)_onqueueUpdateCKDeviceID: (CKAccountInfo*)ckAccountInfo {
211 dispatch_assert_queue(self.queue);
212 __weak __typeof(self) weakSelf = self;
214 // If we're in an account, opportunistically fill in the device id
215 if(ckAccountInfo.accountStatus == CKAccountStatusAvailable) {
216 [self.container fetchCurrentDeviceIDWithCompletionHandler:^(NSString* deviceID, NSError* ckerror) {
217 __strong __typeof(self) strongSelf = weakSelf;
219 secerror("ckksaccount: Received fetchCurrentDeviceIDWithCompletionHandler callback with null AccountStateTracker");
223 // Make sure you synchronize here; if we've logged out before the callback returns, don't record the result
224 dispatch_async(strongSelf.queue, ^{
225 __strong __typeof(self) innerStrongSelf = weakSelf;
226 if(innerStrongSelf.currentCKAccountInfo.accountStatus == CKAccountStatusAvailable) {
227 secnotice("ckksaccount", "CloudKit deviceID is: %@ %@", deviceID, ckerror);
229 innerStrongSelf.ckdeviceID = deviceID;
230 innerStrongSelf.ckdeviceIDError = ckerror;
231 [innerStrongSelf.ckdeviceIDInitialized fulfill];
233 // Logged out! No ckdeviceid.
234 secerror("ckksaccount: Logged back out but still received a fetchCurrentDeviceIDWithCompletionHandler callback");
236 innerStrongSelf.ckdeviceID = nil;
237 innerStrongSelf.ckdeviceIDError = nil;
238 // Don't touch the ckdeviceIDInitialized object; it should have been reset when the logout happened.
243 // Logging out? no more device ID.
244 self.ckdeviceID = nil;
245 self.ckdeviceIDError = nil;
246 self.ckdeviceIDInitialized = [[CKKSCondition alloc] init];
250 // Pulled out for mocking purposes
251 +(void)fetchCirclePeerID:(void (^)(NSString* _Nullable peerID, NSError* _Nullable error))callback {
252 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
253 CFErrorRef cferror = nil;
254 SOSPeerInfoRef egoPeerInfo = SOSCCCopyMyPeerInfo(&cferror);
255 NSString* egoPeerID = egoPeerInfo ? (NSString*)CFBridgingRelease(CFRetainSafe(SOSPeerInfoGetPeerID(egoPeerInfo))) : nil;
256 CFReleaseNull(egoPeerInfo);
258 callback(egoPeerID, CFBridgingRelease(cferror));
262 // Takes the new ckAccountInfo we're moving to
263 -(void)_onqueueUpdateCirclePeerID: (SOSCCStatus)sosccstatus {
264 dispatch_assert_queue(self.queue);
265 __weak __typeof(self) weakSelf = self;
267 // If we're in a circle, fetch the peer id
268 if(sosccstatus == kSOSCCInCircle) {
269 [CKKSCKAccountStateTracker fetchCirclePeerID:^(NSString* peerID, NSError* error) {
270 __strong __typeof(self) strongSelf = weakSelf;
272 secerror("ckksaccount: Received fetchCirclePeerID callback with null AccountStateTracker");
276 dispatch_async(strongSelf.queue, ^{
277 __strong __typeof(self) innerstrongSelf = weakSelf;
279 if(innerstrongSelf.currentCircleStatus == kSOSCCInCircle) {
280 secnotice("ckksaccount", "Circle peerID is: %@ %@", peerID, error);
281 // Still in circle. Proceed.
282 innerstrongSelf.accountCirclePeerID = peerID;
283 innerstrongSelf.accountCirclePeerIDError = error;
284 [innerstrongSelf.accountCirclePeerIDInitialized fulfill];
286 secerror("ckksaccount: Out of circle but still received a fetchCirclePeerID callback");
287 // Not in-circle. Throw away circle id.
288 strongSelf.accountCirclePeerID = nil;
289 strongSelf.accountCirclePeerIDError = nil;
290 // Don't touch the accountCirclePeerIDInitialized object; it should have been reset when the logout happened.
295 // Not in-circle, reset circle ID
296 secnotice("ckksaccount", "out of circle(%d): resetting peer ID", sosccstatus);
297 self.accountCirclePeerID = nil;
298 self.accountCirclePeerIDError = nil;
299 self.accountCirclePeerIDInitialized = [[CKKSCondition alloc] init];
303 - (bool)_onqueueDetermineLoggedIn:(NSError**)error {
304 // We are logged in if we are:
305 // in CKAccountStatusAvailable
306 // and supportsDeviceToDeviceEncryption == true
307 // and the iCloud account is not in grey mode
309 dispatch_assert_queue(self.queue);
310 if(self.currentCKAccountInfo) {
311 if(self.currentCKAccountInfo.accountStatus != CKAccountStatusAvailable) {
313 *error = [NSError errorWithDomain:CKKSErrorDomain
315 description:@"iCloud account is logged out"];
318 } else if(!self.currentCKAccountInfo.supportsDeviceToDeviceEncryption) {
320 *error = [NSError errorWithDomain:CKKSErrorDomain
322 description:@"iCloud account is not HSA2"];
325 } else if(!self.currentCKAccountInfo.hasValidCredentials) {
327 *error = [NSError errorWithDomain:CKKSErrorDomain
328 code:CKKSiCloudGreyMode
329 description:@"iCloud account is in grey mode"];
335 *error = [NSError errorWithDomain:CKKSErrorDomain
337 description:@"No current iCloud account status"];
342 if(self.currentCircleStatus != kSOSCCInCircle) {
344 *error = [NSError errorWithDomain:(__bridge NSString*)kSOSErrorDomain
345 code:kSOSErrorNotInCircle
346 description:@"Not in circle"];
354 -(void)_onqueueUpdateAccountState:(CKAccountInfo*)ckAccountInfo circle:(SOSCCStatus)sosccstatus deliveredSemaphore:(dispatch_semaphore_t)finishedSema {
355 dispatch_assert_queue(self.queue);
357 if([self.currentCKAccountInfo isEqual: ckAccountInfo] && self.currentCircleStatus == sosccstatus) {
359 secinfo("ckksaccount", "received another notification of CK Account State %@ and Circle status %d", ckAccountInfo, (int)sosccstatus);
360 dispatch_semaphore_signal(finishedSema);
364 if(![self.currentCKAccountInfo isEqual: ckAccountInfo]) {
365 secnotice("ckksaccount", "moving to CK Account info: %@", ckAccountInfo);
366 self.currentCKAccountInfo = ckAccountInfo;
368 [self _onqueueUpdateCKDeviceID: ckAccountInfo];
370 if(self.currentCircleStatus != sosccstatus) {
371 secnotice("ckksaccount", "moving to circle status: %@", SOSCCGetStatusDescription(sosccstatus));
372 self.currentCircleStatus = sosccstatus;
373 if (sosccstatus == kSOSCCInCircle) {
374 [[CKKSAnalytics logger] setDateProperty:[NSDate date] forKey:CKKSAnalyticsLastInCircle];
376 [self _onqueueUpdateCirclePeerID: sosccstatus];
379 if(!self.firstSOSCircleFetch || !self.firstCKAccountFetch) {
380 secnotice("ckksaccount", "Haven't received updates from all sources; not passing update along: %@", self);
381 dispatch_semaphore_signal(finishedSema);
385 CKKSAccountStatus oldComputedStatus = self.currentComputedAccountStatus;
387 NSError* error = nil;
388 if([self _onqueueDetermineLoggedIn:&error]) {
389 self.currentComputedAccountStatus = CKKSAccountStatusAvailable;
390 self.currentAccountError = nil;
392 self.currentComputedAccountStatus = CKKSAccountStatusNoAccount;
393 self.currentAccountError = error;
395 [self.currentComputedAccountStatusValid fulfill];
397 if(oldComputedStatus == self.currentComputedAccountStatus) {
398 secnotice("ckksaccount", "No change in computed account status: %@ (%@ %@)",
399 [self currentStatus],
400 self.currentCKAccountInfo,
401 SOSCCGetStatusDescription(self.currentCircleStatus));
402 dispatch_semaphore_signal(finishedSema);
406 secnotice("ckksaccount", "New computed account status: %@ (%@ %@)",
407 [self currentStatus],
408 self.currentCKAccountInfo,
409 SOSCCGetStatusDescription(self.currentCircleStatus));
411 [self _onqueueDeliverStateChanges:oldComputedStatus deliveredSemaphore:finishedSema];
414 -(void)_onqueueDeliverStateChanges:(CKKSAccountStatus)oldStatus deliveredSemaphore:(dispatch_semaphore_t)finishedSema {
415 dispatch_assert_queue(self.queue);
417 dispatch_group_t g = dispatch_group_create();
419 secnotice("ckksaccount", "Unable to get dispatch group.");
423 NSEnumerator *enumerator = [self.changeListeners keyEnumerator];
426 // Queue up the changes for each listener.
427 while ((dq = [enumerator nextObject])) {
428 id<CKKSAccountStateListener> listener = [self.changeListeners objectForKey: dq];
429 [self _onqueueDeliverCurrentState:listener listenerQueue:dq oldStatus:oldStatus group:g];
432 dispatch_group_notify(g, self.queue, ^{
433 dispatch_semaphore_signal(finishedSema);
437 -(void)_onqueueDeliverCurrentState:(id<CKKSAccountStateListener>)listener listenerQueue:(dispatch_queue_t)listenerQueue oldStatus:(CKKSAccountStatus)oldStatus group:(dispatch_group_t)g {
438 dispatch_assert_queue(self.queue);
440 __weak __typeof(listener) weakListener = listener;
443 dispatch_group_async(g, listenerQueue, ^{
444 [weakListener ckAccountStatusChange:oldStatus to:self.currentComputedAccountStatus];
449 -(void)notifyCKAccountStatusChangeAndWaitForSignal {
450 dispatch_semaphore_wait([self notifyCKAccountStatusChange: nil], DISPATCH_TIME_FOREVER);
453 -(void)notifyCircleStatusChangeAndWaitForSignal {
454 dispatch_semaphore_wait([self notifyCircleChange: nil], DISPATCH_TIME_FOREVER);
457 -(dispatch_group_t)checkForAllDeliveries {
459 dispatch_group_t g = dispatch_group_create();
461 secnotice("ckksaccount", "Unable to get dispatch group.");
465 dispatch_sync(self.queue, ^{
466 NSEnumerator *enumerator = [self.changeListeners keyEnumerator];
469 // Queue up the changes for each listener.
470 while ((dq = [enumerator nextObject])) {
471 id<CKKSAccountStateListener> listener = [self.changeListeners objectForKey: dq];
473 secinfo("ckksaccountblock", "Starting blocking for listener %@", listener);
474 __weak __typeof(listener) weakListener = listener;
475 dispatch_group_async(g, dq, ^{
476 __strong __typeof(listener) strongListener = weakListener;
477 // Do nothing in particular. It's just important that this block runs.
478 secinfo("ckksaccountblock", "Done blocking for listener %@", strongListener);
486 // This is its own function to allow OCMock to swoop in and replace the result during testing.
487 +(SOSCCStatus)getCircleStatus {
488 CFErrorRef cferror = NULL;
490 SOSCCStatus status = SOSCCThisDeviceIsInCircle(&cferror);
492 secerror("ckksaccount: error getting circle status: %@", cferror);
493 CFReleaseNull(cferror);
499 -(NSString*)currentStatus {
500 return [CKKSCKAccountStateTracker stringFromAccountStatus:self.currentComputedAccountStatus];
503 +(NSString*)stringFromAccountStatus: (CKKSAccountStatus) status {
505 case CKKSAccountStatusUnknown: return @"account state unknown";
506 case CKKSAccountStatusAvailable: return @"logged in";
507 case CKKSAccountStatusNoAccount: return @"no account";