]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSCKAccountStateTracker.m
Security-58286.51.6.tar.gz
[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 <Security/SecureObjectSync/SOSInternal.h>
31 #include <notify.h>
32
33 #import "keychain/ckks/CKKS.h"
34 #import "keychain/ckks/CloudKitCategories.h"
35 #import "keychain/ckks/CKKSCKAccountStateTracker.h"
36 #import "keychain/ckks/CKKSAnalytics.h"
37
38 @interface CKKSCKAccountStateTracker ()
39 @property (readonly) Class<CKKSNSNotificationCenter> nsnotificationCenterClass;
40
41 @property CKKSAccountStatus currentComputedAccountStatus;
42 @property (nullable, atomic) NSError* currentAccountError;
43
44 @property dispatch_queue_t queue;
45
46 @property NSMapTable<dispatch_queue_t, id<CKKSAccountStateListener>>* changeListeners;
47 @property CKContainer* container; // used only for fetching the CKAccountStatus
48
49 /* We have initialization races. We should report CKKSAccountStatusUnknown until both of
50 * these are true, otherwise on a race, it looks like we logged out. */
51 @property bool firstCKAccountFetch;
52 @property bool firstSOSCircleFetch;
53 @end
54
55 @implementation CKKSCKAccountStateTracker
56
57 -(instancetype)init: (CKContainer*) container nsnotificationCenterClass: (Class<CKKSNSNotificationCenter>) nsnotificationCenterClass {
58 if((self = [super init])) {
59 _nsnotificationCenterClass = nsnotificationCenterClass;
60 _changeListeners = [NSMapTable strongToWeakObjectsMapTable]; // Backwards from how we'd like, but it's the best way to have weak pointers to CKKSAccountStateListener.
61 _currentCKAccountInfo = nil;
62 _currentCircleStatus = kSOSCCError;
63
64 _currentComputedAccountStatus = CKKSAccountStatusUnknown;
65 _currentComputedAccountStatusValid = [[CKKSCondition alloc] init];
66
67 _container = container;
68
69 _queue = dispatch_queue_create("ck-account-state", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
70
71 _firstCKAccountFetch = false;
72 _firstSOSCircleFetch = false;
73
74 _finishedInitialDispatches = [[CKKSCondition alloc] init];
75 _ckdeviceIDInitialized = [[CKKSCondition alloc] init];
76
77 id<CKKSNSNotificationCenter> notificationCenter = [self.nsnotificationCenterClass defaultCenter];
78 secinfo("ckksaccount", "Registering with notification center %@", notificationCenter);
79 [notificationCenter addObserver:self selector:@selector(notifyCKAccountStatusChange:) name:CKAccountChangedNotification object:NULL];
80
81 __weak __typeof(self) weakSelf = self;
82
83 // If this is a live server, register with notify
84 if(!SecCKKSTestsEnabled()) {
85 int token = 0;
86 notify_register_dispatch(kSOSCCCircleChangedNotification, &token, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^(int t) {
87 [weakSelf notifyCircleChange:nil];
88 });
89 }
90
91 // 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.
92 dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
93 __strong __typeof(self) strongSelf = weakSelf;
94 if(!strongSelf) {
95 return;
96 }
97 @autoreleasepool {
98 [strongSelf notifyCKAccountStatusChange:nil];
99 [strongSelf notifyCircleChange:nil];
100 [strongSelf.finishedInitialDispatches fulfill];
101 }
102 });
103 }
104 return self;
105 }
106
107 -(void)dealloc {
108 id<CKKSNSNotificationCenter> notificationCenter = [self.nsnotificationCenterClass defaultCenter];
109 [notificationCenter removeObserver:self];
110 }
111
112 -(NSString*)descriptionInternal: (NSString*) selfString {
113 return [NSString stringWithFormat:@"<%@: %@ (%@ %@) %@>",
114 selfString,
115 [self currentStatus],
116 self.currentCKAccountInfo,
117 SOSCCGetStatusDescription(self.currentCircleStatus),
118 self.currentAccountError ?: @""];
119 }
120
121 -(NSString*)description {
122 return [self descriptionInternal: [[self class] description]];
123 }
124
125 -(NSString*)debugDescription {
126 return [self descriptionInternal: [super description]];
127 }
128
129 -(dispatch_semaphore_t)notifyOnAccountStatusChange:(id<CKKSAccountStateListener>)listener {
130 // signals when we've successfully delivered the first account status
131 dispatch_semaphore_t finishedSema = dispatch_semaphore_create(0);
132
133 dispatch_async(self.queue, ^{
134 bool alreadyRegisteredListener = false;
135 NSEnumerator *enumerator = [self.changeListeners objectEnumerator];
136 id<CKKSAccountStateListener> value;
137
138 while ((value = [enumerator nextObject])) {
139 // do pointer comparison
140 alreadyRegisteredListener |= (value == listener);
141 }
142
143 if(listener && !alreadyRegisteredListener) {
144 NSString* queueName = [NSString stringWithFormat: @"ck-account-state-%@", listener];
145
146 dispatch_queue_t objQueue = dispatch_queue_create([queueName UTF8String], DISPATCH_QUEUE_SERIAL);
147 [self.changeListeners setObject: listener forKey: objQueue];
148
149 secinfo("ckksaccount", "adding a new listener: %@", listener);
150
151 // If we know the current account status, let this listener know
152 if(self.currentComputedAccountStatus != CKKSAccountStatusUnknown) {
153 secinfo("ckksaccount", "notifying new listener %@ of current state %d", listener, (int)self.currentComputedAccountStatus);
154
155 dispatch_group_t g = dispatch_group_create();
156 if(!g) {
157 secnotice("ckksaccount", "Unable to get dispatch group.");
158 return;
159 }
160
161 [self _onqueueDeliverCurrentState:listener listenerQueue:objQueue oldStatus:CKKSAccountStatusUnknown group:g];
162
163 dispatch_group_notify(g, self.queue, ^{
164 dispatch_semaphore_signal(finishedSema);
165 });
166 } else {
167 dispatch_semaphore_signal(finishedSema);
168 }
169 } else {
170 dispatch_semaphore_signal(finishedSema);
171 }
172 });
173
174 return finishedSema;
175 }
176
177 - (dispatch_semaphore_t)notifyCKAccountStatusChange:(__unused id)object {
178 // signals when this notify is Complete, including all downcalls.
179 dispatch_semaphore_t finishedSema = dispatch_semaphore_create(0);
180
181 [self.container accountInfoWithCompletionHandler:^(CKAccountInfo* ckAccountInfo, NSError * _Nullable error) {
182 if(error) {
183 secerror("ckksaccount: error getting account info: %@", error);
184 dispatch_semaphore_signal(finishedSema);
185 return;
186 }
187
188 dispatch_sync(self.queue, ^{
189 self.firstCKAccountFetch = true;
190 [self _onqueueUpdateAccountState:ckAccountInfo circle:self.currentCircleStatus deliveredSemaphore:finishedSema];
191 });
192 }];
193
194 return finishedSema;
195 }
196
197 -(dispatch_semaphore_t)notifyCircleChange:(__unused id)object {
198 dispatch_semaphore_t finishedSema = dispatch_semaphore_create(0);
199
200 SOSCCStatus circleStatus = [CKKSCKAccountStateTracker getCircleStatus];
201 dispatch_sync(self.queue, ^{
202 self.firstSOSCircleFetch = true;
203
204 [self _onqueueUpdateAccountState:self.currentCKAccountInfo circle:circleStatus deliveredSemaphore:finishedSema];
205 });
206 return finishedSema;
207 }
208
209 // Takes the new ckAccountInfo we're moving to
210 -(void)_onqueueUpdateCKDeviceID: (CKAccountInfo*)ckAccountInfo {
211 dispatch_assert_queue(self.queue);
212 __weak __typeof(self) weakSelf = self;
213
214 // If we're in an account, opportunistically fill in the device id
215 if(ckAccountInfo.accountStatus == CKAccountStatusAvailable) {
216 [self.container fetchCurrentDeviceIDWithCompletionHandler:^(NSString* deviceID, NSError* ckerror) {
217 __strong __typeof(self) strongSelf = weakSelf;
218 if(!strongSelf) {
219 secerror("ckksaccount: Received fetchCurrentDeviceIDWithCompletionHandler callback with null AccountStateTracker");
220 return;
221 }
222
223 // Make sure you synchronize here; if we've logged out before the callback returns, don't record the result
224 dispatch_async(strongSelf.queue, ^{
225 __strong __typeof(self) innerStrongSelf = weakSelf;
226 if(innerStrongSelf.currentCKAccountInfo.accountStatus == CKAccountStatusAvailable) {
227 secnotice("ckksaccount", "CloudKit deviceID is: %@ %@", deviceID, ckerror);
228
229 innerStrongSelf.ckdeviceID = deviceID;
230 innerStrongSelf.ckdeviceIDError = ckerror;
231 [innerStrongSelf.ckdeviceIDInitialized fulfill];
232 } else {
233 // Logged out! No ckdeviceid.
234 secerror("ckksaccount: Logged back out but still received a fetchCurrentDeviceIDWithCompletionHandler callback");
235
236 innerStrongSelf.ckdeviceID = nil;
237 innerStrongSelf.ckdeviceIDError = nil;
238 // Don't touch the ckdeviceIDInitialized object; it should have been reset when the logout happened.
239 }
240 });
241 }];
242 } else {
243 // Logging out? no more device ID.
244 self.ckdeviceID = nil;
245 self.ckdeviceIDError = nil;
246 self.ckdeviceIDInitialized = [[CKKSCondition alloc] init];
247 }
248 }
249
250 // Pulled out for mocking purposes
251 +(void)fetchCirclePeerID:(void (^)(NSString* _Nullable peerID, NSError* _Nullable error))callback {
252 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
253 CFErrorRef cferror = nil;
254 SOSPeerInfoRef egoPeerInfo = SOSCCCopyMyPeerInfo(&cferror);
255 NSString* egoPeerID = egoPeerInfo ? (NSString*)CFBridgingRelease(CFRetainSafe(SOSPeerInfoGetPeerID(egoPeerInfo))) : nil;
256 CFReleaseNull(egoPeerInfo);
257
258 callback(egoPeerID, CFBridgingRelease(cferror));
259 });
260 }
261
262 // Takes the new ckAccountInfo we're moving to
263 -(void)_onqueueUpdateCirclePeerID: (SOSCCStatus)sosccstatus {
264 dispatch_assert_queue(self.queue);
265 __weak __typeof(self) weakSelf = self;
266
267 // If we're in a circle, fetch the peer id
268 if(sosccstatus == kSOSCCInCircle) {
269 [CKKSCKAccountStateTracker fetchCirclePeerID:^(NSString* peerID, NSError* error) {
270 __strong __typeof(self) strongSelf = weakSelf;
271 if(!strongSelf) {
272 secerror("ckksaccount: Received fetchCirclePeerID callback with null AccountStateTracker");
273 return;
274 }
275
276 dispatch_async(strongSelf.queue, ^{
277 __strong __typeof(self) innerstrongSelf = weakSelf;
278
279 if(innerstrongSelf.currentCircleStatus == kSOSCCInCircle) {
280 secnotice("ckksaccount", "Circle peerID is: %@ %@", peerID, error);
281 // Still in circle. Proceed.
282 innerstrongSelf.accountCirclePeerID = peerID;
283 innerstrongSelf.accountCirclePeerIDError = error;
284 [innerstrongSelf.accountCirclePeerIDInitialized fulfill];
285 } else {
286 secerror("ckksaccount: Out of circle but still received a fetchCirclePeerID callback");
287 // Not in-circle. Throw away circle id.
288 strongSelf.accountCirclePeerID = nil;
289 strongSelf.accountCirclePeerIDError = nil;
290 // Don't touch the accountCirclePeerIDInitialized object; it should have been reset when the logout happened.
291 }
292 });
293 }];
294 } else {
295 // Not in-circle, reset circle ID
296 secnotice("ckksaccount", "out of circle(%d): resetting peer ID", sosccstatus);
297 self.accountCirclePeerID = nil;
298 self.accountCirclePeerIDError = nil;
299 self.accountCirclePeerIDInitialized = [[CKKSCondition alloc] init];
300 }
301 }
302
303 - (bool)_onqueueDetermineLoggedIn:(NSError**)error {
304 // We are logged in if we are:
305 // in CKAccountStatusAvailable
306 // and supportsDeviceToDeviceEncryption == true
307 // and the iCloud account is not in grey mode
308 // and in circle
309 dispatch_assert_queue(self.queue);
310 if(self.currentCKAccountInfo) {
311 if(self.currentCKAccountInfo.accountStatus != CKAccountStatusAvailable) {
312 if(error) {
313 *error = [NSError errorWithDomain:CKKSErrorDomain
314 code:CKKSNotLoggedIn
315 description:@"iCloud account is logged out"];
316 }
317 return false;
318 } else if(!self.currentCKAccountInfo.supportsDeviceToDeviceEncryption) {
319 if(error) {
320 *error = [NSError errorWithDomain:CKKSErrorDomain
321 code:CKKSNotHSA2
322 description:@"iCloud account is not HSA2"];
323 }
324 return false;
325 } else if(!self.currentCKAccountInfo.hasValidCredentials) {
326 if(error) {
327 *error = [NSError errorWithDomain:CKKSErrorDomain
328 code:CKKSiCloudGreyMode
329 description:@"iCloud account is in grey mode"];
330 }
331 return false;
332 }
333 } else {
334 if(error) {
335 *error = [NSError errorWithDomain:CKKSErrorDomain
336 code:CKKSNotLoggedIn
337 description:@"No current iCloud account status"];
338 }
339 return false;
340 }
341
342 if(self.currentCircleStatus != kSOSCCInCircle) {
343 if(error) {
344 *error = [NSError errorWithDomain:(__bridge NSString*)kSOSErrorDomain
345 code:kSOSErrorNotInCircle
346 description:@"Not in circle"];
347 }
348 return false;
349 }
350
351 return true;
352 }
353
354 -(void)_onqueueUpdateAccountState:(CKAccountInfo*)ckAccountInfo circle:(SOSCCStatus)sosccstatus deliveredSemaphore:(dispatch_semaphore_t)finishedSema {
355 dispatch_assert_queue(self.queue);
356
357 if([self.currentCKAccountInfo isEqual: ckAccountInfo] && self.currentCircleStatus == sosccstatus) {
358 // no-op.
359 secinfo("ckksaccount", "received another notification of CK Account State %@ and Circle status %d", ckAccountInfo, (int)sosccstatus);
360 dispatch_semaphore_signal(finishedSema);
361 return;
362 }
363
364 if(![self.currentCKAccountInfo isEqual: ckAccountInfo]) {
365 secnotice("ckksaccount", "moving to CK Account info: %@", ckAccountInfo);
366 self.currentCKAccountInfo = ckAccountInfo;
367
368 [self _onqueueUpdateCKDeviceID: ckAccountInfo];
369 }
370 if(self.currentCircleStatus != sosccstatus) {
371 secnotice("ckksaccount", "moving to circle status: %@", SOSCCGetStatusDescription(sosccstatus));
372 self.currentCircleStatus = sosccstatus;
373 if (sosccstatus == kSOSCCInCircle) {
374 [[CKKSAnalytics logger] setDateProperty:[NSDate date] forKey:CKKSAnalyticsLastInCircle];
375 }
376 [self _onqueueUpdateCirclePeerID: sosccstatus];
377 }
378
379 if(!self.firstSOSCircleFetch || !self.firstCKAccountFetch) {
380 secnotice("ckksaccount", "Haven't received updates from all sources; not passing update along: %@", self);
381 dispatch_semaphore_signal(finishedSema);
382 return;
383 }
384
385 CKKSAccountStatus oldComputedStatus = self.currentComputedAccountStatus;
386
387 NSError* error = nil;
388 if([self _onqueueDetermineLoggedIn:&error]) {
389 self.currentComputedAccountStatus = CKKSAccountStatusAvailable;
390 self.currentAccountError = nil;
391 } else {
392 self.currentComputedAccountStatus = CKKSAccountStatusNoAccount;
393 self.currentAccountError = error;
394 }
395 [self.currentComputedAccountStatusValid fulfill];
396
397 if(oldComputedStatus == self.currentComputedAccountStatus) {
398 secnotice("ckksaccount", "No change in computed account status: %@ (%@ %@)",
399 [self currentStatus],
400 self.currentCKAccountInfo,
401 SOSCCGetStatusDescription(self.currentCircleStatus));
402 dispatch_semaphore_signal(finishedSema);
403 return;
404 }
405
406 secnotice("ckksaccount", "New computed account status: %@ (%@ %@)",
407 [self currentStatus],
408 self.currentCKAccountInfo,
409 SOSCCGetStatusDescription(self.currentCircleStatus));
410
411 [self _onqueueDeliverStateChanges:oldComputedStatus deliveredSemaphore:finishedSema];
412 }
413
414 -(void)_onqueueDeliverStateChanges:(CKKSAccountStatus)oldStatus deliveredSemaphore:(dispatch_semaphore_t)finishedSema {
415 dispatch_assert_queue(self.queue);
416
417 dispatch_group_t g = dispatch_group_create();
418 if(!g) {
419 secnotice("ckksaccount", "Unable to get dispatch group.");
420 return;
421 }
422
423 NSEnumerator *enumerator = [self.changeListeners keyEnumerator];
424 dispatch_queue_t dq;
425
426 // Queue up the changes for each listener.
427 while ((dq = [enumerator nextObject])) {
428 id<CKKSAccountStateListener> listener = [self.changeListeners objectForKey: dq];
429 [self _onqueueDeliverCurrentState:listener listenerQueue:dq oldStatus:oldStatus group:g];
430 }
431
432 dispatch_group_notify(g, self.queue, ^{
433 dispatch_semaphore_signal(finishedSema);
434 });
435 }
436
437 -(void)_onqueueDeliverCurrentState:(id<CKKSAccountStateListener>)listener listenerQueue:(dispatch_queue_t)listenerQueue oldStatus:(CKKSAccountStatus)oldStatus group:(dispatch_group_t)g {
438 dispatch_assert_queue(self.queue);
439
440 __weak __typeof(listener) weakListener = listener;
441
442 if(listener) {
443 dispatch_group_async(g, listenerQueue, ^{
444 [weakListener ckAccountStatusChange:oldStatus to:self.currentComputedAccountStatus];
445 });
446 }
447 }
448
449 -(void)notifyCKAccountStatusChangeAndWaitForSignal {
450 dispatch_semaphore_wait([self notifyCKAccountStatusChange: nil], DISPATCH_TIME_FOREVER);
451 }
452
453 -(void)notifyCircleStatusChangeAndWaitForSignal {
454 dispatch_semaphore_wait([self notifyCircleChange: nil], DISPATCH_TIME_FOREVER);
455 }
456
457 -(dispatch_group_t)checkForAllDeliveries {
458
459 dispatch_group_t g = dispatch_group_create();
460 if(!g) {
461 secnotice("ckksaccount", "Unable to get dispatch group.");
462 return nil;
463 }
464
465 dispatch_sync(self.queue, ^{
466 NSEnumerator *enumerator = [self.changeListeners keyEnumerator];
467 dispatch_queue_t dq;
468
469 // Queue up the changes for each listener.
470 while ((dq = [enumerator nextObject])) {
471 id<CKKSAccountStateListener> listener = [self.changeListeners objectForKey: dq];
472
473 secinfo("ckksaccountblock", "Starting blocking for listener %@", listener);
474 __weak __typeof(listener) weakListener = listener;
475 dispatch_group_async(g, dq, ^{
476 __strong __typeof(listener) strongListener = weakListener;
477 // Do nothing in particular. It's just important that this block runs.
478 secinfo("ckksaccountblock", "Done blocking for listener %@", strongListener);
479 });
480 }
481 });
482
483 return g;
484 }
485
486 // This is its own function to allow OCMock to swoop in and replace the result during testing.
487 +(SOSCCStatus)getCircleStatus {
488 CFErrorRef cferror = NULL;
489
490 SOSCCStatus status = SOSCCThisDeviceIsInCircle(&cferror);
491 if(cferror) {
492 secerror("ckksaccount: error getting circle status: %@", cferror);
493 CFReleaseNull(cferror);
494 return kSOSCCError;
495 }
496 return status;
497 }
498
499 -(NSString*)currentStatus {
500 return [CKKSCKAccountStateTracker stringFromAccountStatus:self.currentComputedAccountStatus];
501 }
502
503 +(NSString*)stringFromAccountStatus: (CKKSAccountStatus) status {
504 switch(status) {
505 case CKKSAccountStatusUnknown: return @"account state unknown";
506 case CKKSAccountStatusAvailable: return @"logged in";
507 case CKKSAccountStatusNoAccount: return @"no account";
508 }
509 }
510
511 @end
512
513 #endif // OCTAGON