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 dispatch_sync(self.queue, ^{
173 self.firstSOSCircleFetch = true;
175 [self _onqueueUpdateAccountState:self.currentCKAccountInfo circle:circleStatus deliveredSemaphore:finishedSema];
180 // Takes the new ckAccountInfo we're moving to
181 -(void)_onqueueUpdateCKDeviceID: (CKAccountInfo*)ckAccountInfo {
182 dispatch_assert_queue(self.queue);
183 __weak __typeof(self) weakSelf = self;
185 // If we're in an account, opportunistically fill in the device id
186 if(ckAccountInfo.accountStatus == CKAccountStatusAvailable) {
187 [self.container fetchCurrentDeviceIDWithCompletionHandler:^(NSString* deviceID, NSError* ckerror) {
188 __strong __typeof(self) strongSelf = weakSelf;
190 // Make sure you synchronize here; if we've logged out before the callback returns, don't record the result
191 dispatch_async(strongSelf.queue, ^{
192 __strong __typeof(self) innerStrongSelf = weakSelf;
193 if(innerStrongSelf.currentCKAccountInfo.accountStatus == CKAccountStatusAvailable) {
194 secnotice("ckksaccount", "CloudKit deviceID is: %@ %@", deviceID, ckerror);
196 innerStrongSelf.ckdeviceID = deviceID;
197 innerStrongSelf.ckdeviceIDError = ckerror;
198 [innerStrongSelf.ckdeviceIDInitialized fulfill];
200 // Logged out! No ckdeviceid.
201 secerror("ckksaccount: Logged back out but still received a fetchCurrentDeviceIDWithCompletionHandler callback");
203 innerStrongSelf.ckdeviceID = nil;
204 innerStrongSelf.ckdeviceIDError = nil;
205 // Don't touch the ckdeviceIDInitialized object; it should have been reset when the logout happened.
210 // Logging out? no more device ID.
211 self.ckdeviceID = nil;
212 self.ckdeviceIDError = nil;
213 self.ckdeviceIDInitialized = [[CKKSCondition alloc] init];
217 // Pulled out for mocking purposes
218 +(void)fetchCirclePeerID:(void (^)(NSString* _Nullable peerID, NSError* _Nullable error))callback {
219 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
220 CFErrorRef cferror = nil;
221 SOSPeerInfoRef egoPeerInfo = SOSCCCopyMyPeerInfo(&cferror);
222 NSString* egoPeerID = egoPeerInfo ? (NSString*)CFBridgingRelease(CFRetainSafe(SOSPeerInfoGetPeerID(egoPeerInfo))) : nil;
223 CFReleaseNull(egoPeerInfo);
225 callback(egoPeerID, CFBridgingRelease(cferror));
229 // Takes the new ckAccountInfo we're moving to
230 -(void)_onqueueUpdateCirclePeerID: (SOSCCStatus)sosccstatus {
231 dispatch_assert_queue(self.queue);
232 __weak __typeof(self) weakSelf = self;
234 // If we're in a circle, fetch the peer id
235 if(sosccstatus == kSOSCCInCircle) {
236 [CKKSCKAccountStateTracker fetchCirclePeerID:^(NSString* peerID, NSError* error) {
237 __strong __typeof(self) strongSelf = weakSelf;
238 dispatch_async(strongSelf.queue, ^{
239 __strong __typeof(self) innerstrongSelf = weakSelf;
241 if(innerstrongSelf.currentCircleStatus == kSOSCCInCircle) {
242 secnotice("ckksaccount", "Circle peerID is: %@ %@", peerID, error);
243 // Still in circle. Proceed.
244 innerstrongSelf.accountCirclePeerID = peerID;
245 innerstrongSelf.accountCirclePeerIDError = error;
246 [innerstrongSelf.accountCirclePeerIDInitialized fulfill];
248 secerror("ckksaccount: Out of circle but still received a fetchCirclePeerID callback");
249 // Not in-circle. Throw away circle id.
250 strongSelf.accountCirclePeerID = nil;
251 strongSelf.accountCirclePeerIDError = nil;
252 // Don't touch the accountCirclePeerIDInitialized object; it should have been reset when the logout happened.
257 // Not in-circle, reset circle ID
258 secnotice("ckksaccount", "out of circle(%d): resetting peer ID", sosccstatus);
259 self.accountCirclePeerID = nil;
260 self.accountCirclePeerIDError = nil;
261 self.accountCirclePeerIDInitialized = [[CKKSCondition alloc] init];
265 -(void)_onqueueUpdateAccountState: (CKAccountInfo*) ckAccountInfo circle: (SOSCCStatus) sosccstatus deliveredSemaphore: (dispatch_semaphore_t) finishedSema {
266 dispatch_assert_queue(self.queue);
268 if([self.currentCKAccountInfo isEqual: ckAccountInfo] && self.currentCircleStatus == sosccstatus) {
270 secinfo("ckksaccount", "received another notification of CK Account State %@ and Circle status %d", ckAccountInfo, (int)sosccstatus);
271 dispatch_semaphore_signal(finishedSema);
275 if(![self.currentCKAccountInfo isEqual: ckAccountInfo]) {
276 secnotice("ckksaccount", "moving to CK Account info: %@", ckAccountInfo);
277 self.currentCKAccountInfo = ckAccountInfo;
279 [self _onqueueUpdateCKDeviceID: ckAccountInfo];
281 if(self.currentCircleStatus != sosccstatus) {
282 secnotice("ckksaccount", "moving to circle status: %@", SOSCCGetStatusDescription(sosccstatus));
283 self.currentCircleStatus = sosccstatus;
285 [self _onqueueUpdateCirclePeerID: sosccstatus];
288 if(!self.firstSOSCircleFetch || !self.firstCKAccountFetch) {
289 secnotice("ckksaccount", "Haven't received updates from all sources; not passing update along: %@", self);
290 dispatch_semaphore_signal(finishedSema);
294 // We are CKKSAccountStatusAvailable if we are:
295 // in CKAccountStatusAvailable
297 // and supportsDeviceToDeviceEncryption == true
298 CKKSAccountStatus oldComputedStatus = self.currentComputedAccountStatus;
300 if(self.currentCKAccountInfo) {
301 if(self.currentCKAccountInfo.accountStatus == CKAccountStatusAvailable) {
302 // CloudKit thinks we're logged in. Double check!
303 if(self.currentCKAccountInfo.supportsDeviceToDeviceEncryption && self.currentCircleStatus == kSOSCCInCircle) {
304 self.currentComputedAccountStatus = CKKSAccountStatusAvailable;
306 self.currentComputedAccountStatus = CKKSAccountStatusNoAccount;
310 // Account status is not CKAccountStatusAvailable; no more checking required.
311 self.currentComputedAccountStatus = CKKSAccountStatusNoAccount;
314 // No CKAccountInfo? We haven't received an update from cloudd yet; Change nothing.
317 if(oldComputedStatus == self.currentComputedAccountStatus) {
318 secnotice("ckksaccount", "No change in computed account status: %@ (%@ %@)",
319 [self currentStatus],
320 self.currentCKAccountInfo,
321 SOSCCGetStatusDescription(self.currentCircleStatus));
322 dispatch_semaphore_signal(finishedSema);
326 secnotice("ckksaccount", "New computed account status: %@ (%@ %@)",
327 [self currentStatus],
328 self.currentCKAccountInfo,
329 SOSCCGetStatusDescription(self.currentCircleStatus));
331 dispatch_group_t g = dispatch_group_create();
333 secnotice("ckksaccount", "Unable to get dispatch group.");
337 NSEnumerator *enumerator = [self.changeListeners keyEnumerator];
340 // Queue up the changes for each listener.
341 while ((dq = [enumerator nextObject])) {
342 id<CKKSAccountStateListener> listener = [self.changeListeners objectForKey: dq];
343 __weak __typeof(listener) weakListener = listener;
346 dispatch_group_async(g, dq, ^{
347 [weakListener ckAccountStatusChange: oldComputedStatus to: self.currentComputedAccountStatus];
352 dispatch_group_notify(g, self.queue, ^{
353 dispatch_semaphore_signal(finishedSema);
357 -(void)notifyCKAccountStatusChangeAndWaitForSignal {
358 dispatch_semaphore_wait([self notifyCKAccountStatusChange: nil], DISPATCH_TIME_FOREVER);
361 -(void)notifyCircleStatusChangeAndWaitForSignal {
362 dispatch_semaphore_wait([self notifyCircleChange: nil], DISPATCH_TIME_FOREVER);
365 // This is its own function to allow OCMock to swoop in and replace the result during testing.
366 +(SOSCCStatus)getCircleStatus {
367 CFErrorRef cferror = NULL;
369 SOSCCStatus status = SOSCCThisDeviceIsInCircle(&cferror);
371 secerror("ckksaccount: error getting circle status: %@", cferror);
372 CFReleaseNull(cferror);
378 -(NSString*)currentStatus {
379 return [CKKSCKAccountStateTracker stringFromAccountStatus:self.currentComputedAccountStatus];
382 +(NSString*)stringFromAccountStatus: (CKKSAccountStatus) status {
384 case CKKSAccountStatusUnknown: return @"account state unknown";
385 case CKKSAccountStatusAvailable: return @"logged in";
386 case CKKSAccountStatusNoAccount: return @"no account";