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 _finishedInitialCalls = [[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.finishedInitialCalls 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 -(CKKSAccountStatus)currentCKAccountStatusAndNotifyOnChange: (id<CKKSAccountStateListener>) listener {
124 __block CKKSAccountStatus status = CKKSAccountStatusUnknown;
126 dispatch_sync(self.queue, ^{
127 status = self.currentComputedAccountStatus;
129 bool alreadyRegisteredListener = false;
130 NSEnumerator *enumerator = [self.changeListeners objectEnumerator];
131 id<CKKSAccountStateListener> value;
133 while ((value = [enumerator nextObject])) {
134 // do pointer comparison
135 alreadyRegisteredListener |= (value == listener);
138 if(listener && !alreadyRegisteredListener) {
139 NSString* queueName = [NSString stringWithFormat: @"ck-account-state-%@", listener];
141 dispatch_queue_t objQueue = dispatch_queue_create([queueName UTF8String], DISPATCH_QUEUE_SERIAL);
142 [self.changeListeners setObject: listener forKey: objQueue];
148 - (dispatch_semaphore_t)notifyCKAccountStatusChange:(__unused id)object {
149 // signals when this notify is Complete, including all downcalls.
150 dispatch_semaphore_t finishedSema = dispatch_semaphore_create(0);
152 [self.container accountInfoWithCompletionHandler:^(CKAccountInfo* ckAccountInfo, NSError * _Nullable error) {
154 secerror("ckksaccount: error getting account info: %@", error);
155 dispatch_semaphore_signal(finishedSema);
159 dispatch_sync(self.queue, ^{
160 self.firstCKAccountFetch = true;
161 [self _onqueueUpdateAccountState:ckAccountInfo circle:self.currentCircleStatus deliveredSemaphore:finishedSema];
168 -(dispatch_semaphore_t)notifyCircleChange:(__unused id)object {
169 dispatch_semaphore_t finishedSema = dispatch_semaphore_create(0);
171 SOSCCStatus circleStatus = [CKKSCKAccountStateTracker getCircleStatus];
172 if(circleStatus == kSOSCCError) {
173 dispatch_semaphore_signal(finishedSema);
177 dispatch_sync(self.queue, ^{
178 self.firstSOSCircleFetch = true;
180 [self _onqueueUpdateAccountState:self.currentCKAccountInfo circle:circleStatus deliveredSemaphore:finishedSema];
185 // Takes the new ckAccountInfo we're moving to
186 -(void)_onqueueUpdateCKDeviceID: (CKAccountInfo*)ckAccountInfo {
187 dispatch_assert_queue(self.queue);
188 __weak __typeof(self) weakSelf = self;
190 // If we're in an account, opportunistically fill in the device id
191 if(ckAccountInfo.accountStatus == CKAccountStatusAvailable) {
192 [self.container fetchCurrentDeviceIDWithCompletionHandler:^(NSString* deviceID, NSError* ckerror) {
193 __strong __typeof(self) strongSelf = weakSelf;
195 // Make sure you synchronize here; if we've logged out before the callback returns, don't record the result
196 dispatch_async(strongSelf.queue, ^{
197 __strong __typeof(self) innerStrongSelf = weakSelf;
198 if(innerStrongSelf.currentCKAccountInfo.accountStatus == CKAccountStatusAvailable) {
199 secnotice("ckksaccount", "CloudKit deviceID is: %@ %@", deviceID, ckerror);
201 innerStrongSelf.ckdeviceID = deviceID;
202 innerStrongSelf.ckdeviceIDError = ckerror;
203 [innerStrongSelf.ckdeviceIDInitialized fulfill];
205 // Logged out! No ckdeviceid.
206 secerror("ckksaccount: Logged back out but still received a fetchCurrentDeviceIDWithCompletionHandler callback");
208 innerStrongSelf.ckdeviceID = nil;
209 innerStrongSelf.ckdeviceIDError = nil;
210 // Don't touch the ckdeviceIDInitialized object; it should have been reset when the logout happened.
215 // Logging out? no more device ID.
216 self.ckdeviceID = nil;
217 self.ckdeviceIDError = nil;
218 self.ckdeviceIDInitialized = [[CKKSCondition alloc] init];
222 // Pulled out for mocking purposes
223 +(void)fetchCirclePeerID:(void (^)(NSString* _Nullable peerID, NSError* _Nullable error))callback {
224 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
225 CFErrorRef cferror = nil;
226 SOSPeerInfoRef egoPeerInfo = SOSCCCopyMyPeerInfo(&cferror);
227 NSString* egoPeerID = egoPeerInfo ? (NSString*)CFBridgingRelease(CFRetainSafe(SOSPeerInfoGetPeerID(egoPeerInfo))) : nil;
228 CFReleaseNull(egoPeerInfo);
230 callback(egoPeerID, CFBridgingRelease(cferror));
234 // Takes the new ckAccountInfo we're moving to
235 -(void)_onqueueUpdateCirclePeerID: (SOSCCStatus)sosccstatus {
236 dispatch_assert_queue(self.queue);
237 __weak __typeof(self) weakSelf = self;
239 // If we're in a circle, fetch the peer id
240 if(sosccstatus == kSOSCCInCircle) {
241 [CKKSCKAccountStateTracker fetchCirclePeerID:^(NSString* peerID, NSError* error) {
242 __strong __typeof(self) strongSelf = weakSelf;
243 dispatch_async(strongSelf.queue, ^{
244 __strong __typeof(self) innerstrongSelf = weakSelf;
246 if(innerstrongSelf.currentCircleStatus == kSOSCCInCircle) {
247 secnotice("ckksaccount", "Circle peerID is: %@ %@", peerID, error);
248 // Still in circle. Proceed.
249 innerstrongSelf.accountCirclePeerID = peerID;
250 innerstrongSelf.accountCirclePeerIDError = error;
251 [innerstrongSelf.accountCirclePeerIDInitialized fulfill];
253 secerror("ckksaccount: Out of circle but still received a fetchCirclePeerID callback");
254 // Not in-circle. Throw away circle id.
255 strongSelf.accountCirclePeerID = nil;
256 strongSelf.accountCirclePeerIDError = nil;
257 // Don't touch the accountCirclePeerIDInitialized object; it should have been reset when the logout happened.
262 // Not in-circle, reset circle ID
263 self.accountCirclePeerID = nil;
264 self.accountCirclePeerIDError = nil;
265 self.accountCirclePeerIDInitialized = [[CKKSCondition alloc] init];
269 -(void)_onqueueUpdateAccountState: (CKAccountInfo*) ckAccountInfo circle: (SOSCCStatus) sosccstatus deliveredSemaphore: (dispatch_semaphore_t) finishedSema {
270 dispatch_assert_queue(self.queue);
272 if([self.currentCKAccountInfo isEqual: ckAccountInfo] && self.currentCircleStatus == sosccstatus) {
274 secinfo("ckksaccount", "received another notification of CK Account State %@ and Circle status %d", ckAccountInfo, (int)sosccstatus);
275 dispatch_semaphore_signal(finishedSema);
279 if(![self.currentCKAccountInfo isEqual: ckAccountInfo]) {
280 secnotice("ckksaccount", "moving to CK Account info: %@", ckAccountInfo);
281 self.currentCKAccountInfo = ckAccountInfo;
283 [self _onqueueUpdateCKDeviceID: ckAccountInfo];
285 if(self.currentCircleStatus != sosccstatus) {
286 secnotice("ckksaccount", "moving to circle status: %@", SOSCCGetStatusDescription(sosccstatus));
287 self.currentCircleStatus = sosccstatus;
289 [self _onqueueUpdateCirclePeerID: sosccstatus];
292 if(!self.firstSOSCircleFetch || !self.firstCKAccountFetch) {
293 secnotice("ckksaccount", "Haven't received updates from all sources; not passing update along: %@", self);
294 dispatch_semaphore_signal(finishedSema);
298 // We are CKKSAccountStatusAvailable if we are:
299 // in CKAccountStatusAvailable
301 // and supportsDeviceToDeviceEncryption == true
302 CKKSAccountStatus oldComputedStatus = self.currentComputedAccountStatus;
304 if(self.currentCKAccountInfo) {
305 if(self.currentCKAccountInfo.accountStatus == CKAccountStatusAvailable) {
306 // CloudKit thinks we're logged in. Double check!
307 if(self.currentCKAccountInfo.supportsDeviceToDeviceEncryption && self.currentCircleStatus == kSOSCCInCircle) {
308 self.currentComputedAccountStatus = CKKSAccountStatusAvailable;
310 self.currentComputedAccountStatus = CKKSAccountStatusNoAccount;
314 // Account status is not CKAccountStatusAvailable; no more checking required.
315 self.currentComputedAccountStatus = CKKSAccountStatusNoAccount;
318 // No CKAccountInfo? We haven't received an update from cloudd yet; Change nothing.
321 if(oldComputedStatus == self.currentComputedAccountStatus) {
322 secnotice("ckksaccount", "No change in computed account status: %@ (%@ %@)",
323 [self currentStatus],
324 self.currentCKAccountInfo,
325 SOSCCGetStatusDescription(self.currentCircleStatus));
326 dispatch_semaphore_signal(finishedSema);
330 secnotice("ckksaccount", "New computed account status: %@ (%@ %@)",
331 [self currentStatus],
332 self.currentCKAccountInfo,
333 SOSCCGetStatusDescription(self.currentCircleStatus));
335 dispatch_group_t g = dispatch_group_create();
337 secnotice("ckksaccount", "Unable to get dispatch group.");
341 NSEnumerator *enumerator = [self.changeListeners keyEnumerator];
344 // Queue up the changes for each listener.
345 while ((dq = [enumerator nextObject])) {
346 id<CKKSAccountStateListener> listener = [self.changeListeners objectForKey: dq];
347 __weak __typeof(listener) weakListener = listener;
350 dispatch_group_async(g, dq, ^{
351 [weakListener ckAccountStatusChange: oldComputedStatus to: self.currentComputedAccountStatus];
356 dispatch_group_notify(g, self.queue, ^{
357 dispatch_semaphore_signal(finishedSema);
361 -(void)notifyCKAccountStatusChangeAndWaitForSignal {
362 dispatch_semaphore_wait([self notifyCKAccountStatusChange: nil], DISPATCH_TIME_FOREVER);
365 -(void)notifyCircleStatusChangeAndWaitForSignal {
366 dispatch_semaphore_wait([self notifyCircleChange: nil], DISPATCH_TIME_FOREVER);
369 // This is its own function to allow OCMock to swoop in and replace the result during testing.
370 +(SOSCCStatus)getCircleStatus {
371 CFErrorRef cferror = NULL;
373 SOSCCStatus status = SOSCCThisDeviceIsInCircle(&cferror);
375 secerror("ckksaccount: error getting circle status: %@", cferror);
376 CFReleaseNull(cferror);
382 -(NSString*)currentStatus {
383 return [CKKSCKAccountStateTracker stringFromAccountStatus:self.currentComputedAccountStatus];
386 +(NSString*)stringFromAccountStatus: (CKKSAccountStatus) status {
388 case CKKSAccountStatusUnknown: return @"account state unknown";
389 case CKKSAccountStatusAvailable: return @"logged in";
390 case CKKSAccountStatusNoAccount: return @"no account";