]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSAccountStateTracker.m
Security-59306.41.2.tar.gz
[apple/security.git] / keychain / ckks / CKKSAccountStateTracker.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 "keychain/SecureObjectSync/SOSInternal.h"
31 #include <notify.h>
32
33 #import "keychain/ot/OTManager.h"
34 #import "keychain/ot/OTConstants.h"
35 #import "keychain/ckks/CKKS.h"
36 #import "keychain/ckks/CloudKitCategories.h"
37 #import "keychain/ckks/CKKSAccountStateTracker.h"
38 #import "keychain/ckks/CKKSAnalytics.h"
39 #import "keychain/categories/NSError+UsefulConstructors.h"
40 #import "keychain/ot/ObjCImprovements.h"
41
42
43 NSString* CKKSAccountStatusToString(CKKSAccountStatus status)
44 {
45 switch(status) {
46 case CKKSAccountStatusAvailable:
47 return @"available";
48 case CKKSAccountStatusNoAccount:
49 return @"no account";
50 case CKKSAccountStatusUnknown:
51 return @"unknown";
52 }
53 }
54
55 @interface CKKSAccountStateTracker ()
56 @property (readonly) Class<CKKSNSNotificationCenter> nsnotificationCenterClass;
57
58 @property dispatch_queue_t queue;
59
60 @property NSMapTable<dispatch_queue_t, id<CKKSCloudKitAccountStateListener>>* ckChangeListeners;
61
62 @property CKContainer* container; // used only for fetching the CKAccountStatus
63 @property bool firstCKAccountFetch;
64
65 // make writable
66 @property (nullable) OTCliqueStatusWrapper* octagonStatus;
67 @property (nullable) NSString* octagonPeerID;
68 @property CKKSCondition* octagonInformationInitialized;
69
70 @property CKKSAccountStatus hsa2iCloudAccountStatus;
71 @property CKKSCondition* hsa2iCloudAccountInitialized;
72 @end
73
74 @implementation CKKSAccountStateTracker
75 @synthesize octagonPeerID = _octagonPeerID;
76
77 -(instancetype)init: (CKContainer*) container nsnotificationCenterClass: (Class<CKKSNSNotificationCenter>) nsnotificationCenterClass {
78 if((self = [super init])) {
79 _nsnotificationCenterClass = nsnotificationCenterClass;
80 // These map tables are backwards from how we'd like, but it's the best way to have weak pointers to CKKSCombinedAccountStateListener.
81 _ckChangeListeners = [NSMapTable strongToWeakObjectsMapTable];
82
83 _currentCKAccountInfo = nil;
84 _ckAccountInfoInitialized = [[CKKSCondition alloc] init];
85
86 _currentCircleStatus = [[SOSAccountStatus alloc] init:kSOSCCError error:nil];
87
88 _container = container;
89
90 _queue = dispatch_queue_create("ck-account-state", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
91
92 _firstCKAccountFetch = false;
93
94 _finishedInitialDispatches = [[CKKSCondition alloc] init];
95 _ckdeviceIDInitialized = [[CKKSCondition alloc] init];
96
97 _octagonInformationInitialized = [[CKKSCondition alloc] init];
98
99 _hsa2iCloudAccountStatus = CKKSAccountStatusUnknown;
100 _hsa2iCloudAccountInitialized = [[CKKSCondition alloc] init];
101
102 id<CKKSNSNotificationCenter> notificationCenter = [self.nsnotificationCenterClass defaultCenter];
103 secinfo("ckksaccount", "Registering with notification center %@", notificationCenter);
104 [notificationCenter addObserver:self selector:@selector(notifyCKAccountStatusChange:) name:CKAccountChangedNotification object:NULL];
105
106 WEAKIFY(self);
107
108 // If this is a live server, register with notify
109 if(!SecCKKSTestsEnabled()) {
110 int token = 0;
111 notify_register_dispatch(kSOSCCCircleChangedNotification, &token, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^(int t) {
112 STRONGIFY(self);
113 [self notifyCircleChange:nil];
114 });
115
116 // 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.
117 // Note: if you're in the tests, you must call performInitialDispatches yourself!
118 dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
119 STRONGIFY(self);
120 if(!self) {
121 return;
122 }
123 @autoreleasepool {
124 [self performInitialDispatches];
125 }
126 });
127 }
128 }
129 return self;
130 }
131
132 - (void)performInitialDispatches
133 {
134 @autoreleasepool {
135 [self notifyCKAccountStatusChange:nil];
136 [self notifyCircleChange:nil];
137 [self.finishedInitialDispatches fulfill];
138 }
139 }
140
141 -(void)dealloc {
142 id<CKKSNSNotificationCenter> notificationCenter = [self.nsnotificationCenterClass defaultCenter];
143 [notificationCenter removeObserver:self];
144 }
145
146 -(NSString*)descriptionInternal: (NSString*) selfString {
147 return [NSString stringWithFormat:@"<%@: %@, hsa2: %@>",
148 selfString,
149 self.currentCKAccountInfo,
150 CKKSAccountStatusToString(self.hsa2iCloudAccountStatus)];
151 }
152
153 -(NSString*)description {
154 return [self descriptionInternal: [[self class] description]];
155 }
156
157 -(NSString*)debugDescription {
158 return [self descriptionInternal: [super description]];
159 }
160
161 - (dispatch_semaphore_t)registerForNotificationsOfCloudKitAccountStatusChange:(id<CKKSCloudKitAccountStateListener>)listener {
162 // signals when we've successfully delivered the first account status
163 dispatch_semaphore_t finishedSema = dispatch_semaphore_create(0);
164
165 dispatch_async(self.queue, ^{
166 bool alreadyRegisteredListener = false;
167 NSEnumerator *enumerator = [self.ckChangeListeners objectEnumerator];
168 id<CKKSCloudKitAccountStateListener> value;
169
170 while ((value = [enumerator nextObject])) {
171 // do pointer comparison
172 alreadyRegisteredListener |= (value == listener);
173 }
174
175 if(listener && !alreadyRegisteredListener) {
176 NSString* queueName = [NSString stringWithFormat: @"ck-account-state-%@", listener];
177
178 dispatch_queue_t objQueue = dispatch_queue_create([queueName UTF8String], DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
179 [self.ckChangeListeners setObject:listener forKey: objQueue];
180
181 secinfo("ckksaccount-ck", "adding a new listener: %@", listener);
182
183 // If we know the current account status, let this listener know
184 if(self.firstCKAccountFetch) {
185 secinfo("ckksaccount-ck", "notifying new listener %@ of current state %@", listener, self.currentCKAccountInfo);
186
187 dispatch_group_t g = dispatch_group_create();
188 if(!g) {
189 secnotice("ckksaccount-ck", "Unable to get dispatch group.");
190 dispatch_semaphore_signal(finishedSema);
191 return;
192 }
193
194 [self _onqueueDeliverCurrentCloudKitState:listener listenerQueue:objQueue oldStatus:nil group:g];
195
196 dispatch_group_notify(g, self.queue, ^{
197 dispatch_semaphore_signal(finishedSema);
198 });
199 } else {
200 dispatch_semaphore_signal(finishedSema);
201 }
202 } else {
203 dispatch_semaphore_signal(finishedSema);
204 }
205 });
206
207 return finishedSema;
208 }
209
210 - (dispatch_semaphore_t)notifyCKAccountStatusChange:(__unused id)object {
211 // signals when this notify is Complete, including all downcalls.
212 dispatch_semaphore_t finishedSema = dispatch_semaphore_create(0);
213
214 WEAKIFY(self);
215
216 [self.container accountInfoWithCompletionHandler:^(CKAccountInfo* ckAccountInfo, NSError * _Nullable error) {
217 STRONGIFY(self);
218
219 if(error) {
220 secerror("ckksaccount: error getting account info: %@", error);
221 dispatch_semaphore_signal(finishedSema);
222 return;
223 }
224
225 dispatch_sync(self.queue, ^{
226 self.firstCKAccountFetch = true;
227 secnotice("ckksaccount", "received CK Account info: %@", ckAccountInfo);
228 [self _onqueueUpdateAccountState:ckAccountInfo deliveredSemaphore:finishedSema];
229 });
230 }];
231
232 return finishedSema;
233 }
234
235 // Takes the new ckAccountInfo we're moving to
236 -(void)_onqueueUpdateCKDeviceID:(CKAccountInfo*)ckAccountInfo {
237 dispatch_assert_queue(self.queue);
238 WEAKIFY(self);
239
240 // If we're in an account, opportunistically fill in the device id
241 if(ckAccountInfo.accountStatus == CKAccountStatusAvailable) {
242 [self.container fetchCurrentDeviceIDWithCompletionHandler:^(NSString* deviceID, NSError* ckerror) {
243 STRONGIFY(self);
244 if(!self) {
245 secerror("ckksaccount: Received fetchCurrentDeviceIDWithCompletionHandler callback with null AccountStateTracker");
246 return;
247 }
248
249 // Make sure you synchronize here; if we've logged out before the callback returns, don't record the result
250 dispatch_async(self.queue, ^{
251 STRONGIFY(self);
252 if(self.currentCKAccountInfo.accountStatus == CKAccountStatusAvailable) {
253 secnotice("ckksaccount", "CloudKit deviceID is: %@ %@", deviceID, ckerror);
254
255 self.ckdeviceID = deviceID;
256 self.ckdeviceIDError = ckerror;
257 [self.ckdeviceIDInitialized fulfill];
258 } else {
259 // Logged out! No ckdeviceid.
260 secerror("ckksaccount: Logged back out but still received a fetchCurrentDeviceIDWithCompletionHandler callback");
261
262 self.ckdeviceID = nil;
263 self.ckdeviceIDError = nil;
264 // Don't touch the ckdeviceIDInitialized object; it should have been reset when the logout happened.
265 }
266 });
267 }];
268 } else {
269 // Logging out? no more device ID.
270 self.ckdeviceID = nil;
271 self.ckdeviceIDError = nil;
272 self.ckdeviceIDInitialized = [[CKKSCondition alloc] init];
273 }
274 }
275
276 - (dispatch_semaphore_t)notifyCircleChange:(__unused id)object {
277 dispatch_semaphore_t finishedSema = dispatch_semaphore_create(0);
278
279 SOSAccountStatus* sosstatus = [CKKSAccountStateTracker getCircleStatus];
280 dispatch_sync(self.queue, ^{
281 if(self.currentCircleStatus == nil || ![self.currentCircleStatus isEqual:sosstatus]) {
282 secnotice("ckksaccount", "moving to circle status: %@", sosstatus);
283 self.currentCircleStatus = sosstatus;
284
285 if (sosstatus.status == kSOSCCInCircle) {
286 [[CKKSAnalytics logger] setDateProperty:[NSDate date] forKey:CKKSAnalyticsLastInCircle];
287 }
288 [self _onqueueUpdateCirclePeerID:sosstatus];
289 }
290 dispatch_semaphore_signal(finishedSema);
291 });
292
293 return finishedSema;
294 }
295
296 // Pulled out for mocking purposes
297 + (void)fetchCirclePeerID:(void (^)(NSString* _Nullable peerID, NSError* _Nullable error))callback {
298 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
299 CFErrorRef cferror = nil;
300 SOSPeerInfoRef egoPeerInfo = SOSCCCopyMyPeerInfo(&cferror);
301 NSString* egoPeerID = egoPeerInfo ? (NSString*)CFBridgingRelease(CFRetainSafe(SOSPeerInfoGetPeerID(egoPeerInfo))) : nil;
302 CFReleaseNull(egoPeerInfo);
303
304 callback(egoPeerID, CFBridgingRelease(cferror));
305 });
306 }
307
308 // Takes the new ckAccountInfo we're moving to
309 - (void)_onqueueUpdateCirclePeerID:(SOSAccountStatus*)sosstatus {
310 dispatch_assert_queue(self.queue);
311 WEAKIFY(self);
312
313 // If we're in a circle, fetch the peer id
314 if(sosstatus.status == kSOSCCInCircle) {
315 [CKKSAccountStateTracker fetchCirclePeerID:^(NSString* peerID, NSError* error) {
316 STRONGIFY(self);
317 if(!self) {
318 secerror("ckksaccount: Received fetchCirclePeerID callback with null AccountStateTracker");
319 return;
320 }
321
322 dispatch_async(self.queue, ^{
323 STRONGIFY(self);
324
325 if(self.currentCircleStatus && self.currentCircleStatus.status == kSOSCCInCircle) {
326 secnotice("ckksaccount", "Circle peerID is: %@ %@", peerID, error);
327 // Still in circle. Proceed.
328 self.accountCirclePeerID = peerID;
329 self.accountCirclePeerIDError = error;
330 [self.accountCirclePeerIDInitialized fulfill];
331 } else {
332 secerror("ckksaccount: Out of circle but still received a fetchCirclePeerID callback");
333 // Not in-circle. Throw away circle id.
334 self.accountCirclePeerID = nil;
335 self.accountCirclePeerIDError = nil;
336 // Don't touch the accountCirclePeerIDInitialized object; it should have been reset when the logout happened.
337 }
338 });
339 }];
340 } else {
341 // Not in-circle, reset circle ID
342 secnotice("ckksaccount", "out of circle(%@): resetting peer ID", sosstatus);
343 self.accountCirclePeerID = nil;
344 self.accountCirclePeerIDError = nil;
345 self.accountCirclePeerIDInitialized = [[CKKSCondition alloc] init];
346 }
347 }
348
349 - (void)_onqueueUpdateAccountState:(CKAccountInfo*)ckAccountInfo
350 deliveredSemaphore:(dispatch_semaphore_t)finishedSema
351 {
352 // Launder the finishedSema into a dispatch_group.
353 // _onqueueUpdateAccountState:circle:dispatchGroup: will then add any blocks it thinks is necessary,
354 // then the group will fire the semaphore.
355 dispatch_assert_queue(self.queue);
356
357 dispatch_group_t g = dispatch_group_create();
358 if(!g) {
359 secnotice("ckksaccount", "Unable to get dispatch group.");
360 dispatch_semaphore_signal(finishedSema);
361 return;
362 }
363
364 [self _onqueueUpdateAccountState:ckAccountInfo
365 dispatchGroup:g];
366
367 dispatch_group_notify(g, self.queue, ^{
368 dispatch_semaphore_signal(finishedSema);
369 });
370 }
371
372 - (void)_onqueueUpdateAccountState:(CKAccountInfo*)ckAccountInfo
373 dispatchGroup:(dispatch_group_t)g
374 {
375 dispatch_assert_queue(self.queue);
376
377 if([self.currentCKAccountInfo isEqual: ckAccountInfo]) {
378 // no-op.
379 secinfo("ckksaccount", "received another notification of CK Account State %@", ckAccountInfo);
380 return;
381 }
382
383 if((self.currentCKAccountInfo == nil && ckAccountInfo != nil) ||
384 !(self.currentCKAccountInfo == ckAccountInfo || [self.currentCKAccountInfo isEqual: ckAccountInfo])) {
385 secnotice("ckksaccount", "moving to CK Account info: %@", ckAccountInfo);
386 CKAccountInfo* oldAccountInfo = self.currentCKAccountInfo;
387 self.currentCKAccountInfo = ckAccountInfo;
388 [self.ckAccountInfoInitialized fulfill];
389
390 [self _onqueueUpdateCKDeviceID: ckAccountInfo];
391
392 [self _onqueueDeliverCloudKitStateChanges:oldAccountInfo dispatchGroup:g];
393 }
394 }
395
396 - (void)_onqueueDeliverCloudKitStateChanges:(CKAccountInfo*)oldStatus
397 dispatchGroup:(dispatch_group_t)g
398 {
399 dispatch_assert_queue(self.queue);
400
401 NSEnumerator *enumerator = [self.ckChangeListeners keyEnumerator];
402 dispatch_queue_t dq;
403
404 // Queue up the changes for each listener.
405 while ((dq = [enumerator nextObject])) {
406 id<CKKSCloudKitAccountStateListener> listener = [self.ckChangeListeners objectForKey: dq];
407 [self _onqueueDeliverCurrentCloudKitState:listener listenerQueue:dq oldStatus:oldStatus group:g];
408 }
409 }
410
411 - (void)_onqueueDeliverCurrentCloudKitState:(id<CKKSCloudKitAccountStateListener>)listener
412 listenerQueue:(dispatch_queue_t)listenerQueue
413 oldStatus:(CKAccountInfo* _Nullable)oldStatus
414 group:(dispatch_group_t)g
415 {
416 dispatch_assert_queue(self.queue);
417
418 __weak __typeof(listener) weakListener = listener;
419
420 if(listener) {
421 dispatch_group_async(g, listenerQueue, ^{
422 [weakListener cloudkitAccountStateChange:oldStatus to:self.currentCKAccountInfo];
423 });
424 }
425 }
426
427 -(void)notifyCKAccountStatusChangeAndWaitForSignal {
428 dispatch_semaphore_wait([self notifyCKAccountStatusChange: nil], DISPATCH_TIME_FOREVER);
429 }
430
431 -(void)notifyCircleStatusChangeAndWaitForSignal {
432 dispatch_semaphore_wait([self notifyCircleChange: nil], DISPATCH_TIME_FOREVER);
433 }
434
435 -(dispatch_group_t)checkForAllDeliveries {
436
437 dispatch_group_t g = dispatch_group_create();
438 if(!g) {
439 secnotice("ckksaccount", "Unable to get dispatch group.");
440 return nil;
441 }
442
443 dispatch_sync(self.queue, ^{
444 NSEnumerator *enumerator = [self.ckChangeListeners keyEnumerator];
445 dispatch_queue_t dq;
446
447 // Queue up the changes for each listener.
448 while ((dq = [enumerator nextObject])) {
449 id<CKKSCloudKitAccountStateListener> listener = [self.ckChangeListeners objectForKey: dq];
450
451 secinfo("ckksaccountblock", "Starting blocking for listener %@", listener);
452 WEAKIFY(listener);
453 dispatch_group_async(g, dq, ^{
454 STRONGIFY(listener);
455 // Do nothing in particular. It's just important that this block runs.
456 secinfo("ckksaccountblock", "Done blocking for listener %@", listener);
457 });
458 }
459 });
460
461 return g;
462 }
463
464 // This is its own function to allow OCMock to swoop in and replace the result during testing.
465 + (SOSAccountStatus*)getCircleStatus {
466 CFErrorRef cferror = NULL;
467
468 SOSCCStatus status = SOSCCThisDeviceIsInCircle(&cferror);
469 if(cferror) {
470 secerror("ckksaccount: error getting circle status: %@", cferror);
471 return [[SOSAccountStatus alloc] init:kSOSCCError error:CFBridgingRelease(cferror)];
472 }
473
474 return [[SOSAccountStatus alloc] init:status error:nil];
475 }
476
477 + (NSString*)stringFromAccountStatus: (CKKSAccountStatus) status {
478 switch(status) {
479 case CKKSAccountStatusUnknown: return @"account state unknown";
480 case CKKSAccountStatusAvailable: return @"logged in";
481 case CKKSAccountStatusNoAccount: return @"no account";
482 }
483 }
484
485 - (void)triggerOctagonStatusFetch
486 {
487 WEAKIFY(self);
488
489 __block CKKSCondition* blockPointer = nil;
490 dispatch_sync(self.queue, ^{
491 self.octagonInformationInitialized = [[CKKSCondition alloc] initToChain:self.octagonInformationInitialized];
492 blockPointer = self.octagonInformationInitialized;
493 });
494
495 // Explicitly do not use the OTClique API, as that might include SOS status as well
496 OTOperationConfiguration* config = [[OTOperationConfiguration alloc] init];
497 config.timeoutWaitForCKAccount = 100*NSEC_PER_MSEC;
498 [[OTManager manager] fetchTrustStatus:nil
499 context:OTDefaultContext
500 configuration:config
501 reply:^(CliqueStatus status, NSString * _Nullable peerID, NSNumber * _Nullable numberOfPeersInOctagon, BOOL isExcluded, NSError * _Nullable error) {
502 STRONGIFY(self);
503
504 dispatch_sync(self.queue, ^{
505 if(error) {
506 secerror("ckksaccount: error getting octagon status: %@", error);
507 self.octagonStatus = [[OTCliqueStatusWrapper alloc] initWithStatus:CliqueStatusError];
508 } else {
509 secnotice("ckksaccount", "Caching octagon status as (%@, %@)", OTCliqueStatusToString(status), peerID);
510 self.octagonStatus = [[OTCliqueStatusWrapper alloc] initWithStatus:status];
511 }
512
513 self.octagonPeerID = peerID;
514 [blockPointer fulfill];
515 });
516 }];
517 }
518
519
520 - (void)setHSA2iCloudAccountStatus:(CKKSAccountStatus)status
521 {
522 self.hsa2iCloudAccountStatus = status;
523 if(status == CKKSAccountStatusUnknown) {
524 self.hsa2iCloudAccountInitialized = [[CKKSCondition alloc] initToChain:self.hsa2iCloudAccountInitialized];
525 } else {
526 [self.hsa2iCloudAccountInitialized fulfill];
527 }
528 }
529
530 @end
531
532 @implementation SOSAccountStatus
533 - (instancetype)init:(SOSCCStatus)status error:(NSError*)error
534 {
535 if((self = [super init])) {
536 _status = status;
537 _error = error;
538 }
539 return self;
540 }
541
542 - (BOOL)isEqual:(id)object
543 {
544 if(![object isKindOfClass:[SOSAccountStatus class]]) {
545 return NO;
546 }
547
548 if(object == nil) {
549 return NO;
550 }
551
552 SOSAccountStatus* obj = (SOSAccountStatus*) object;
553 return self.status == obj.status &&
554 ((self.error == nil && obj.error == nil) || [self.error isEqual:obj.error]);
555 }
556
557 - (NSString*)description
558 {
559 return [NSString stringWithFormat:@"<SOSStatus: %@ (%@)>", SOSCCGetStatusDescription(self.status), self.error];
560 }
561 @end
562
563
564 @implementation OTCliqueStatusWrapper
565 - (instancetype)initWithStatus:(CliqueStatus)status
566 {
567 if((self = [super init])) {
568 _status = status;
569 }
570 return self;
571 }
572
573 - (BOOL)isEqual:(id)object
574 {
575 if(![object isKindOfClass:[OTCliqueStatusWrapper class]]) {
576 return NO;
577 }
578
579 OTCliqueStatusWrapper* obj = (OTCliqueStatusWrapper*)object;
580 return obj.status == self.status;
581 }
582 - (NSString*)description
583 {
584 return [NSString stringWithFormat:@"<CliqueStatus: %@>", OTCliqueStatusToString(self.status)];
585 }
586 @end
587
588
589 #endif // OCTAGON