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>
32 #import "keychain/ckks/CKKS.h"
33 #import "keychain/ckks/CKKSCKAccountStateTracker.h"
36 @interface CKKSCKAccountStateTracker ()
37 @property (readonly) Class<CKKSNSNotificationCenter> nsnotificationCenterClass;
39 @property CKKSAccountStatus currentComputedAccountStatus;
41 @property dispatch_queue_t queue;
43 @property NSMapTable<dispatch_queue_t, id<CKKSAccountStateListener>>* changeListeners;
44 @property CKContainer* container; // used only for fetching the CKAccountStatus
46 /* We have initialization races. We should report CKKSAccountStatusUnknown until both of
47 * these are true, otherwise on a race, it looks like we logged out. */
48 @property bool firstCKAccountFetch;
49 @property bool firstSOSCircleFetch;
52 @implementation CKKSCKAccountStateTracker
54 -(instancetype)init: (CKContainer*) container nsnotificationCenterClass: (Class<CKKSNSNotificationCenter>) nsnotificationCenterClass {
55 if((self = [super init])) {
56 _nsnotificationCenterClass = nsnotificationCenterClass;
57 _changeListeners = [NSMapTable strongToWeakObjectsMapTable]; // Backwards from how we'd like, but it's the best way to have weak pointers to CKKSAccountStateListener.
58 _currentCKAccountInfo = nil;
59 _currentCircleStatus = kSOSCCError;
61 _currentComputedAccountStatus = CKKSAccountStatusUnknown;
63 _container = container;
65 _queue = dispatch_queue_create("ck-account-state", DISPATCH_QUEUE_SERIAL);
67 _firstCKAccountFetch = false;
68 _firstSOSCircleFetch = false;
70 _finishedInitialDispatches = [[CKKSCondition alloc] init];
71 _ckdeviceIDInitialized = [[CKKSCondition alloc] init];
73 id<CKKSNSNotificationCenter> notificationCenter = [self.nsnotificationCenterClass defaultCenter];
74 secinfo("ckksaccount", "Registering with notification center %@", notificationCenter);
75 [notificationCenter addObserver:self selector:@selector(notifyCKAccountStatusChange:) name:CKAccountChangedNotification object:NULL];
77 __weak __typeof(self) weakSelf = self;
79 // If this is a live server, register with notify
80 if(!SecCKKSTestsEnabled()) {
82 notify_register_dispatch(kSOSCCCircleChangedNotification, &token, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^(int t) {
83 [weakSelf notifyCircleChange:nil];
87 // 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.
88 dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
89 __strong __typeof(self) strongSelf = weakSelf;
93 [strongSelf notifyCKAccountStatusChange:nil];
94 [strongSelf notifyCircleChange:nil];
95 [strongSelf.finishedInitialDispatches fulfill];
102 id<CKKSNSNotificationCenter> notificationCenter = [self.nsnotificationCenterClass defaultCenter];
103 [notificationCenter removeObserver:self];
106 -(NSString*)descriptionInternal: (NSString*) selfString {
107 return [NSString stringWithFormat:@"<%@: %@ (%@ %@)",
109 [self currentStatus],
110 self.currentCKAccountInfo,
111 SOSCCGetStatusDescription(self.currentCircleStatus)];
114 -(NSString*)description {
115 return [self descriptionInternal: [[self class] description]];
118 -(NSString*)debugDescription {
119 return [self descriptionInternal: [super description]];
122 -(dispatch_semaphore_t)notifyOnAccountStatusChange:(id<CKKSAccountStateListener>)listener {
123 // signals when we've successfully delivered the first account status
124 dispatch_semaphore_t finishedSema = dispatch_semaphore_create(0);
126 dispatch_async(self.queue, ^{
127 bool alreadyRegisteredListener = false;
128 NSEnumerator *enumerator = [self.changeListeners objectEnumerator];
129 id<CKKSAccountStateListener> value;
131 while ((value = [enumerator nextObject])) {
132 // do pointer comparison
133 alreadyRegisteredListener |= (value == listener);
136 if(listener && !alreadyRegisteredListener) {
137 NSString* queueName = [NSString stringWithFormat: @"ck-account-state-%@", listener];
139 dispatch_queue_t objQueue = dispatch_queue_create([queueName UTF8String], DISPATCH_QUEUE_SERIAL);
140 [self.changeListeners setObject: listener forKey: objQueue];
142 // If we know the current account status, let this listener know
143 if(self.currentComputedAccountStatus != CKKSAccountStatusUnknown) {
145 dispatch_group_t g = dispatch_group_create();
147 secnotice("ckksaccount", "Unable to get dispatch group.");
151 [self _onqueueDeliverCurrentState:listener listenerQueue:objQueue oldStatus:CKKSAccountStatusUnknown group:g];
153 dispatch_group_notify(g, self.queue, ^{
154 dispatch_semaphore_signal(finishedSema);
157 dispatch_semaphore_signal(finishedSema);
160 dispatch_semaphore_signal(finishedSema);
167 - (dispatch_semaphore_t)notifyCKAccountStatusChange:(__unused id)object {
168 // signals when this notify is Complete, including all downcalls.
169 dispatch_semaphore_t finishedSema = dispatch_semaphore_create(0);
171 [self.container accountInfoWithCompletionHandler:^(CKAccountInfo* ckAccountInfo, NSError * _Nullable error) {
173 secerror("ckksaccount: error getting account info: %@", error);
174 dispatch_semaphore_signal(finishedSema);
178 dispatch_sync(self.queue, ^{
179 self.firstCKAccountFetch = true;
180 [self _onqueueUpdateAccountState:ckAccountInfo circle:self.currentCircleStatus deliveredSemaphore:finishedSema];
187 -(dispatch_semaphore_t)notifyCircleChange:(__unused id)object {
188 dispatch_semaphore_t finishedSema = dispatch_semaphore_create(0);
190 SOSCCStatus circleStatus = [CKKSCKAccountStateTracker getCircleStatus];
191 dispatch_sync(self.queue, ^{
192 self.firstSOSCircleFetch = true;
194 [self _onqueueUpdateAccountState:self.currentCKAccountInfo circle:circleStatus deliveredSemaphore:finishedSema];
199 // Takes the new ckAccountInfo we're moving to
200 -(void)_onqueueUpdateCKDeviceID: (CKAccountInfo*)ckAccountInfo {
201 dispatch_assert_queue(self.queue);
202 __weak __typeof(self) weakSelf = self;
204 // If we're in an account, opportunistically fill in the device id
205 if(ckAccountInfo.accountStatus == CKAccountStatusAvailable) {
206 [self.container fetchCurrentDeviceIDWithCompletionHandler:^(NSString* deviceID, NSError* ckerror) {
207 __strong __typeof(self) strongSelf = weakSelf;
209 // Make sure you synchronize here; if we've logged out before the callback returns, don't record the result
210 dispatch_async(strongSelf.queue, ^{
211 __strong __typeof(self) innerStrongSelf = weakSelf;
212 if(innerStrongSelf.currentCKAccountInfo.accountStatus == CKAccountStatusAvailable) {
213 secnotice("ckksaccount", "CloudKit deviceID is: %@ %@", deviceID, ckerror);
215 innerStrongSelf.ckdeviceID = deviceID;
216 innerStrongSelf.ckdeviceIDError = ckerror;
217 [innerStrongSelf.ckdeviceIDInitialized fulfill];
219 // Logged out! No ckdeviceid.
220 secerror("ckksaccount: Logged back out but still received a fetchCurrentDeviceIDWithCompletionHandler callback");
222 innerStrongSelf.ckdeviceID = nil;
223 innerStrongSelf.ckdeviceIDError = nil;
224 // Don't touch the ckdeviceIDInitialized object; it should have been reset when the logout happened.
229 // Logging out? no more device ID.
230 self.ckdeviceID = nil;
231 self.ckdeviceIDError = nil;
232 self.ckdeviceIDInitialized = [[CKKSCondition alloc] init];
236 // Pulled out for mocking purposes
237 +(void)fetchCirclePeerID:(void (^)(NSString* _Nullable peerID, NSError* _Nullable error))callback {
238 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
239 CFErrorRef cferror = nil;
240 SOSPeerInfoRef egoPeerInfo = SOSCCCopyMyPeerInfo(&cferror);
241 NSString* egoPeerID = egoPeerInfo ? (NSString*)CFBridgingRelease(CFRetainSafe(SOSPeerInfoGetPeerID(egoPeerInfo))) : nil;
242 CFReleaseNull(egoPeerInfo);
244 callback(egoPeerID, CFBridgingRelease(cferror));
248 // Takes the new ckAccountInfo we're moving to
249 -(void)_onqueueUpdateCirclePeerID: (SOSCCStatus)sosccstatus {
250 dispatch_assert_queue(self.queue);
251 __weak __typeof(self) weakSelf = self;
253 // If we're in a circle, fetch the peer id
254 if(sosccstatus == kSOSCCInCircle) {
255 [CKKSCKAccountStateTracker fetchCirclePeerID:^(NSString* peerID, NSError* error) {
256 __strong __typeof(self) strongSelf = weakSelf;
257 dispatch_async(strongSelf.queue, ^{
258 __strong __typeof(self) innerstrongSelf = weakSelf;
260 if(innerstrongSelf.currentCircleStatus == kSOSCCInCircle) {
261 secnotice("ckksaccount", "Circle peerID is: %@ %@", peerID, error);
262 // Still in circle. Proceed.
263 innerstrongSelf.accountCirclePeerID = peerID;
264 innerstrongSelf.accountCirclePeerIDError = error;
265 [innerstrongSelf.accountCirclePeerIDInitialized fulfill];
267 secerror("ckksaccount: Out of circle but still received a fetchCirclePeerID callback");
268 // Not in-circle. Throw away circle id.
269 strongSelf.accountCirclePeerID = nil;
270 strongSelf.accountCirclePeerIDError = nil;
271 // Don't touch the accountCirclePeerIDInitialized object; it should have been reset when the logout happened.
276 // Not in-circle, reset circle ID
277 secnotice("ckksaccount", "out of circle(%d): resetting peer ID", sosccstatus);
278 self.accountCirclePeerID = nil;
279 self.accountCirclePeerIDError = nil;
280 self.accountCirclePeerIDInitialized = [[CKKSCondition alloc] init];
284 -(void)_onqueueUpdateAccountState:(CKAccountInfo*)ckAccountInfo circle:(SOSCCStatus)sosccstatus deliveredSemaphore:(dispatch_semaphore_t)finishedSema {
285 dispatch_assert_queue(self.queue);
287 if([self.currentCKAccountInfo isEqual: ckAccountInfo] && self.currentCircleStatus == sosccstatus) {
289 secinfo("ckksaccount", "received another notification of CK Account State %@ and Circle status %d", ckAccountInfo, (int)sosccstatus);
290 dispatch_semaphore_signal(finishedSema);
294 if(![self.currentCKAccountInfo isEqual: ckAccountInfo]) {
295 secnotice("ckksaccount", "moving to CK Account info: %@", ckAccountInfo);
296 self.currentCKAccountInfo = ckAccountInfo;
298 [self _onqueueUpdateCKDeviceID: ckAccountInfo];
300 if(self.currentCircleStatus != sosccstatus) {
301 secnotice("ckksaccount", "moving to circle status: %@", SOSCCGetStatusDescription(sosccstatus));
302 self.currentCircleStatus = sosccstatus;
304 [self _onqueueUpdateCirclePeerID: sosccstatus];
307 if(!self.firstSOSCircleFetch || !self.firstCKAccountFetch) {
308 secnotice("ckksaccount", "Haven't received updates from all sources; not passing update along: %@", self);
309 dispatch_semaphore_signal(finishedSema);
313 // We are CKKSAccountStatusAvailable if we are:
314 // in CKAccountStatusAvailable
316 // and supportsDeviceToDeviceEncryption == true
317 CKKSAccountStatus oldComputedStatus = self.currentComputedAccountStatus;
319 if(self.currentCKAccountInfo) {
320 if(self.currentCKAccountInfo.accountStatus == CKAccountStatusAvailable) {
321 // CloudKit thinks we're logged in. Double check!
322 if(self.currentCKAccountInfo.supportsDeviceToDeviceEncryption && self.currentCircleStatus == kSOSCCInCircle) {
323 self.currentComputedAccountStatus = CKKSAccountStatusAvailable;
325 self.currentComputedAccountStatus = CKKSAccountStatusNoAccount;
329 // Account status is not CKAccountStatusAvailable; no more checking required.
330 self.currentComputedAccountStatus = CKKSAccountStatusNoAccount;
333 // No CKAccountInfo? We haven't received an update from cloudd yet; Change nothing.
336 if(oldComputedStatus == self.currentComputedAccountStatus) {
337 secnotice("ckksaccount", "No change in computed account status: %@ (%@ %@)",
338 [self currentStatus],
339 self.currentCKAccountInfo,
340 SOSCCGetStatusDescription(self.currentCircleStatus));
341 dispatch_semaphore_signal(finishedSema);
345 secnotice("ckksaccount", "New computed account status: %@ (%@ %@)",
346 [self currentStatus],
347 self.currentCKAccountInfo,
348 SOSCCGetStatusDescription(self.currentCircleStatus));
350 [self _onqueueDeliverStateChanges:oldComputedStatus deliveredSemaphore:finishedSema];
353 -(void)_onqueueDeliverStateChanges:(CKKSAccountStatus)oldStatus deliveredSemaphore:(dispatch_semaphore_t)finishedSema {
354 dispatch_assert_queue(self.queue);
356 dispatch_group_t g = dispatch_group_create();
358 secnotice("ckksaccount", "Unable to get dispatch group.");
362 NSEnumerator *enumerator = [self.changeListeners keyEnumerator];
365 // Queue up the changes for each listener.
366 while ((dq = [enumerator nextObject])) {
367 id<CKKSAccountStateListener> listener = [self.changeListeners objectForKey: dq];
368 [self _onqueueDeliverCurrentState:listener listenerQueue:dq oldStatus:oldStatus group:g];
371 dispatch_group_notify(g, self.queue, ^{
372 dispatch_semaphore_signal(finishedSema);
376 -(void)_onqueueDeliverCurrentState:(id<CKKSAccountStateListener>)listener listenerQueue:(dispatch_queue_t)listenerQueue oldStatus:(CKKSAccountStatus)oldStatus group:(dispatch_group_t)g {
377 dispatch_assert_queue(self.queue);
379 __weak __typeof(listener) weakListener = listener;
382 dispatch_group_async(g, listenerQueue, ^{
383 [weakListener ckAccountStatusChange:oldStatus to:self.currentComputedAccountStatus];
388 -(void)notifyCKAccountStatusChangeAndWaitForSignal {
389 dispatch_semaphore_wait([self notifyCKAccountStatusChange: nil], DISPATCH_TIME_FOREVER);
392 -(void)notifyCircleStatusChangeAndWaitForSignal {
393 dispatch_semaphore_wait([self notifyCircleChange: nil], DISPATCH_TIME_FOREVER);
396 -(dispatch_group_t)checkForAllDeliveries {
398 dispatch_group_t g = dispatch_group_create();
400 secnotice("ckksaccount", "Unable to get dispatch group.");
404 dispatch_sync(self.queue, ^{
405 NSEnumerator *enumerator = [self.changeListeners keyEnumerator];
408 // Queue up the changes for each listener.
409 while ((dq = [enumerator nextObject])) {
410 id<CKKSAccountStateListener> listener = [self.changeListeners objectForKey: dq];
412 secinfo("ckksaccountblock", "Starting blocking for listener %@", listener);
413 __weak __typeof(listener) weakListener = listener;
414 dispatch_group_async(g, dq, ^{
415 __strong __typeof(listener) strongListener = weakListener;
416 // Do nothing in particular. It's just important that this block runs.
417 secinfo("ckksaccountblock", "Done blocking for listener %@", strongListener);
425 // This is its own function to allow OCMock to swoop in and replace the result during testing.
426 +(SOSCCStatus)getCircleStatus {
427 CFErrorRef cferror = NULL;
429 SOSCCStatus status = SOSCCThisDeviceIsInCircle(&cferror);
431 secerror("ckksaccount: error getting circle status: %@", cferror);
432 CFReleaseNull(cferror);
438 -(NSString*)currentStatus {
439 return [CKKSCKAccountStateTracker stringFromAccountStatus:self.currentComputedAccountStatus];
442 +(NSString*)stringFromAccountStatus: (CKKSAccountStatus) status {
444 case CKKSAccountStatusUnknown: return @"account state unknown";
445 case CKKSAccountStatusAvailable: return @"logged in";
446 case CKKSAccountStatusNoAccount: return @"no account";