]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSCKAccountStateTracker.m
9c340e19817a8de74d675f174b964cb5f477bdbe
[apple/security.git] / keychain / ckks / CKKSCKAccountStateTracker.m
1 /*
2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
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
11 * file.
12 *
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.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 #if OCTAGON
25
26 #include <dispatch/dispatch.h>
27 #include <utilities/debugging.h>
28 #include <Security/SecureObjectSync/SOSCloudCircle.h>
29 #include <Security/SecureObjectSync/SOSCloudCircleInternal.h>
30 #include <notify.h>
31
32 #import "keychain/ckks/CKKS.h"
33 #import "keychain/ckks/CKKSCKAccountStateTracker.h"
34
35
36 @interface CKKSCKAccountStateTracker ()
37 @property (readonly) Class<CKKSNSNotificationCenter> nsnotificationCenterClass;
38
39 @property CKKSAccountStatus currentComputedAccountStatus;
40
41 @property dispatch_queue_t queue;
42
43 @property NSMapTable<dispatch_queue_t, id<CKKSAccountStateListener>>* changeListeners;
44 @property CKContainer* container; // used only for fetching the CKAccountStatus
45
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;
50 @end
51
52 @implementation CKKSCKAccountStateTracker
53
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;
60
61 _currentComputedAccountStatus = CKKSAccountStatusUnknown;
62
63 _container = container;
64
65 _queue = dispatch_queue_create("ck-account-state", DISPATCH_QUEUE_SERIAL);
66
67 _firstCKAccountFetch = false;
68 _firstSOSCircleFetch = false;
69
70 _finishedInitialDispatches = [[CKKSCondition alloc] init];
71 _ckdeviceIDInitialized = [[CKKSCondition alloc] init];
72
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];
76
77 __weak __typeof(self) weakSelf = self;
78
79 // If this is a live server, register with notify
80 if(!SecCKKSTestsEnabled()) {
81 int token = 0;
82 notify_register_dispatch(kSOSCCCircleChangedNotification, &token, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^(int t) {
83 [weakSelf notifyCircleChange:nil];
84 });
85 }
86
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;
90 if(!strongSelf) {
91 return;
92 }
93 [strongSelf notifyCKAccountStatusChange:nil];
94 [strongSelf notifyCircleChange:nil];
95 [strongSelf.finishedInitialDispatches fulfill];
96 });
97 }
98 return self;
99 }
100
101 -(void)dealloc {
102 id<CKKSNSNotificationCenter> notificationCenter = [self.nsnotificationCenterClass defaultCenter];
103 [notificationCenter removeObserver:self];
104 }
105
106 -(NSString*)descriptionInternal: (NSString*) selfString {
107 return [NSString stringWithFormat:@"<%@: %@ (%@ %@)",
108 selfString,
109 [self currentStatus],
110 self.currentCKAccountInfo,
111 SOSCCGetStatusDescription(self.currentCircleStatus)];
112 }
113
114 -(NSString*)description {
115 return [self descriptionInternal: [[self class] description]];
116 }
117
118 -(NSString*)debugDescription {
119 return [self descriptionInternal: [super description]];
120 }
121
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);
125
126 dispatch_async(self.queue, ^{
127 bool alreadyRegisteredListener = false;
128 NSEnumerator *enumerator = [self.changeListeners objectEnumerator];
129 id<CKKSAccountStateListener> value;
130
131 while ((value = [enumerator nextObject])) {
132 // do pointer comparison
133 alreadyRegisteredListener |= (value == listener);
134 }
135
136 if(listener && !alreadyRegisteredListener) {
137 NSString* queueName = [NSString stringWithFormat: @"ck-account-state-%@", listener];
138
139 dispatch_queue_t objQueue = dispatch_queue_create([queueName UTF8String], DISPATCH_QUEUE_SERIAL);
140 [self.changeListeners setObject: listener forKey: objQueue];
141
142 // If we know the current account status, let this listener know
143 if(self.currentComputedAccountStatus != CKKSAccountStatusUnknown) {
144
145 dispatch_group_t g = dispatch_group_create();
146 if(!g) {
147 secnotice("ckksaccount", "Unable to get dispatch group.");
148 return;
149 }
150
151 [self _onqueueDeliverCurrentState:listener listenerQueue:objQueue oldStatus:CKKSAccountStatusUnknown group:g];
152
153 dispatch_group_notify(g, self.queue, ^{
154 dispatch_semaphore_signal(finishedSema);
155 });
156 } else {
157 dispatch_semaphore_signal(finishedSema);
158 }
159 } else {
160 dispatch_semaphore_signal(finishedSema);
161 }
162 });
163
164 return finishedSema;
165 }
166
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);
170
171 [self.container accountInfoWithCompletionHandler:^(CKAccountInfo* ckAccountInfo, NSError * _Nullable error) {
172 if(error) {
173 secerror("ckksaccount: error getting account info: %@", error);
174 dispatch_semaphore_signal(finishedSema);
175 return;
176 }
177
178 dispatch_sync(self.queue, ^{
179 self.firstCKAccountFetch = true;
180 [self _onqueueUpdateAccountState:ckAccountInfo circle:self.currentCircleStatus deliveredSemaphore:finishedSema];
181 });
182 }];
183
184 return finishedSema;
185 }
186
187 -(dispatch_semaphore_t)notifyCircleChange:(__unused id)object {
188 dispatch_semaphore_t finishedSema = dispatch_semaphore_create(0);
189
190 SOSCCStatus circleStatus = [CKKSCKAccountStateTracker getCircleStatus];
191 dispatch_sync(self.queue, ^{
192 self.firstSOSCircleFetch = true;
193
194 [self _onqueueUpdateAccountState:self.currentCKAccountInfo circle:circleStatus deliveredSemaphore:finishedSema];
195 });
196 return finishedSema;
197 }
198
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;
203
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;
208
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);
214
215 innerStrongSelf.ckdeviceID = deviceID;
216 innerStrongSelf.ckdeviceIDError = ckerror;
217 [innerStrongSelf.ckdeviceIDInitialized fulfill];
218 } else {
219 // Logged out! No ckdeviceid.
220 secerror("ckksaccount: Logged back out but still received a fetchCurrentDeviceIDWithCompletionHandler callback");
221
222 innerStrongSelf.ckdeviceID = nil;
223 innerStrongSelf.ckdeviceIDError = nil;
224 // Don't touch the ckdeviceIDInitialized object; it should have been reset when the logout happened.
225 }
226 });
227 }];
228 } else {
229 // Logging out? no more device ID.
230 self.ckdeviceID = nil;
231 self.ckdeviceIDError = nil;
232 self.ckdeviceIDInitialized = [[CKKSCondition alloc] init];
233 }
234 }
235
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);
243
244 callback(egoPeerID, CFBridgingRelease(cferror));
245 });
246 }
247
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;
252
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;
259
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];
266 } else {
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.
272 }
273 });
274 }];
275 } else {
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];
281 }
282 }
283
284 -(void)_onqueueUpdateAccountState:(CKAccountInfo*)ckAccountInfo circle:(SOSCCStatus)sosccstatus deliveredSemaphore:(dispatch_semaphore_t)finishedSema {
285 dispatch_assert_queue(self.queue);
286
287 if([self.currentCKAccountInfo isEqual: ckAccountInfo] && self.currentCircleStatus == sosccstatus) {
288 // no-op.
289 secinfo("ckksaccount", "received another notification of CK Account State %@ and Circle status %d", ckAccountInfo, (int)sosccstatus);
290 dispatch_semaphore_signal(finishedSema);
291 return;
292 }
293
294 if(![self.currentCKAccountInfo isEqual: ckAccountInfo]) {
295 secnotice("ckksaccount", "moving to CK Account info: %@", ckAccountInfo);
296 self.currentCKAccountInfo = ckAccountInfo;
297
298 [self _onqueueUpdateCKDeviceID: ckAccountInfo];
299 }
300 if(self.currentCircleStatus != sosccstatus) {
301 secnotice("ckksaccount", "moving to circle status: %@", SOSCCGetStatusDescription(sosccstatus));
302 self.currentCircleStatus = sosccstatus;
303
304 [self _onqueueUpdateCirclePeerID: sosccstatus];
305 }
306
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);
310 return;
311 }
312
313 // We are CKKSAccountStatusAvailable if we are:
314 // in CKAccountStatusAvailable
315 // and in circle
316 // and supportsDeviceToDeviceEncryption == true
317 CKKSAccountStatus oldComputedStatus = self.currentComputedAccountStatus;
318
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;
324 } else {
325 self.currentComputedAccountStatus = CKKSAccountStatusNoAccount;
326 }
327
328 } else {
329 // Account status is not CKAccountStatusAvailable; no more checking required.
330 self.currentComputedAccountStatus = CKKSAccountStatusNoAccount;
331 }
332 } else {
333 // No CKAccountInfo? We haven't received an update from cloudd yet; Change nothing.
334 }
335
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);
342 return;
343 }
344
345 secnotice("ckksaccount", "New computed account status: %@ (%@ %@)",
346 [self currentStatus],
347 self.currentCKAccountInfo,
348 SOSCCGetStatusDescription(self.currentCircleStatus));
349
350 [self _onqueueDeliverStateChanges:oldComputedStatus deliveredSemaphore:finishedSema];
351 }
352
353 -(void)_onqueueDeliverStateChanges:(CKKSAccountStatus)oldStatus deliveredSemaphore:(dispatch_semaphore_t)finishedSema {
354 dispatch_assert_queue(self.queue);
355
356 dispatch_group_t g = dispatch_group_create();
357 if(!g) {
358 secnotice("ckksaccount", "Unable to get dispatch group.");
359 return;
360 }
361
362 NSEnumerator *enumerator = [self.changeListeners keyEnumerator];
363 dispatch_queue_t dq;
364
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];
369 }
370
371 dispatch_group_notify(g, self.queue, ^{
372 dispatch_semaphore_signal(finishedSema);
373 });
374 }
375
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);
378
379 __weak __typeof(listener) weakListener = listener;
380
381 if(listener) {
382 dispatch_group_async(g, listenerQueue, ^{
383 [weakListener ckAccountStatusChange:oldStatus to:self.currentComputedAccountStatus];
384 });
385 }
386 }
387
388 -(void)notifyCKAccountStatusChangeAndWaitForSignal {
389 dispatch_semaphore_wait([self notifyCKAccountStatusChange: nil], DISPATCH_TIME_FOREVER);
390 }
391
392 -(void)notifyCircleStatusChangeAndWaitForSignal {
393 dispatch_semaphore_wait([self notifyCircleChange: nil], DISPATCH_TIME_FOREVER);
394 }
395
396 -(dispatch_group_t)checkForAllDeliveries {
397
398 dispatch_group_t g = dispatch_group_create();
399 if(!g) {
400 secnotice("ckksaccount", "Unable to get dispatch group.");
401 return nil;
402 }
403
404 dispatch_sync(self.queue, ^{
405 NSEnumerator *enumerator = [self.changeListeners keyEnumerator];
406 dispatch_queue_t dq;
407
408 // Queue up the changes for each listener.
409 while ((dq = [enumerator nextObject])) {
410 id<CKKSAccountStateListener> listener = [self.changeListeners objectForKey: dq];
411
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);
418 });
419 }
420 });
421
422 return g;
423 }
424
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;
428
429 SOSCCStatus status = SOSCCThisDeviceIsInCircle(&cferror);
430 if(cferror) {
431 secerror("ckksaccount: error getting circle status: %@", cferror);
432 CFReleaseNull(cferror);
433 return kSOSCCError;
434 }
435 return status;
436 }
437
438 -(NSString*)currentStatus {
439 return [CKKSCKAccountStateTracker stringFromAccountStatus:self.currentComputedAccountStatus];
440 }
441
442 +(NSString*)stringFromAccountStatus: (CKKSAccountStatus) status {
443 switch(status) {
444 case CKKSAccountStatusUnknown: return @"account state unknown";
445 case CKKSAccountStatusAvailable: return @"logged in";
446 case CKKSAccountStatusNoAccount: return @"no account";
447 }
448 }
449
450 @end
451
452 #endif // OCTAGON