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 <Security/SecureObjectSync/SOSCloudCircle.h>
28 #include <Security/SecureObjectSync/SOSCloudCircleInternal.h>
29 #include "keychain/SecureObjectSync/SOSInternal.h"
32 #import "keychain/ot/OTManager.h"
33 #import "keychain/ot/OTConstants.h"
34 #import "keychain/ckks/CKKS.h"
35 #import "keychain/ckks/CloudKitCategories.h"
36 #import "keychain/ckks/CKKSAccountStateTracker.h"
37 #import "keychain/ckks/CKKSAnalytics.h"
38 #import "keychain/categories/NSError+UsefulConstructors.h"
39 #import "keychain/ot/ObjCImprovements.h"
42 NSString* CKKSAccountStatusToString(CKKSAccountStatus status)
45 case CKKSAccountStatusAvailable:
47 case CKKSAccountStatusNoAccount:
49 case CKKSAccountStatusUnknown:
54 @interface CKKSAccountStateTracker ()
55 @property (readonly) Class<CKKSNSNotificationCenter> nsnotificationCenterClass;
57 @property dispatch_queue_t queue;
59 @property NSMapTable<dispatch_queue_t, id<CKKSCloudKitAccountStateListener>>* ckChangeListeners;
61 @property CKContainer* container; // used only for fetching the CKAccountStatus
62 @property bool firstCKAccountFetch;
65 @property (nullable) OTCliqueStatusWrapper* octagonStatus;
66 @property (nullable) NSString* octagonPeerID;
67 @property CKKSCondition* octagonInformationInitialized;
69 @property CKKSAccountStatus hsa2iCloudAccountStatus;
70 @property CKKSCondition* hsa2iCloudAccountInitialized;
73 @implementation CKKSAccountStateTracker
74 @synthesize octagonPeerID = _octagonPeerID;
76 -(instancetype)init: (CKContainer*) container nsnotificationCenterClass: (Class<CKKSNSNotificationCenter>) nsnotificationCenterClass {
77 if((self = [super init])) {
78 _nsnotificationCenterClass = nsnotificationCenterClass;
79 // These map tables are backwards from how we'd like, but it's the best way to have weak pointers to CKKSCombinedAccountStateListener.
80 _ckChangeListeners = [NSMapTable strongToWeakObjectsMapTable];
82 _currentCKAccountInfo = nil;
83 _ckAccountInfoInitialized = [[CKKSCondition alloc] init];
85 _currentCircleStatus = [[SOSAccountStatus alloc] init:kSOSCCError error:nil];
87 _container = container;
89 _queue = dispatch_queue_create("ck-account-state", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
91 _firstCKAccountFetch = false;
93 _finishedInitialDispatches = [[CKKSCondition alloc] init];
94 _ckdeviceIDInitialized = [[CKKSCondition alloc] init];
96 _octagonInformationInitialized = [[CKKSCondition alloc] init];
98 _hsa2iCloudAccountStatus = CKKSAccountStatusUnknown;
99 _hsa2iCloudAccountInitialized = [[CKKSCondition alloc] init];
101 id<CKKSNSNotificationCenter> notificationCenter = [self.nsnotificationCenterClass defaultCenter];
102 ckksinfo_global("ckksaccount", "Registering with notification center %@", notificationCenter);
103 [notificationCenter addObserver:self selector:@selector(notifyCKAccountStatusChange:) name:CKAccountChangedNotification object:NULL];
107 // If this is a live server, register with notify
108 if(!SecCKKSTestsEnabled()) {
110 notify_register_dispatch(kSOSCCCircleChangedNotification, &token, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^(int t) {
112 [self notifyCircleChange:nil];
115 // 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.
116 // Note: if you're in the tests, you must call performInitialDispatches yourself!
117 dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
123 [self performInitialDispatches];
131 - (void)performInitialDispatches
134 [self notifyCKAccountStatusChange:nil];
135 [self notifyCircleChange:nil];
136 [self.finishedInitialDispatches fulfill];
141 id<CKKSNSNotificationCenter> notificationCenter = [self.nsnotificationCenterClass defaultCenter];
142 [notificationCenter removeObserver:self];
145 -(NSString*)descriptionInternal: (NSString*) selfString {
146 return [NSString stringWithFormat:@"<%@: %@, hsa2: %@>",
148 self.currentCKAccountInfo,
149 CKKSAccountStatusToString(self.hsa2iCloudAccountStatus)];
152 -(NSString*)description {
153 return [self descriptionInternal: [[self class] description]];
156 -(NSString*)debugDescription {
157 return [self descriptionInternal: [super description]];
160 - (dispatch_semaphore_t)registerForNotificationsOfCloudKitAccountStatusChange:(id<CKKSCloudKitAccountStateListener>)listener {
161 // signals when we've successfully delivered the first account status
162 dispatch_semaphore_t finishedSema = dispatch_semaphore_create(0);
164 dispatch_async(self.queue, ^{
165 bool alreadyRegisteredListener = false;
166 NSEnumerator *enumerator = [self.ckChangeListeners objectEnumerator];
167 id<CKKSCloudKitAccountStateListener> value;
169 while ((value = [enumerator nextObject])) {
170 // do pointer comparison
171 alreadyRegisteredListener |= (value == listener);
174 if(listener && !alreadyRegisteredListener) {
175 NSString* queueName = [NSString stringWithFormat: @"ck-account-state-%@", listener];
177 dispatch_queue_t objQueue = dispatch_queue_create([queueName UTF8String], DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
178 [self.ckChangeListeners setObject:listener forKey: objQueue];
180 ckksinfo_global("ckksaccount-ck", "adding a new listener: %@", listener);
182 // If we know the current account status, let this listener know
183 if(self.firstCKAccountFetch) {
184 ckksinfo_global("ckksaccount-ck", "notifying new listener %@ of current state %@", listener, self.currentCKAccountInfo);
186 dispatch_group_t g = dispatch_group_create();
188 ckkserror_global("ckksaccount-ck", "Unable to get dispatch group.");
189 dispatch_semaphore_signal(finishedSema);
193 [self _onqueueDeliverCurrentCloudKitState:listener listenerQueue:objQueue oldStatus:nil group:g];
195 dispatch_group_notify(g, self.queue, ^{
196 dispatch_semaphore_signal(finishedSema);
199 dispatch_semaphore_signal(finishedSema);
202 dispatch_semaphore_signal(finishedSema);
209 - (dispatch_semaphore_t)notifyCKAccountStatusChange:(__unused id)object {
210 // signals when this notify is Complete, including all downcalls.
211 dispatch_semaphore_t finishedSema = dispatch_semaphore_create(0);
215 [self.container accountInfoWithCompletionHandler:^(CKAccountInfo* ckAccountInfo, NSError * _Nullable error) {
219 ckkserror_global("ckksaccount", "error getting account info: %@", error);
220 dispatch_semaphore_signal(finishedSema);
224 dispatch_sync(self.queue, ^{
225 self.firstCKAccountFetch = true;
226 ckksnotice_global("ckksaccount", "received CK Account info: %@", ckAccountInfo);
227 [self _onqueueUpdateAccountState:ckAccountInfo deliveredSemaphore:finishedSema];
234 // Takes the new ckAccountInfo we're moving to
235 -(void)_onqueueUpdateCKDeviceID:(CKAccountInfo*)ckAccountInfo {
236 dispatch_assert_queue(self.queue);
239 // If we're in an account, opportunistically fill in the device id
240 if(ckAccountInfo.accountStatus == CKAccountStatusAvailable) {
241 [self.container fetchCurrentDeviceIDWithCompletionHandler:^(NSString* deviceID, NSError* ckerror) {
244 ckkserror_global("ckksaccount", "Received fetchCurrentDeviceIDWithCompletionHandler callback with null AccountStateTracker");
248 // Make sure you synchronize here; if we've logged out before the callback returns, don't record the result
249 dispatch_async(self.queue, ^{
251 if(self.currentCKAccountInfo.accountStatus == CKAccountStatusAvailable) {
252 ckksnotice_global("ckksaccount", "CloudKit deviceID is: %@ %@", deviceID, ckerror);
254 self.ckdeviceID = deviceID;
255 self.ckdeviceIDError = ckerror;
256 [self.ckdeviceIDInitialized fulfill];
258 // Logged out! No ckdeviceid.
259 ckkserror_global("ckksaccount", "Logged back out but still received a fetchCurrentDeviceIDWithCompletionHandler callback");
261 self.ckdeviceID = nil;
262 self.ckdeviceIDError = nil;
263 // Don't touch the ckdeviceIDInitialized object; it should have been reset when the logout happened.
268 // Logging out? no more device ID.
269 self.ckdeviceID = nil;
270 self.ckdeviceIDError = nil;
271 self.ckdeviceIDInitialized = [[CKKSCondition alloc] init];
275 - (dispatch_semaphore_t)notifyCircleChange:(__unused id)object {
276 dispatch_semaphore_t finishedSema = dispatch_semaphore_create(0);
278 SOSAccountStatus* sosstatus = [CKKSAccountStateTracker getCircleStatus];
279 dispatch_sync(self.queue, ^{
280 if(self.currentCircleStatus == nil || ![self.currentCircleStatus isEqual:sosstatus]) {
281 ckksnotice_global("ckksaccount", "moving to circle status: %@", sosstatus);
282 self.currentCircleStatus = sosstatus;
284 if (sosstatus.status == kSOSCCInCircle) {
285 [[CKKSAnalytics logger] setDateProperty:[NSDate date] forKey:CKKSAnalyticsLastInCircle];
287 [self _onqueueUpdateCirclePeerID:sosstatus];
289 dispatch_semaphore_signal(finishedSema);
295 // Pulled out for mocking purposes
296 + (void)fetchCirclePeerID:(void (^)(NSString* _Nullable peerID, NSError* _Nullable error))callback {
297 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
298 CFErrorRef cferror = nil;
299 SOSPeerInfoRef egoPeerInfo = SOSCCCopyMyPeerInfo(&cferror);
300 NSString* egoPeerID = egoPeerInfo ? (NSString*)CFBridgingRelease(CFRetainSafe(SOSPeerInfoGetPeerID(egoPeerInfo))) : nil;
301 CFReleaseNull(egoPeerInfo);
303 callback(egoPeerID, CFBridgingRelease(cferror));
307 // Takes the new ckAccountInfo we're moving to
308 - (void)_onqueueUpdateCirclePeerID:(SOSAccountStatus*)sosstatus {
309 dispatch_assert_queue(self.queue);
312 // If we're in a circle, fetch the peer id
313 if(sosstatus.status == kSOSCCInCircle) {
314 [CKKSAccountStateTracker fetchCirclePeerID:^(NSString* peerID, NSError* error) {
317 ckkserror_global("ckksaccount", "Received fetchCirclePeerID callback with null AccountStateTracker");
321 dispatch_async(self.queue, ^{
324 if(self.currentCircleStatus && self.currentCircleStatus.status == kSOSCCInCircle) {
325 ckksnotice_global("ckksaccount", "Circle peerID is: %@ %@", peerID, error);
326 // Still in circle. Proceed.
327 self.accountCirclePeerID = peerID;
328 self.accountCirclePeerIDError = error;
329 [self.accountCirclePeerIDInitialized fulfill];
331 ckkserror_global("ckksaccount", "Out of circle but still received a fetchCirclePeerID callback");
332 // Not in-circle. Throw away circle id.
333 self.accountCirclePeerID = nil;
334 self.accountCirclePeerIDError = nil;
335 // Don't touch the accountCirclePeerIDInitialized object; it should have been reset when the logout happened.
340 // Not in-circle, reset circle ID
341 ckksnotice_global("ckksaccount", "out of circle(%@): resetting peer ID", sosstatus);
342 self.accountCirclePeerID = nil;
343 self.accountCirclePeerIDError = nil;
344 self.accountCirclePeerIDInitialized = [[CKKSCondition alloc] init];
348 - (void)_onqueueUpdateAccountState:(CKAccountInfo*)ckAccountInfo
349 deliveredSemaphore:(dispatch_semaphore_t)finishedSema
351 // Launder the finishedSema into a dispatch_group.
352 // _onqueueUpdateAccountState:circle:dispatchGroup: will then add any blocks it thinks is necessary,
353 // then the group will fire the semaphore.
354 dispatch_assert_queue(self.queue);
356 dispatch_group_t g = dispatch_group_create();
358 ckksnotice_global("ckksaccount", "Unable to get dispatch group.");
359 dispatch_semaphore_signal(finishedSema);
363 [self _onqueueUpdateAccountState:ckAccountInfo
366 dispatch_group_notify(g, self.queue, ^{
367 dispatch_semaphore_signal(finishedSema);
371 - (void)_onqueueUpdateAccountState:(CKAccountInfo*)ckAccountInfo
372 dispatchGroup:(dispatch_group_t)g
374 dispatch_assert_queue(self.queue);
376 if([self.currentCKAccountInfo isEqual: ckAccountInfo]) {
378 ckksinfo_global("ckksaccount", "received another notification of CK Account State %@", ckAccountInfo);
382 if((self.currentCKAccountInfo == nil && ckAccountInfo != nil) ||
383 !(self.currentCKAccountInfo == ckAccountInfo || [self.currentCKAccountInfo isEqual: ckAccountInfo])) {
384 ckksnotice_global("ckksaccount", "moving to CK Account info: %@", ckAccountInfo);
385 CKAccountInfo* oldAccountInfo = self.currentCKAccountInfo;
386 self.currentCKAccountInfo = ckAccountInfo;
387 [self.ckAccountInfoInitialized fulfill];
389 [self _onqueueUpdateCKDeviceID: ckAccountInfo];
391 [self _onqueueDeliverCloudKitStateChanges:oldAccountInfo dispatchGroup:g];
395 - (void)_onqueueDeliverCloudKitStateChanges:(CKAccountInfo*)oldStatus
396 dispatchGroup:(dispatch_group_t)g
398 dispatch_assert_queue(self.queue);
400 NSEnumerator *enumerator = [self.ckChangeListeners keyEnumerator];
403 // Queue up the changes for each listener.
404 while ((dq = [enumerator nextObject])) {
405 id<CKKSCloudKitAccountStateListener> listener = [self.ckChangeListeners objectForKey: dq];
406 [self _onqueueDeliverCurrentCloudKitState:listener listenerQueue:dq oldStatus:oldStatus group:g];
410 - (void)_onqueueDeliverCurrentCloudKitState:(id<CKKSCloudKitAccountStateListener>)listener
411 listenerQueue:(dispatch_queue_t)listenerQueue
412 oldStatus:(CKAccountInfo* _Nullable)oldStatus
413 group:(dispatch_group_t)g
415 dispatch_assert_queue(self.queue);
417 __weak __typeof(listener) weakListener = listener;
420 dispatch_group_async(g, listenerQueue, ^{
421 [weakListener cloudkitAccountStateChange:oldStatus to:self.currentCKAccountInfo];
426 - (BOOL)notifyCKAccountStatusChangeAndWait:(dispatch_time_t)timeout
428 return dispatch_semaphore_wait([self notifyCKAccountStatusChange:nil], dispatch_time(DISPATCH_TIME_NOW, timeout)) == 0;
431 -(void)notifyCKAccountStatusChangeAndWaitForSignal {
432 [self notifyCKAccountStatusChangeAndWait:DISPATCH_TIME_FOREVER];
435 -(void)notifyCircleStatusChangeAndWaitForSignal {
436 dispatch_semaphore_wait([self notifyCircleChange: nil], DISPATCH_TIME_FOREVER);
439 -(dispatch_group_t)checkForAllDeliveries {
441 dispatch_group_t g = dispatch_group_create();
443 ckksnotice_global("ckksaccount", "Unable to get dispatch group.");
447 dispatch_sync(self.queue, ^{
448 NSEnumerator *enumerator = [self.ckChangeListeners keyEnumerator];
451 // Queue up the changes for each listener.
452 while ((dq = [enumerator nextObject])) {
453 id<CKKSCloudKitAccountStateListener> listener = [self.ckChangeListeners objectForKey: dq];
455 ckksinfo_global("ckksaccountblock", "Starting blocking for listener %@", listener);
457 dispatch_group_async(g, dq, ^{
459 // Do nothing in particular. It's just important that this block runs.
460 ckksinfo_global("ckksaccountblock", "Done blocking for listener %@", listener);
468 // This is its own function to allow OCMock to swoop in and replace the result during testing.
469 + (SOSAccountStatus*)getCircleStatus {
470 CFErrorRef cferror = NULL;
472 SOSCCStatus status = SOSCCThisDeviceIsInCircle(&cferror);
474 ckkserror_global("ckksaccount", "error getting circle status: %@", cferror);
475 return [[SOSAccountStatus alloc] init:kSOSCCError error:CFBridgingRelease(cferror)];
478 return [[SOSAccountStatus alloc] init:status error:nil];
481 + (NSString*)stringFromAccountStatus: (CKKSAccountStatus) status {
483 case CKKSAccountStatusUnknown: return @"account state unknown";
484 case CKKSAccountStatusAvailable: return @"logged in";
485 case CKKSAccountStatusNoAccount: return @"no account";
489 - (void)triggerOctagonStatusFetch
493 __block CKKSCondition* blockPointer = nil;
494 dispatch_sync(self.queue, ^{
495 self.octagonInformationInitialized = [[CKKSCondition alloc] initToChain:self.octagonInformationInitialized];
496 blockPointer = self.octagonInformationInitialized;
499 // Explicitly do not use the OTClique API, as that might include SOS status as well
500 OTOperationConfiguration* config = [[OTOperationConfiguration alloc] init];
501 config.timeoutWaitForCKAccount = 100*NSEC_PER_MSEC;
502 [[OTManager manager] fetchTrustStatus:nil
503 context:OTDefaultContext
505 reply:^(CliqueStatus status, NSString * _Nullable peerID, NSNumber * _Nullable numberOfPeersInOctagon, BOOL isExcluded, NSError * _Nullable error) {
508 dispatch_sync(self.queue, ^{
510 ckkserror_global("ckksaccount", "error getting octagon status: %@", error);
511 self.octagonStatus = [[OTCliqueStatusWrapper alloc] initWithStatus:CliqueStatusError];
513 ckksnotice_global("ckksaccount", "Caching octagon status as (%@, %@)", OTCliqueStatusToString(status), peerID);
514 self.octagonStatus = [[OTCliqueStatusWrapper alloc] initWithStatus:status];
517 self.octagonPeerID = peerID;
518 [blockPointer fulfill];
524 - (void)setHSA2iCloudAccountStatus:(CKKSAccountStatus)status
526 self.hsa2iCloudAccountStatus = status;
527 if(status == CKKSAccountStatusUnknown) {
528 self.hsa2iCloudAccountInitialized = [[CKKSCondition alloc] initToChain:self.hsa2iCloudAccountInitialized];
530 [self.hsa2iCloudAccountInitialized fulfill];
536 @implementation SOSAccountStatus
537 - (instancetype)init:(SOSCCStatus)status error:(NSError*)error
539 if((self = [super init])) {
546 - (BOOL)isEqual:(id)object
548 if(![object isKindOfClass:[SOSAccountStatus class]]) {
556 SOSAccountStatus* obj = (SOSAccountStatus*) object;
557 return self.status == obj.status &&
558 ((self.error == nil && obj.error == nil) || [self.error isEqual:obj.error]);
561 - (NSString*)description
563 return [NSString stringWithFormat:@"<SOSStatus: %@ (%@)>", SOSCCGetStatusDescription(self.status), self.error];
568 @implementation OTCliqueStatusWrapper
569 - (instancetype)initWithStatus:(CliqueStatus)status
571 if((self = [super init])) {
577 - (BOOL)isEqual:(id)object
579 if(![object isKindOfClass:[OTCliqueStatusWrapper class]]) {
583 OTCliqueStatusWrapper* obj = (OTCliqueStatusWrapper*)object;
584 return obj.status == self.status;
586 - (NSString*)description
588 return [NSString stringWithFormat:@"<CliqueStatus: %@>", OTCliqueStatusToString(self.status)];