2 * Copyright (c) 2016 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@
24 #import "keychain/ckks/CKKSViewManager.h"
25 #import "keychain/ckks/CKKSKeychainView.h"
26 #import "keychain/ckks/CKKSSynchronizeOperation.h"
27 #import "keychain/ckks/CKKSKey.h"
28 #import "keychain/ckks/CKKSZoneStateEntry.h"
29 #import "keychain/ckks/CKKSNearFutureScheduler.h"
30 #import "keychain/ckks/CKKSNotifier.h"
31 #import "keychain/ckks/CKKSCondition.h"
32 #import "keychain/ckks/CloudKitCategories.h"
34 #import "keychain/ot/OTDefines.h"
36 #import "SecEntitlements.h"
38 #include <securityd/SecDbItem.h>
39 #include <securityd/SecDbKeychainItem.h>
40 #include <securityd/SecItemSchema.h>
41 #include <Security/SecureObjectSync/SOSViews.h>
43 #import <Foundation/NSXPCConnection.h>
44 #import <Foundation/NSXPCConnection_Private.h>
46 #include <Security/SecureObjectSync/SOSAccount.h>
47 #include <Security/SecItemBackup.h>
50 #import <CloudKit/CloudKit.h>
51 #import <CloudKit/CloudKit_Private.h>
53 #import <SecurityFoundation/SFKey.h>
54 #import <SecurityFoundation/SFKey_Private.h>
56 #import "CKKSAnalytics.h"
59 @interface CKKSViewManager () <NSXPCListenerDelegate>
61 @property NSXPCListener *listener;
63 // Once you set these, all CKKSKeychainViews created will use them
64 @property (readonly) Class<CKKSFetchRecordZoneChangesOperation> fetchRecordZoneChangesOperationClass;
65 @property (readonly) Class<CKKSFetchRecordsOperation> fetchRecordsOperationClass;
66 @property (readonly) Class<CKKSQueryOperation> queryOperationClass;
67 @property (readonly) Class<CKKSModifySubscriptionsOperation> modifySubscriptionsOperationClass;
68 @property (readonly) Class<CKKSModifyRecordZonesOperation> modifyRecordZonesOperationClass;
69 @property (readonly) Class<CKKSAPSConnection> apsConnectionClass;
70 @property (readonly) Class<CKKSNotifier> notifierClass;
71 @property (readonly) Class<CKKSNSNotificationCenter> nsnotificationCenterClass;
73 @property NSMutableDictionary<NSString*, CKKSKeychainView*>* views;
75 @property NSMutableDictionary<NSString*, SecBoolNSErrorCallback>* pendingSyncCallbacks;
76 @property CKKSNearFutureScheduler* savedTLKNotifier;;
77 @property NSOperationQueue* operationQueue;
79 @property NSMapTable<dispatch_queue_t, id<CKKSPeerUpdateListener>>* peerChangeListeners;
84 @interface CKKSViewManager (lockstateTracker) <CKKSLockStateNotification>
88 @implementation CKKSViewManager
91 - (instancetype)initCloudKitWithContainerName: (NSString*) containerName usePCS:(bool)usePCS {
92 return [self initWithContainerName:containerName
94 fetchRecordZoneChangesOperationClass:[CKFetchRecordZoneChangesOperation class]
95 fetchRecordsOperationClass:[CKFetchRecordsOperation class]
96 queryOperationClass:[CKQueryOperation class]
97 modifySubscriptionsOperationClass:[CKModifySubscriptionsOperation class]
98 modifyRecordZonesOperationClass:[CKModifyRecordZonesOperation class]
99 apsConnectionClass:[APSConnection class]
100 nsnotificationCenterClass:[NSNotificationCenter class]
101 notifierClass:[CKKSNotifyPostNotifier class]];
104 - (instancetype)initWithContainerName: (NSString*) containerName
106 fetchRecordZoneChangesOperationClass: (Class<CKKSFetchRecordZoneChangesOperation>) fetchRecordZoneChangesOperationClass
107 fetchRecordsOperationClass: (Class<CKKSFetchRecordsOperation>)fetchRecordsOperationClass
108 queryOperationClass: (Class<CKKSQueryOperation>)queryOperationClass
109 modifySubscriptionsOperationClass: (Class<CKKSModifySubscriptionsOperation>) modifySubscriptionsOperationClass
110 modifyRecordZonesOperationClass: (Class<CKKSModifyRecordZonesOperation>) modifyRecordZonesOperationClass
111 apsConnectionClass: (Class<CKKSAPSConnection>) apsConnectionClass
112 nsnotificationCenterClass: (Class<CKKSNSNotificationCenter>) nsnotificationCenterClass
113 notifierClass: (Class<CKKSNotifier>) notifierClass
115 if(self = [super init]) {
116 _fetchRecordZoneChangesOperationClass = fetchRecordZoneChangesOperationClass;
117 _fetchRecordsOperationClass = fetchRecordsOperationClass;
118 _queryOperationClass = queryOperationClass;
119 _modifySubscriptionsOperationClass = modifySubscriptionsOperationClass;
120 _modifyRecordZonesOperationClass = modifyRecordZonesOperationClass;
121 _apsConnectionClass = apsConnectionClass;
122 _nsnotificationCenterClass = nsnotificationCenterClass;
123 _notifierClass = notifierClass;
125 _container = [self makeCKContainer: containerName usePCS:usePCS];
126 _accountTracker = [[CKKSCKAccountStateTracker alloc] init:self.container nsnotificationCenterClass:nsnotificationCenterClass];
127 _lockStateTracker = [[CKKSLockStateTracker alloc] init];
128 [_lockStateTracker addLockStateObserver:self];
129 _reachabilityTracker = [[CKKSReachabilityTracker alloc] init];
131 _operationQueue = [[NSOperationQueue alloc] init];
133 // Backwards from how we'd like, but it's the best way to have weak pointers to CKKSPeerUpdateListener.
134 _peerChangeListeners = [NSMapTable strongToWeakObjectsMapTable];
136 _views = [[NSMutableDictionary alloc] init];
137 _pendingSyncCallbacks = [[NSMutableDictionary alloc] init];
139 _initializeNewZones = false;
141 _completedSecCKKSInitialize = [[CKKSCondition alloc] init];
143 __weak __typeof(self) weakSelf = self;
144 _savedTLKNotifier = [[CKKSNearFutureScheduler alloc] initWithName:@"newtlks"
146 keepProcessAlive:true
147 dependencyDescriptionCode:CKKSResultDescriptionNone
149 [weakSelf notifyNewTLKsInKeychain];
152 _listener = [NSXPCListener anonymousListener];
153 _listener.delegate = self;
156 // If this is a live server, register with notify
157 if(!SecCKKSTestsEnabled()) {
159 notify_register_dispatch(kSOSCCCircleOctagonKeysChangedNotification, &token, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^(int t) {
160 // Since SOS doesn't change the self peer, we can reliably just send "trusted peers changed"; it'll be mostly right
161 secnotice("ckksshare", "Received a notification that the SOS Octagon peer set changed");
162 [weakSelf sendTrustedPeerSetChangedUpdate];
169 -(CKContainer*)makeCKContainer:(NSString*)containerName usePCS:(bool)usePCS {
170 CKContainer* container = [CKContainer containerWithIdentifier:containerName];
172 CKContainerOptions* containerOptions = [[CKContainerOptions alloc] init];
173 containerOptions.bypassPCSEncryption = YES;
175 // We don't have a great way to set these, so replace the entire container object
176 container = [[CKContainer alloc] initWithContainerID: container.containerID options:containerOptions];
181 - (void)setupAnalytics
183 __weak __typeof(self) weakSelf = self;
185 // Tests shouldn't continue here; it leads to entitlement crashes with CloudKit if the mocks aren't enabled when this function runs
186 if(SecCKKSTestsEnabled()) {
190 [[CKKSAnalytics logger] AddMultiSamplerForName:@"CKKS-healthSummary" withTimeInterval:SFAnalyticsSamplerIntervalOncePerReport block:^NSDictionary<NSString *,NSNumber *> *{
191 __strong __typeof(self) strongSelf = weakSelf;
196 NSMutableDictionary* values = [NSMutableDictionary dictionary];
197 BOOL inCircle = (strongSelf.accountTracker.currentCircleStatus == kSOSCCInCircle);
199 [[CKKSAnalytics logger] setDateProperty:[NSDate date] forKey:CKKSAnalyticsLastInCircle];
201 values[CKKSAnalyticsInCircle] = @(inCircle);
203 BOOL validCredentials = strongSelf.accountTracker.currentCKAccountInfo.hasValidCredentials;
204 if (!validCredentials) {
205 values[CKKSAnalyticsValidCredentials] = @(validCredentials);
208 NSArray<NSString *>* keys = @[ CKKSAnalyticsLastUnlock, CKKSAnalyticsLastInCircle];
209 for (NSString * key in keys) {
210 NSDate *date = [[CKKSAnalytics logger] datePropertyForKey:key];
211 values[key] = @([CKKSAnalytics fuzzyDaysSinceDate:date]);
216 for (NSString* viewName in [self viewList]) {
217 [[CKKSAnalytics logger] AddMultiSamplerForName:[NSString stringWithFormat:@"CKKS-%@-healthSummary", viewName] withTimeInterval:SFAnalyticsSamplerIntervalOncePerReport block:^NSDictionary<NSString *,NSNumber *> *{
218 __strong __typeof(self) strongSelf = weakSelf;
222 BOOL inCircle = strongSelf.accountTracker && strongSelf.accountTracker.currentCircleStatus == kSOSCCInCircle;
223 NSMutableDictionary* values = [NSMutableDictionary dictionary];
224 CKKSKeychainView* view = [strongSelf findOrCreateView:viewName];
225 NSDate* dateOfLastSyncClassA = [[CKKSAnalytics logger] dateOfLastSuccessForEvent:CKKSEventProcessIncomingQueueClassA inView:view];
226 NSDate* dateOfLastSyncClassC = [[CKKSAnalytics logger] dateOfLastSuccessForEvent:CKKSEventProcessIncomingQueueClassC inView:view];
227 NSDate* dateOfLastKSR = [[CKKSAnalytics logger] datePropertyForKey:CKKSAnalyticsLastKeystateReady inView:view];
229 NSInteger fuzzyDaysSinceClassASync = [CKKSAnalytics fuzzyDaysSinceDate:dateOfLastSyncClassA];
230 NSInteger fuzzyDaysSinceClassCSync = [CKKSAnalytics fuzzyDaysSinceDate:dateOfLastSyncClassC];
231 NSInteger fuzzyDaysSinceKSR = [CKKSAnalytics fuzzyDaysSinceDate:dateOfLastKSR];
232 [values setValue:@(fuzzyDaysSinceClassASync) forKey:[NSString stringWithFormat:@"%@-daysSinceClassASync", viewName]];
233 [values setValue:@(fuzzyDaysSinceClassCSync) forKey:[NSString stringWithFormat:@"%@-daysSinceClassCSync", viewName]];
234 [values setValue:@(fuzzyDaysSinceKSR) forKey:[NSString stringWithFormat:@"%@-daysSinceLastKeystateReady", viewName]];
236 BOOL hasTLKs = [view.keyHierarchyState isEqualToString:SecCKKSZoneKeyStateReady] || [view.keyHierarchyState isEqualToString:SecCKKSZoneKeyStateReadyPendingUnlock];
237 BOOL syncedClassARecently = fuzzyDaysSinceClassASync < 7;
238 BOOL syncedClassCRecently = fuzzyDaysSinceClassCSync < 7;
239 BOOL incomingQueueIsErrorFree = view.lastIncomingQueueOperation.error == nil;
240 BOOL outgoingQueueIsErrorFree = view.lastOutgoingQueueOperation.error == nil;
242 NSString* hasTLKsKey = [NSString stringWithFormat:@"%@-%@", viewName, CKKSAnalyticsHasTLKs];
243 NSString* syncedClassARecentlyKey = [NSString stringWithFormat:@"%@-%@", viewName, CKKSAnalyticsSyncedClassARecently];
244 NSString* syncedClassCRecentlyKey = [NSString stringWithFormat:@"%@-%@", viewName, CKKSAnalyticsSyncedClassCRecently];
245 NSString* incomingQueueIsErrorFreeKey = [NSString stringWithFormat:@"%@-%@", viewName, CKKSAnalyticsIncomingQueueIsErrorFree];
246 NSString* outgoingQueueIsErrorFreeKey = [NSString stringWithFormat:@"%@-%@", viewName, CKKSAnalyticsOutgoingQueueIsErrorFree];
248 values[hasTLKsKey] = @(hasTLKs);
249 values[syncedClassARecentlyKey] = @(syncedClassARecently);
250 values[syncedClassCRecentlyKey] = @(syncedClassCRecently);
251 values[incomingQueueIsErrorFreeKey] = @(incomingQueueIsErrorFree);
252 values[outgoingQueueIsErrorFreeKey] = @(outgoingQueueIsErrorFree);
254 BOOL weThinkWeAreInSync = inCircle && hasTLKs && syncedClassARecently && syncedClassCRecently && incomingQueueIsErrorFree && outgoingQueueIsErrorFree;
255 NSString* inSyncKey = [NSString stringWithFormat:@"%@-%@", viewName, CKKSAnalyticsInSync];
256 values[inSyncKey] = @(weThinkWeAreInSync);
264 [self clearAllViews];
267 dispatch_queue_t globalZoneStateQueue = NULL;
268 dispatch_once_t globalZoneStateQueueOnce;
270 // We can't load the rate limiter in an init method, as the method might end up calling itself (if the database layer isn't yet initialized).
271 // Lazy-load it here.
272 - (CKKSRateLimiter*)getGlobalRateLimiter {
273 dispatch_once(&globalZoneStateQueueOnce, ^{
274 globalZoneStateQueue = dispatch_queue_create("CKKS global zone state", DISPATCH_QUEUE_SERIAL);
277 if(_globalRateLimiter != nil) {
278 return _globalRateLimiter;
281 __block CKKSRateLimiter* blocklimit = nil;
283 dispatch_sync(globalZoneStateQueue, ^{
284 NSError* error = nil;
286 // Special object containing state for all zones. Currently, just the rate limiter.
287 CKKSZoneStateEntry* allEntry = [CKKSZoneStateEntry tryFromDatabase: @"all" error:&error];
290 secerror("CKKSViewManager: couldn't load global zone state: %@", error);
293 if(!error && allEntry.rateLimiter) {
294 blocklimit = allEntry.rateLimiter;
296 blocklimit = [[CKKSRateLimiter alloc] init];
299 _globalRateLimiter = blocklimit;
300 return _globalRateLimiter;
303 - (void)lockStateChangeNotification:(bool)unlocked
306 [[CKKSAnalytics logger] setDateProperty:[NSDate date] forKey:CKKSAnalyticsLastUnlock];
310 // Mostly exists to be mocked out.
312 return CFBridgingRelease(SOSViewCopyViewSet(kViewSetCKKS));
315 - (void)setView: (CKKSKeychainView*) obj {
316 CKKSKeychainView* kcv = nil;
318 @synchronized(self.views) {
319 kcv = self.views[obj.zoneName];
320 self.views[obj.zoneName] = obj;
324 [kcv cancelAllOperations];
328 - (void)clearAllViews {
329 NSArray<CKKSKeychainView*>* tempviews = nil;
330 @synchronized(self.views) {
331 tempviews = [self.views.allValues copy];
332 [self.views removeAllObjects];
335 for(CKKSKeychainView* view in tempviews) {
336 [view cancelAllOperations];
340 - (void)clearView:(NSString*) viewName {
341 CKKSKeychainView* kcv = nil;
342 @synchronized(self.views) {
343 kcv = self.views[viewName];
344 self.views[viewName] = nil;
348 [kcv cancelAllOperations];
352 - (CKKSKeychainView*)findView:(NSString*)viewName {
356 @synchronized(self.views) {
357 return self.views[viewName];
361 - (CKKSKeychainView*)findOrCreateView:(NSString*)viewName {
362 @synchronized(self.views) {
363 CKKSKeychainView* kcv = self.views[viewName];
368 self.views[viewName] = [[CKKSKeychainView alloc] initWithContainer: self.container
370 accountTracker: self.accountTracker
371 lockStateTracker: self.lockStateTracker
372 reachabilityTracker: self.reachabilityTracker
373 savedTLKNotifier: self.savedTLKNotifier
375 fetchRecordZoneChangesOperationClass: self.fetchRecordZoneChangesOperationClass
376 fetchRecordsOperationClass: self.fetchRecordsOperationClass
377 queryOperationClass:self.queryOperationClass
378 modifySubscriptionsOperationClass: self.modifySubscriptionsOperationClass
379 modifyRecordZonesOperationClass: self.modifyRecordZonesOperationClass
380 apsConnectionClass: self.apsConnectionClass
381 notifierClass: self.notifierClass];
383 if(self.initializeNewZones) {
384 [self.views[viewName] initializeZone];
387 return self.views[viewName];
391 - (NSDictionary<NSString *,NSString *> *)activeTLKs
393 NSMutableDictionary<NSString *,NSString *> *tlks = [NSMutableDictionary new];
394 @synchronized(self.views) {
395 for (NSString *name in self.views) {
396 CKKSKeychainView *view = self.views[name];
397 NSString *tlk = view.lastActiveTLKUUID;
406 - (CKKSKeychainView*)restartZone:(NSString*)viewName {
407 @synchronized(self.views) {
408 [self.views[viewName] halt];
409 self.views[viewName] = nil;
411 return [self findOrCreateView: viewName];
414 // Allows all views to begin initializing, and opens the floodgates so that new views will be initalized immediately
415 - (void)initializeZones {
416 if(!SecCKKSIsEnabled()) {
417 secnotice("ckks", "Not initializing CKKS view set as CKKS is disabled");
421 @synchronized(self.views) {
422 self.initializeNewZones = true;
424 NSSet* viewSet = [self viewList];
425 for(NSString* s in viewSet) {
426 [self findOrCreateView:s]; // initializes any newly-created views
430 [self setupAnalytics];
433 - (NSString*)viewNameForViewHint: (NSString*) viewHint {
434 // For now, choose view based on viewhints.
435 if(viewHint && ![viewHint isEqual: [NSNull null]]) {
439 // If there isn't a provided view hint, use the "keychain" view if we're testing. Otherwise, nil.
440 if(SecCKKSTestsEnabled()) {
447 - (NSString*)viewNameForItem: (SecDbItemRef) item {
448 CFErrorRef cferror = NULL;
449 NSString* viewHint = (__bridge NSString*) SecDbItemGetValue(item, &v7vwht, &cferror);
452 secerror("ckks: Couldn't fetch the viewhint for some reason: %@", cferror);
453 CFReleaseNull(cferror);
457 return [self viewNameForViewHint: viewHint];
460 - (NSString*)viewNameForAttributes: (NSDictionary*) item {
461 return [self viewNameForViewHint: item[(id)kSecAttrSyncViewHint]];
464 - (void)registerSyncStatusCallback: (NSString*) uuid callback: (SecBoolNSErrorCallback) callback {
465 // Someone is requesting future notification of this item.
466 @synchronized(self.pendingSyncCallbacks) {
467 self.pendingSyncCallbacks[uuid] = callback;
471 - (void) handleKeychainEventDbConnection: (SecDbConnectionRef) dbconn source:(SecDbTransactionSource)txionSource added: (SecDbItemRef) added deleted: (SecDbItemRef) deleted {
473 SecDbItemRef modified = added ? added : deleted;
475 NSString* viewName = [self viewNameForItem: modified];
476 NSString* keyViewName = [CKKSKey isItemKeyForKeychainView: modified];
479 // This might be some key material for this view! Poke it.
480 CKKSKeychainView* view = [self findView: keyViewName];
482 if(!SecCKKSTestDisableKeyNotifications()) {
483 ckksnotice("ckks", view, "Potential new key material from %@ (source %lu)", keyViewName, txionSource);
484 [view keyStateMachineRequestProcess];
486 ckksnotice("ckks", view, "Ignoring potential new key material from %@ (source %lu)", keyViewName, txionSource);
491 // When SOS is in charge of a view, CKKS is not.
492 // Since this isn't a CKKS key item, we don't care about it.
493 if(txionSource == kSecDbSOSTransaction) {
494 secinfo("ckks", "Ignoring new non-CKKS item in kSecDbSOSTransaction notification");
497 // Looks like a normal item. Proceed!
498 CKKSKeychainView* view = [self findView:viewName];
500 NSString* uuid = (__bridge NSString*) SecDbItemGetValue(modified, &v10itemuuid, NULL);
501 SecBoolNSErrorCallback syncCallback = nil;
503 @synchronized(self.pendingSyncCallbacks) {
504 syncCallback = self.pendingSyncCallbacks[uuid];
505 self.pendingSyncCallbacks[uuid] = nil;
508 secinfo("ckks", "Have a pending callback for %@; passing along", uuid);
514 secinfo("ckks", "No CKKS view for %@, skipping: %@", viewName, modified);
516 syncCallback(false, [NSError errorWithDomain:@"securityd"
517 code:kSOSCCNoSuchView
518 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"No syncing view for '%@'", viewName]}]);
523 ckksnotice("ckks", view, "Routing item to zone %@: %@", viewName, modified);
524 [view handleKeychainEventDbConnection: dbconn added:added deleted:deleted rateLimiter:self.globalRateLimiter syncCallback: syncCallback];
527 -(void)setCurrentItemForAccessGroup:(NSData* _Nonnull)newItemPersistentRef
528 hash:(NSData*)newItemSHA1
529 accessGroup:(NSString*)accessGroup
530 identifier:(NSString*)identifier
531 viewHint:(NSString*)viewHint
532 replacing:(NSData* _Nullable)oldCurrentItemPersistentRef
533 hash:(NSData*)oldItemSHA1
534 complete:(void (^) (NSError* operror)) complete
536 CKKSKeychainView* view = [self findView:viewHint];
539 secnotice("ckks", "No CKKS view for %@, skipping current request", viewHint);
540 complete([NSError errorWithDomain:CKKSErrorDomain
542 description:[NSString stringWithFormat: @"No syncing view for view hint '%@'", viewHint]]);
546 [view setCurrentItemForAccessGroup:newItemPersistentRef
548 accessGroup:accessGroup
549 identifier:identifier
550 replacing:oldCurrentItemPersistentRef
555 -(void)getCurrentItemForAccessGroup:(NSString*)accessGroup
556 identifier:(NSString*)identifier
557 viewHint:(NSString*)viewHint
558 fetchCloudValue:(bool)fetchCloudValue
559 complete:(void (^) (NSString* uuid, NSError* operror)) complete
561 CKKSKeychainView* view = [self findView:viewHint];
563 secnotice("ckks", "No CKKS view for %@, skipping current fetch request", viewHint);
564 complete(NULL, [NSError errorWithDomain:CKKSErrorDomain
566 description:[NSString stringWithFormat: @"No view for '%@'", viewHint]]);
570 [view getCurrentItemForAccessGroup:accessGroup
571 identifier:identifier
572 fetchCloudValue:fetchCloudValue
577 + (instancetype) manager {
578 return [self resetManager: false setTo: nil];
581 + (instancetype) resetManager: (bool) reset setTo: (CKKSViewManager*) obj {
582 static CKKSViewManager* manager = nil;
584 if([CKDatabase class] == nil) {
585 secerror("CKKS: CloudKit.framework appears to not be linked. Can't create CKKS objects.");
589 if(!manager || reset || obj) {
590 @synchronized([self class]) {
592 [manager clearAllViews];
596 [manager clearAllViews];
598 } else if (manager == nil && SecCKKSIsEnabled()) {
599 manager = [[CKKSViewManager alloc] initCloudKitWithContainerName:SecCKKSContainerName usePCS:SecCKKSContainerUsePCS];
608 - (void)cancelPendingOperations {
609 [self.savedTLKNotifier cancel];
612 -(void)notifyNewTLKsInKeychain {
613 // Why two functions here? Limitation of OCMock, unfortunately: can't stub and expect the same method
614 secnotice("ckksbackup", "New TLKs have arrived");
615 [self syncBackupAndNotifyAboutSync];
618 - (void)syncBackupAndNotifyAboutSync {
619 SOSAccount* account = (__bridge SOSAccount*)SOSKeychainAccountGetSharedAccount();
621 [account performTransaction:^(SOSAccountTransaction * _Nonnull txn) {
622 CFErrorRef error = NULL;
623 NSSet* ignore = CFBridgingRelease(SOSAccountCopyBackupPeersAndForceSync(txn, &error));
627 secerror("ckksbackup: Couldn't process sync with backup peers: %@", error);
629 secnotice("ckksbackup", "telling CloudServices about TLK arrival");
630 notify_post(kSecItemBackupNotification);
635 #pragma mark - RPCs to manage and report state
637 - (void)performanceCounters:(void(^)(NSDictionary <NSString *, NSNumber *> *counter))reply {
641 - (NSArray<CKKSKeychainView*>*)views:(NSString*)viewName operation:(NSString*)opName error:(NSError**)error
643 NSArray* actualViews = nil;
645 // Ensure we've actually set up, but don't wait too long. Clients get impatient.
646 if([self.completedSecCKKSInitialize wait:5*NSEC_PER_SEC]) {
647 secerror("ckks: Haven't yet initialized zones; expect failure fetching views");
650 @synchronized(self.views) {
652 CKKSKeychainView* view = self.views[viewName];
653 secnotice("ckks", "Received a %@ request for zone %@ (%@)", opName, viewName, view);
657 *error = [NSError errorWithDomain:CKKSErrorDomain
659 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"No view for '%@'", viewName]}];
664 actualViews = @[view];
666 actualViews = [self.views.allValues copy];
667 secnotice("ckks", "Received a %@ request for all zones: %@", opName, actualViews);
673 - (void)rpcResetLocal:(NSString*)viewName reply: (void(^)(NSError* result)) reply {
674 NSError* localError = nil;
675 NSArray* actualViews = [self views:viewName operation:@"local reset" error:&localError];
677 secerror("ckks: Error getting view %@: %@", viewName, localError);
682 CKKSResultOperation* op = [CKKSResultOperation named:@"local-reset-zones-waiter" withBlockTakingSelf:^(CKKSResultOperation * _Nonnull strongOp) {
683 if(!strongOp.error) {
684 secnotice("ckksreset", "Completed rpcResetLocal");
686 secnotice("ckks", "Completed rpcResetLocal with error: %@", strongOp.error);
688 reply(CKXPCSuitableError(strongOp.error));
691 for(CKKSKeychainView* view in actualViews) {
692 ckksnotice("ckksreset", view, "Beginning local reset for %@", view);
693 [op addSuccessDependency:[view resetLocalData]];
696 [op timeout:120*NSEC_PER_SEC];
697 [self.operationQueue addOperation: op];
700 - (void)rpcResetCloudKit:(NSString*)viewName reply: (void(^)(NSError* result)) reply {
701 NSError* localError = nil;
702 NSArray* actualViews = [self views:viewName operation:@"CloudKit reset" error:&localError];
704 secerror("ckks: Error getting view %@: %@", viewName, localError);
709 CKKSResultOperation* op = [CKKSResultOperation named:@"cloudkit-reset-zones-waiter" withBlockTakingSelf:^(CKKSResultOperation * _Nonnull strongOp) {
710 if(!strongOp.error) {
711 secnotice("ckksreset", "Completed rpcResetCloudKit");
713 secnotice("ckksreset", "Completed rpcResetCloudKit with error: %@", strongOp.error);
715 reply(CKXPCSuitableError(strongOp.error));
718 for(CKKSKeychainView* view in actualViews) {
719 ckksnotice("ckksreset", view, "Beginning CloudKit reset for %@", view);
720 [op addSuccessDependency:[view resetCloudKitZone:[CKOperationGroup CKKSGroupWithName:@"api-reset"]]];
723 [op timeout:120*NSEC_PER_SEC];
724 [self.operationQueue addOperation: op];
727 - (void)rpcResync:(NSString*)viewName reply: (void(^)(NSError* result)) reply {
728 NSError* localError = nil;
729 NSArray* actualViews = [self views:viewName operation:@"CloudKit resync" error:&localError];
731 secerror("ckks: Error getting view %@: %@", viewName, localError);
736 CKKSResultOperation* op = [[CKKSResultOperation alloc] init];
737 op.name = @"rpc-resync-cloudkit";
738 __weak __typeof(op) weakOp = op;
739 [op addExecutionBlock:^{
740 __strong __typeof(op) strongOp = weakOp;
741 secnotice("ckks", "Ending rsync-CloudKit rpc with %@", strongOp.error);
744 for(CKKSKeychainView* view in actualViews) {
745 ckksnotice("ckksresync", view, "Beginning resync (CloudKit) for %@", view);
747 CKKSSynchronizeOperation* resyncOp = [view resyncWithCloud];
748 [op addSuccessDependency:resyncOp];
751 [op timeout:120*NSEC_PER_SEC];
752 [self.operationQueue addOperation:op];
753 [op waitUntilFinished];
754 reply(CKXPCSuitableError(op.error));
757 - (void)rpcResyncLocal:(NSString*)viewName reply:(void(^)(NSError* result))reply {
758 NSError* localError = nil;
759 NSArray* actualViews = [self views:viewName operation:@"local resync" error:&localError];
761 secerror("ckks: Error getting view %@: %@", viewName, localError);
766 CKKSResultOperation* op = [[CKKSResultOperation alloc] init];
767 op.name = @"rpc-resync-local";
768 __weak __typeof(op) weakOp = op;
769 [op addExecutionBlock:^{
770 __strong __typeof(op) strongOp = weakOp;
771 secnotice("ckks", "Ending rsync-local rpc with %@", strongOp.error);
772 reply(CKXPCSuitableError(strongOp.error));
775 for(CKKSKeychainView* view in actualViews) {
776 ckksnotice("ckksresync", view, "Beginning resync (local) for %@", view);
778 CKKSLocalSynchronizeOperation* resyncOp = [view resyncLocal];
779 [op addSuccessDependency:resyncOp];
782 [op timeout:120*NSEC_PER_SEC];
785 - (void)rpcStatus: (NSString*)viewName reply: (void(^)(NSArray<NSDictionary*>* result, NSError* error)) reply {
786 NSMutableArray* a = [[NSMutableArray alloc] init];
788 // Now, query the views about their status
789 NSError* error = nil;
790 NSArray* actualViews = [self views:viewName operation:@"status" error:&error];
791 if(!actualViews || error) {
796 __weak __typeof(self) weakSelf = self;
797 CKKSResultOperation* statusOp = [CKKSResultOperation named:@"status-rpc" withBlock:^{
798 __strong __typeof(self) strongSelf = weakSelf;
800 // The first element is always the current global state (non-view-specific)
801 NSError* selfPeersError = nil;
802 CKKSSelves* selves = [strongSelf fetchSelfPeers:&selfPeersError];
803 NSError* trustedPeersError = nil;
804 NSSet<id<CKKSPeer>>* peers = [strongSelf fetchTrustedPeers:&trustedPeersError];
806 // Get account state, even wait for it a little
807 [self.accountTracker.ckdeviceIDInitialized wait:1*NSEC_PER_SEC];
808 NSString *deviceID = self.accountTracker.ckdeviceID;
809 NSError *deviceIDError = self.accountTracker.ckdeviceIDError;
811 NSMutableArray<NSString*>* mutTrustedPeers = [[NSMutableArray alloc] init];
812 [peers enumerateObjectsUsingBlock:^(id<CKKSPeer> _Nonnull obj, BOOL * _Nonnull stop) {
813 [mutTrustedPeers addObject: [obj description]];
816 #define stringify(obj) CKKSNilToNSNull([obj description])
817 NSDictionary* global = @{
819 @"selfPeers": stringify(selves),
820 @"selfPeersError": CKKSNilToNSNull(selfPeersError),
821 @"trustedPeers": CKKSNilToNSNull(mutTrustedPeers),
822 @"trustedPeersError": CKKSNilToNSNull(trustedPeersError),
823 @"reachability": strongSelf.reachabilityTracker.currentReachability ? @"network" : @"no-network",
824 @"ckdeviceID": CKKSNilToNSNull(deviceID),
825 @"ckdeviceIDError": CKKSNilToNSNull(deviceIDError),
827 [a addObject: global];
829 for(CKKSKeychainView* view in actualViews) {
830 ckksnotice("ckks", view, "Fetching status for %@", view.zoneName);
831 NSDictionary* status = [view status];
832 ckksinfo("ckks", view, "Status is %@", status);
834 [a addObject: status];
840 // If we're signed in, give the views a few seconds to enter what they consider to be a non-transient state (in case this daemon just launched)
841 if([self.accountTracker.currentComputedAccountStatusValid wait:5*NSEC_PER_SEC]) {
842 secerror("ckks status: Haven't yet figured out login state");
845 if(self.accountTracker.currentComputedAccountStatus == CKKSAccountStatusAvailable) {
846 CKKSResultOperation* blockOp = [CKKSResultOperation named:@"wait-for-status" withBlock:^{}];
847 [blockOp timeout:8*NSEC_PER_SEC];
848 for(CKKSKeychainView* view in actualViews) {
849 [blockOp addNullableDependency:view.keyStateNonTransientDependency];
850 [statusOp addDependency:blockOp];
852 [self.operationQueue addOperation:blockOp];
854 [self.operationQueue addOperation:statusOp];
859 - (void)rpcFetchAndProcessChanges:(NSString*)viewName reply: (void(^)(NSError* result))reply {
860 [self rpcFetchAndProcessChanges:viewName classA:false reply: (void(^)(NSError* result))reply];
863 - (void)rpcFetchAndProcessClassAChanges:(NSString*)viewName reply: (void(^)(NSError* result))reply {
864 [self rpcFetchAndProcessChanges:viewName classA:true reply:(void(^)(NSError* result))reply];
867 - (void)rpcFetchAndProcessChanges:(NSString*)viewName classA:(bool)classAError reply: (void(^)(NSError* result)) reply {
868 NSError* error = nil;
869 NSArray* actualViews = [self views:viewName operation:@"fetch" error:&error];
870 if(!actualViews || error) {
875 CKKSResultOperation* blockOp = [[CKKSResultOperation alloc] init];
876 blockOp.name = @"rpc-fetch-and-process-result";
877 __weak __typeof(blockOp) weakBlockOp = blockOp;
878 // Use the completion block instead of the operation block, so that it runs even if the cancel fires
879 [blockOp setCompletionBlock:^{
880 __strong __typeof(blockOp) strongBlockOp = weakBlockOp;
881 [strongBlockOp allDependentsSuccessful];
882 reply(CKXPCSuitableError(strongBlockOp.error));
885 for(CKKSKeychainView* view in actualViews) {
886 ckksnotice("ckks", view, "Beginning fetch for %@", view);
888 CKKSResultOperation* op = [view processIncomingQueue:classAError after:[view.zoneChangeFetcher requestSuccessfulFetch: CKKSFetchBecauseAPIFetchRequest]];
889 [blockOp addDependency:op];
892 [self.operationQueue addOperation: [blockOp timeout:(SecCKKSTestsEnabled() ? NSEC_PER_SEC * 5 : NSEC_PER_SEC * 120)]];
895 - (void)rpcPushOutgoingChanges:(NSString*)viewName reply: (void(^)(NSError* result))reply {
896 NSError* error = nil;
897 NSArray* actualViews = [self views:viewName operation:@"push" error:&error];
898 if(!actualViews || error) {
903 CKKSResultOperation* blockOp = [[CKKSResultOperation alloc] init];
904 blockOp.name = @"rpc-push";
905 __weak __typeof(blockOp) weakBlockOp = blockOp;
906 // Use the completion block instead of the operation block, so that it runs even if the cancel fires
907 [blockOp setCompletionBlock:^{
908 __strong __typeof(blockOp) strongBlockOp = weakBlockOp;
909 [strongBlockOp allDependentsSuccessful];
910 reply(CKXPCSuitableError(strongBlockOp.error));
913 for(CKKSKeychainView* view in actualViews) {
914 ckksnotice("ckks-rpc", view, "Beginning push for %@", view);
916 CKKSResultOperation* op = [view processOutgoingQueue: [CKOperationGroup CKKSGroupWithName:@"rpc-push"]];
917 [blockOp addDependency:op];
920 [self.operationQueue addOperation: [blockOp timeout:(SecCKKSTestsEnabled() ? NSEC_PER_SEC * 2 : NSEC_PER_SEC * 120)]];
923 - (void)rpcGetCKDeviceIDWithReply:(void (^)(NSString *))reply {
924 reply(self.accountTracker.ckdeviceID);
927 -(void)xpc24HrNotification {
928 // XPC has poked us and said we should do some cleanup!
930 // For now, poke the views and tell them to update their device states if they'd like
931 NSArray* actualViews = nil;
932 @synchronized(self.views) {
933 // Can't safely iterate a mutable collection, so copy it.
934 actualViews = self.views.allValues;
937 secnotice("ckks", "Received a 24hr notification from XPC");
938 CKOperationGroup* group = [CKOperationGroup CKKSGroupWithName:@"periodic-device-state-update"];
939 for(CKKSKeychainView* view in actualViews) {
940 ckksnotice("ckks", view, "Starting device state XPC update");
941 // Let the update know it should rate-limit itself
942 [view updateDeviceState:true waitForKeyHierarchyInitialization:30*NSEC_PER_SEC ckoperationGroup:group];
946 - (NSArray<NSDictionary *> * _Nullable)loadRestoredBottledKeysOfType:(OctagonKeyType)keyType error:(NSError**)error
948 CFTypeRef result = NULL;
949 NSMutableArray* bottledPeerKeychainItems = nil;
951 NSDictionary* query = @{
952 (id)kSecClass : (id)kSecClassInternetPassword,
953 (id)kSecAttrAccessible: (id)kSecAttrAccessibleWhenUnlocked,
954 (id)kSecAttrNoLegacy : @YES,
955 (id)kSecAttrType : [[NSNumber alloc]initWithInt: keyType],
956 (id)kSecAttrServer : (keyType == 1) ? @"Octagon Signing Key" : @"Octagon Encryption Key",
957 (id)kSecAttrAccessGroup: @"com.apple.security.ckks",
958 (id)kSecMatchLimit : (id)kSecMatchLimitAll,
959 (id)kSecReturnAttributes: @YES,
960 (id)kSecReturnData: @YES,
962 OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
964 if(status == errSecSuccess && result && isArray(result)) {
965 bottledPeerKeychainItems = CFBridgingRelease(result);
969 *error = [NSError errorWithDomain:NSOSStatusErrorDomain
971 description:@"could not load bottled peer keys"];
973 CFReleaseNull(result);
976 return bottledPeerKeychainItems;
979 -(NSDictionary *) keychainItemForPeerID:(NSString*)neededPeerID
980 keychainItems:(NSArray<NSDictionary*> *)keychainItems
981 escrowSigningPubKeyHash:(NSString *)hashWeNeedToMatch
983 NSDictionary* peerItem = nil;
985 for(NSDictionary* item in keychainItems){
986 if(item && [item count] > 0){
987 NSString* peerIDFromItem = [item objectForKey:(id)kSecAttrAccount];
988 NSString* hashToConsider = [item objectForKey:(id)kSecAttrLabel];
989 if([peerIDFromItem isEqualToString:neededPeerID] &&
990 [hashWeNeedToMatch isEqualToString:hashToConsider])
992 peerItem = [item copy];
1001 - (NSSet<id<CKKSSelfPeer>>*)pastSelves:(NSError**)error
1003 NSError* localError = nil;
1005 // get bottled peer identities from the keychain
1006 NSMutableSet<id<CKKSSelfPeer>>* allSelves = [NSMutableSet set];
1007 NSArray<NSDictionary*>* signingKeys = [self loadRestoredBottledKeysOfType:OctagonSigningKey error:&localError];
1009 // Item not found isn't actually an error here
1010 if(error && !(localError && [localError.domain isEqualToString: NSOSStatusErrorDomain] && localError.code == errSecItemNotFound)) {
1011 *error = localError;
1017 NSArray<NSDictionary*>* encryptionKeys = [self loadRestoredBottledKeysOfType:OctagonEncryptionKey error:&localError];
1018 if(!encryptionKeys) {
1019 if(error && !(localError && [localError.domain isEqualToString: NSOSStatusErrorDomain] && localError.code == errSecItemNotFound)) {
1020 *error = localError;
1025 for(NSDictionary* signingKey in signingKeys) {
1026 NSError* peerError = nil;
1027 NSString* peerid = signingKey[(id)kSecAttrAccount];
1028 NSString* hash = signingKey[(id)kSecAttrLabel]; // escrow signing pub key hash
1030 //use peer id AND escrow signing public key hash to look up the matching item in encryptionKeys list
1031 NSDictionary* encryptionKeyItem = [self keychainItemForPeerID:peerid keychainItems:encryptionKeys escrowSigningPubKeyHash:hash];
1032 if(!encryptionKeyItem) {
1033 secerror("octagon: no encryption key available to pair with signing key %@,%@", peerid, hash);
1037 NSData* signingKeyData = signingKey[(id)kSecValueData];
1038 if(!signingKeyData) {
1039 secerror("octagon: no signing key data for %@,%@", peerid,hash);
1043 SFECKeyPair* restoredSigningKey = [[SFECKeyPair alloc] initWithData:signingKeyData
1044 specifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]
1046 if(!restoredSigningKey) {
1047 secerror("octagon: couldn't make signing key for %@,%@: %@", peerid, hash, peerError);
1051 NSData* encryptionKeyData = [encryptionKeyItem objectForKey:(id)kSecValueData];
1052 if(!encryptionKeyData) {
1053 secerror("octagon: no encryption key data for %@,%@", peerid,hash);
1057 SFECKeyPair* restoredEncryptionKey = [[SFECKeyPair alloc] initWithData:encryptionKeyData
1058 specifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]
1060 if(!restoredEncryptionKey) {
1061 secerror("octagon: couldn't make encryption key for %@,%@: %@", peerid,hash, peerError);
1065 //create the SOS self peer
1066 CKKSSOSSelfPeer* restoredIdentity = [[CKKSSOSSelfPeer alloc]initWithSOSPeerID:peerid encryptionKey:restoredEncryptionKey signingKey:restoredSigningKey];
1068 if(restoredIdentity){
1069 secnotice("octagon","adding bottled peer identity: %@", restoredIdentity);
1070 [allSelves addObject:restoredIdentity];
1072 secerror("octagon: could not create restored identity from: %@: %@", peerid, peerError);
1078 - (id<CKKSSelfPeer> _Nullable)currentSOSSelf:(NSError**)error
1080 __block SFECKeyPair* signingPrivateKey = nil;
1081 __block SFECKeyPair* encryptionPrivateKey = nil;
1083 __block NSError* localerror = nil;
1085 // Wait for this to initialize, but don't worry if it isn't.
1086 [self.accountTracker.accountCirclePeerIDInitialized wait:500*NSEC_PER_MSEC];
1087 NSString* peerID = self.accountTracker.accountCirclePeerID;
1088 if(!peerID || self.accountTracker.accountCirclePeerIDError) {
1089 secerror("ckkspeer: Error fetching self peer : %@", self.accountTracker.accountCirclePeerIDError);
1091 *error = self.accountTracker.accountCirclePeerIDError;
1096 SOSCCPerformWithAllOctagonKeys(^(SecKeyRef octagonEncryptionKey, SecKeyRef octagonSigningKey, CFErrorRef cferror) {
1098 localerror = (__bridge NSError*)cferror;
1101 if (!cferror && octagonEncryptionKey && octagonSigningKey) {
1102 signingPrivateKey = [[SFECKeyPair alloc] initWithSecKey:octagonSigningKey];
1103 encryptionPrivateKey = [[SFECKeyPair alloc] initWithSecKey:octagonEncryptionKey];
1105 localerror = [NSError errorWithDomain:CKKSErrorDomain
1106 code:CKKSNoPeersAvailable
1107 description:@"Not all SOS peer keys available, but no error returned"];
1112 if(![self.lockStateTracker isLockedError:localerror]) {
1113 secerror("ckkspeer: Error fetching self encryption keys: %@", localerror);
1116 *error = localerror;
1121 CKKSSOSSelfPeer* selfPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:peerID
1122 encryptionKey:encryptionPrivateKey
1123 signingKey:signingPrivateKey];
1127 #pragma mark - CKKSPeerProvider implementation
1129 - (CKKSSelves*)fetchSelfPeers:(NSError* __autoreleasing *)error {
1130 NSError* localError = nil;
1132 id<CKKSSelfPeer> selfPeer = [self currentSOSSelf:&localError];
1133 if(!selfPeer || localError) {
1134 if(![self.lockStateTracker isLockedError:localError]) {
1135 secerror("ckks: Error fetching current SOS self: %@", localError);
1138 *error = localError;
1143 NSSet<id<CKKSSelfPeer>>* allSelves = [self pastSelves:&localError];
1144 if(!allSelves || localError) {
1145 secerror("ckks: Error fetching past selves: %@", localError);
1147 *error = localError;
1152 CKKSSelves* selves = [[CKKSSelves alloc] initWithCurrent:selfPeer allSelves:allSelves];
1156 - (NSSet<id<CKKSPeer>>*)fetchTrustedPeers:(NSError* __autoreleasing *)error {
1157 __block NSMutableSet<id<CKKSPeer>>* peerSet = [NSMutableSet set];
1159 SOSCCPerformWithTrustedPeers(^(CFSetRef sosPeerInfoRefs, CFErrorRef cfTrustedPeersError) {
1160 if(cfTrustedPeersError) {
1161 secerror("ckks: Error fetching trusted peers: %@", cfTrustedPeersError);
1163 *error = (__bridge NSError*)cfTrustedPeersError;
1167 CFSetForEach(sosPeerInfoRefs, ^(const void* voidPeer) {
1168 CFErrorRef cfPeerError = NULL;
1169 SOSPeerInfoRef sosPeerInfoRef = (SOSPeerInfoRef)voidPeer;
1171 if(!sosPeerInfoRef) {
1175 CFStringRef cfpeerID = SOSPeerInfoGetPeerID(sosPeerInfoRef);
1176 SecKeyRef cfOctagonSigningKey = NULL, cfOctagonEncryptionKey = NULL;
1178 cfOctagonSigningKey = SOSPeerInfoCopyOctagonSigningPublicKey(sosPeerInfoRef, &cfPeerError);
1179 if (cfOctagonSigningKey) {
1180 cfOctagonEncryptionKey = SOSPeerInfoCopyOctagonEncryptionPublicKey(sosPeerInfoRef, &cfPeerError);
1183 if(cfOctagonSigningKey == NULL || cfOctagonEncryptionKey == NULL) {
1184 // Don't log non-debug for -50; it almost always just means this peer didn't have octagon keys
1185 if(cfPeerError == NULL
1186 || !(CFEqualSafe(CFErrorGetDomain(cfPeerError), kCFErrorDomainOSStatus) && (CFErrorGetCode(cfPeerError) == errSecParam)))
1188 secerror("ckkspeer: error fetching octagon keys for peer: %@ %@", sosPeerInfoRef, cfPeerError);
1190 secinfo("ckkspeer", "Peer(%@) doesn't have Octagon keys, but this is expected: %@", cfpeerID, cfPeerError);
1194 // Add all peers to the trust set: old-style SOS peers will just have null keys
1195 SFECPublicKey* signingPublicKey = cfOctagonSigningKey ? [[SFECPublicKey alloc] initWithSecKey:cfOctagonSigningKey] : nil;
1196 SFECPublicKey* encryptionPublicKey = cfOctagonEncryptionKey ? [[SFECPublicKey alloc] initWithSecKey:cfOctagonEncryptionKey] : nil;
1198 CKKSSOSPeer* peer = [[CKKSSOSPeer alloc] initWithSOSPeerID:(__bridge NSString*)cfpeerID
1199 encryptionPublicKey:encryptionPublicKey
1200 signingPublicKey:signingPublicKey];
1201 [peerSet addObject:peer];
1203 CFReleaseNull(cfOctagonSigningKey);
1204 CFReleaseNull(cfOctagonEncryptionKey);
1205 CFReleaseNull(cfPeerError);
1212 - (void)registerForPeerChangeUpdates:(id<CKKSPeerUpdateListener>)listener {
1213 @synchronized(self.peerChangeListeners) {
1214 bool alreadyRegisteredListener = false;
1215 NSEnumerator *enumerator = [self.peerChangeListeners objectEnumerator];
1216 id<CKKSPeerUpdateListener> value;
1218 while ((value = [enumerator nextObject])) {
1219 // do pointer comparison
1220 alreadyRegisteredListener |= (value == listener);
1223 if(listener && !alreadyRegisteredListener) {
1224 NSString* queueName = [NSString stringWithFormat: @"ck-peer-change-%@", listener];
1226 dispatch_queue_t objQueue = dispatch_queue_create([queueName UTF8String], DISPATCH_QUEUE_SERIAL);
1227 [self.peerChangeListeners setObject: listener forKey: objQueue];
1232 - (void)iteratePeerListenersOnTheirQueue:(void (^)(id<CKKSPeerUpdateListener>))block {
1233 @synchronized(self.peerChangeListeners) {
1234 NSEnumerator *enumerator = [self.peerChangeListeners keyEnumerator];
1235 dispatch_queue_t dq;
1237 // Queue up the changes for each listener.
1238 while ((dq = [enumerator nextObject])) {
1239 id<CKKSPeerUpdateListener> listener = [self.peerChangeListeners objectForKey: dq];
1240 __weak id<CKKSPeerUpdateListener> weakListener = listener;
1243 dispatch_async(dq, ^{
1244 __strong id<CKKSPeerUpdateListener> strongListener = weakListener;
1245 block(strongListener);
1252 - (void)sendSelfPeerChangedUpdate {
1253 [self.completedSecCKKSInitialize wait:5*NSEC_PER_SEC]; // Wait for bringup, but don't worry if this times out
1255 [self iteratePeerListenersOnTheirQueue: ^(id<CKKSPeerUpdateListener> listener) {
1256 [listener selfPeerChanged];
1260 - (void)sendTrustedPeerSetChangedUpdate {
1261 [self.completedSecCKKSInitialize wait:5*NSEC_PER_SEC]; // Wait for bringup, but don't worry if this times out
1263 [self iteratePeerListenersOnTheirQueue: ^(id<CKKSPeerUpdateListener> listener) {
1264 [listener trustedPeerSetChanged];