CKKSViewManager.m
1 /* 2 * Copyright (c) 2016 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 #import <os/feature_private.h> 25 26 #import "keychain/ckks/CKKSAccountStateTracker.h" 27 #import "keychain/ckks/CKKSViewManager.h" 28 #import "keychain/ckks/CKKSKeychainView.h" 29 #import "keychain/ckks/CKKSSynchronizeOperation.h" 30 #import "keychain/ckks/CKKSKey.h" 31 #import "keychain/ckks/CKKSZoneStateEntry.h" 32 #import "keychain/ckks/CKKSNearFutureScheduler.h" 33 #import "keychain/ckks/CKKSNotifier.h" 34 #import "keychain/ckks/CKKSCondition.h" 35 #import "keychain/ckks/CKKSListenerCollection.h" 36 #import "keychain/ckks/CloudKitCategories.h" 37 #import "keychain/ckks/OctagonAPSReceiver.h" 38 #import "keychain/categories/NSError+UsefulConstructors.h" 39 #import "keychain/analytics/SecEventMetric.h" 40 #import "keychain/analytics/SecMetrics.h" 41 42 #import "keychain/ot/OTManager.h" 43 #import "keychain/ot/OTDefines.h" 44 #import "keychain/ot/OTConstants.h" 45 #import "keychain/ot/ObjCImprovements.h" 46 47 #import "keychain/trust/TrustedPeers/TPSyncingPolicy.h" 48 49 #import "SecEntitlements.h" 50 51 #include "keychain/securityd/SecDbItem.h" 52 #include "keychain/securityd/SecDbKeychainItem.h" 53 #include "keychain/securityd/SecItemSchema.h" 54 #include <Security/SecureObjectSync/SOSViews.h> 55 56 #import <Foundation/NSXPCConnection.h> 57 #import <Foundation/NSXPCConnection_Private.h> 58 59 #include "keychain/SecureObjectSync/SOSAccount.h" 60 #include <Security/SecItemBackup.h> 61 62 #if OCTAGON 63 #import <CloudKit/CloudKit.h> 64 #import <CloudKit/CloudKit_Private.h> 65 66 #import <SecurityFoundation/SFKey.h> 67 #import <SecurityFoundation/SFKey_Private.h> 68 69 #import "CKKSAnalytics.h" 70 #endif 71 72 #if !OCTAGON 73 @interface CKKSViewManager () <NSXPCListenerDelegate> 74 #else 75 @interface CKKSViewManager () <NSXPCListenerDelegate, 76 CKKSCloudKitAccountStateListener> 77 78 @property NSXPCListener *listener; 79 80 @property (nullable) NSSet<NSString*>* viewAllowList; 81 82 // Once you set these, all CKKSKeychainViews created will use them 83 @property CKKSCloudKitClassDependencies* cloudKitClassDependencies; 84 85 @property NSMutableDictionary<NSString*, SecBoolNSErrorCallback>* pendingSyncCallbacks; 86 @property CKKSNearFutureScheduler* savedTLKNotifier;; 87 @property NSOperationQueue* operationQueue; 88 89 @property CKKSListenerCollection<id<CKKSPeerUpdateListener>>* peerChangeListenerCollection; 90 91 @property (nonatomic) BOOL overrideCKKSViewsFromPolicy; 92 @property (nonatomic) BOOL valueCKKSViewsFromPolicy; 93 @property (nonatomic) BOOL startCKOperationAtViewCreation; 94 95 @property BOOL itemModificationsBeforePolicyLoaded; 96 97 // Make writable 98 @property (nullable) TPSyncingPolicy* policy; 99 @property CKKSCondition* policyLoaded; 100 101 #endif 102 @end 103 104 #if OCTAGON 105 @interface CKKSViewManager (lockstateTracker) <CKKSLockStateNotification> 106 @end 107 #endif 108 109 @implementation CKKSViewManager 110 #if OCTAGON 111 112 - (instancetype)initWithContainer:(CKContainer*)container 113 sosAdapter:(id<OTSOSAdapter> _Nullable)sosAdapter 114 accountStateTracker:(CKKSAccountStateTracker*)accountTracker 115 lockStateTracker:(CKKSLockStateTracker*)lockStateTracker 116 reachabilityTracker:(CKKSReachabilityTracker*)reachabilityTracker 117 cloudKitClassDependencies:(CKKSCloudKitClassDependencies*)cloudKitClassDependencies 118 { 119 if(self = [super init]) { 120 _cloudKitClassDependencies = cloudKitClassDependencies; 121 _sosPeerAdapter = sosAdapter; 122 123 _viewAllowList = nil; 124 _container = container; 125 _accountTracker = accountTracker; 126 _lockStateTracker = lockStateTracker; 127 [_lockStateTracker addLockStateObserver:self]; 128 _reachabilityTracker = reachabilityTracker; 129 _itemModificationsBeforePolicyLoaded = NO; 130 131 _zoneChangeFetcher = [[CKKSZoneChangeFetcher alloc] initWithContainer:_container 132 fetchClass:cloudKitClassDependencies.fetchRecordZoneChangesOperationClass 133 reachabilityTracker:_reachabilityTracker]; 134 OctagonAPSReceiver* globalAPSReceiver = [OctagonAPSReceiver receiverForNamedDelegatePort:SecCKKSAPSNamedPort 135 apsConnectionClass:cloudKitClassDependencies.apsConnectionClass]; 136 [globalAPSReceiver registerCKKSReceiver:_zoneChangeFetcher]; 137 138 _zoneModifier = [[CKKSZoneModifier alloc] initWithContainer:_container 139 reachabilityTracker:_reachabilityTracker 140 cloudkitDependencies:cloudKitClassDependencies]; 141 142 _operationQueue = [[NSOperationQueue alloc] init]; 143 144 _peerChangeListenerCollection = [[CKKSListenerCollection alloc] initWithName:@"sos-peer-set"]; 145 146 _views = [[NSMutableDictionary alloc] init]; 147 _pendingSyncCallbacks = [[NSMutableDictionary alloc] init]; 148 149 _startCKOperationAtViewCreation = NO; 150 151 _completedSecCKKSInitialize = [[CKKSCondition alloc] init]; 152 153 WEAKIFY(self); 154 _savedTLKNotifier = [[CKKSNearFutureScheduler alloc] initWithName:@"newtlks" 155 delay:5*NSEC_PER_SEC 156 keepProcessAlive:true 157 dependencyDescriptionCode:CKKSResultDescriptionNone 158 block:^{ 159 STRONGIFY(self); 160 [self notifyNewTLKsInKeychain]; 161 }]; 162 163 _policy = nil; 164 _policyLoaded = [[CKKSCondition alloc] init]; 165 166 _listener = [NSXPCListener anonymousListener]; 167 _listener.delegate = self; 168 [_listener resume]; 169 170 // Start listening for CK account status (for sync callbacks) 171 [_accountTracker registerForNotificationsOfCloudKitAccountStatusChange:self]; 172 } 173 return self; 174 } 175 176 + (CKContainer*)makeCKContainer:(NSString*)containerName 177 usePCS:(bool)usePCS 178 { 179 CKContainer* container = [CKContainer containerWithIdentifier:containerName]; 180 if(!usePCS) { 181 CKContainerOptions* containerOptions = [[CKContainerOptions alloc] init]; 182 containerOptions.bypassPCSEncryption = YES; 183 184 // We don't have a great way to set these, so replace the entire container object 185 container = [[CKContainer alloc] initWithContainerID: container.containerID options:containerOptions]; 186 } 187 return container; 188 } 189 190 - (BOOL)waitForTrustReady { 191 static dispatch_once_t onceToken; 192 __block BOOL success = YES; 193 dispatch_once(&onceToken, ^{ 194 OTManager* manager = [OTManager manager]; 195 if (![manager waitForReady:OTCKContainerName context:OTDefaultContext wait:2*NSEC_PER_SEC]) { 196 success = NO; 197 } 198 }); 199 return success; 200 } 201 202 - (void)setupAnalytics 203 { 204 WEAKIFY(self); 205 206 // Tests shouldn't continue here; it leads to entitlement crashes with CloudKit if the mocks aren't enabled when this function runs 207 if(SecCKKSTestsEnabled()) { 208 return; 209 } 210 211 [[CKKSAnalytics logger] AddMultiSamplerForName:@"CKKS-healthSummary" withTimeInterval:SFAnalyticsSamplerIntervalOncePerReport block:^NSDictionary<NSString *,NSNumber *> *{ 212 STRONGIFY(self); 213 if(!self) { 214 return nil; 215 } 216 217 NSError* sosCircleError = nil; 218 SOSCCStatus sosStatus = [self.sosPeerAdapter circleStatus:&sosCircleError]; 219 if(sosCircleError) { 220 ckkserror_global("manager", " couldn't fetch sos status for SF report: %@", sosCircleError); 221 } 222 223 NSMutableDictionary* values = [NSMutableDictionary dictionary]; 224 BOOL inCircle = (sosStatus == kSOSCCInCircle); 225 if (inCircle) { 226 [[CKKSAnalytics logger] setDateProperty:[NSDate date] forKey:CKKSAnalyticsLastInCircle]; 227 } 228 values[CKKSAnalyticsInCircle] = @(inCircle); 229 230 BOOL validCredentials = self.accountTracker.currentCKAccountInfo.hasValidCredentials; 231 if (!validCredentials) { 232 values[CKKSAnalyticsValidCredentials] = @(validCredentials); 233 } 234 235 NSArray<NSString *>* keys = @[ CKKSAnalyticsLastUnlock, CKKSAnalyticsLastInCircle]; 236 for (NSString * key in keys) { 237 NSDate *date = [[CKKSAnalytics logger] datePropertyForKey:key]; 238 values[key] = @([CKKSAnalytics fuzzyDaysSinceDate:date]); 239 } 240 return values; 241 }]; 242 243 for (NSString* viewName in [self viewList]) { 244 [[CKKSAnalytics logger] AddMultiSamplerForName:[NSString stringWithFormat:@"CKKS-%@-healthSummary", viewName] withTimeInterval:SFAnalyticsSamplerIntervalOncePerReport block:^NSDictionary<NSString *,NSNumber *> *{ 245 STRONGIFY(self); 246 if(!self) { 247 return nil; 248 } 249 250 NSError* sosCircleError = nil; 251 SOSCCStatus sosStatus = [self.sosPeerAdapter circleStatus:&sosCircleError]; 252 if(sosCircleError) { 253 ckkserror_global("manager", " couldn't fetch sos status for SF report: %@", sosCircleError); 254 } 255 BOOL inCircle = (sosStatus == kSOSCCInCircle); 256 NSMutableDictionary* values = [NSMutableDictionary dictionary]; 257 CKKSKeychainView* view = [self findOrCreateView:viewName]; 258 NSDate* dateOfLastSyncClassA = [[CKKSAnalytics logger] dateOfLastSuccessForEvent:CKKSEventProcessIncomingQueueClassA zoneName:view.zoneName]; 259 NSDate* dateOfLastSyncClassC = [[CKKSAnalytics logger] dateOfLastSuccessForEvent:CKKSEventProcessIncomingQueueClassC zoneName:view.zoneName]; 260 NSDate* dateOfLastKSR = [[CKKSAnalytics logger] datePropertyForKey:CKKSAnalyticsLastKeystateReady zoneName:view.zoneName]; 261 262 NSInteger fuzzyDaysSinceClassASync = [CKKSAnalytics fuzzyDaysSinceDate:dateOfLastSyncClassA]; 263 NSInteger fuzzyDaysSinceClassCSync = [CKKSAnalytics fuzzyDaysSinceDate:dateOfLastSyncClassC]; 264 NSInteger fuzzyDaysSinceKSR = [CKKSAnalytics fuzzyDaysSinceDate:dateOfLastKSR]; 265 [values setValue:@(fuzzyDaysSinceClassASync) forKey:[NSString stringWithFormat:@"%@-daysSinceClassASync", viewName]]; 266 [values setValue:@(fuzzyDaysSinceClassCSync) forKey:[NSString stringWithFormat:@"%@-daysSinceClassCSync", viewName]]; 267 [values setValue:@(fuzzyDaysSinceKSR) forKey:[NSString stringWithFormat:@"%@-daysSinceLastKeystateReady", viewName]]; 268 269 NSError* ckmeError = nil; 270 NSNumber* syncedItemRecords = [CKKSMirrorEntry counts:view.zoneID error:&ckmeError]; 271 if(ckmeError || !syncedItemRecords) { 272 ckkserror_global("manager", "couldn't fetch CKMirror counts for %@: %@", view.zoneID, ckmeError); 273 } else { 274 ckksnotice("metrics", view, "View has %@ item ckrecords", syncedItemRecords); 275 values[[NSString stringWithFormat:@"%@-%@", viewName, CKKSAnalyticsNumberOfSyncItems]] = [CKKSAnalytics fuzzyNumber:syncedItemRecords]; 276 } 277 278 NSError* tlkShareCountError = nil; 279 NSNumber* tlkShareCount = [CKKSTLKShareRecord counts:view.zoneID error:&tlkShareCountError]; 280 if(tlkShareCountError || !tlkShareCount) { 281 ckkserror_global("manager", "couldn't fetch CKKSTLKShare counts for %@: %@", view.zoneID, tlkShareCountError); 282 } else { 283 ckksnotice("metrics", view, "View has %@ tlkshares", tlkShareCount); 284 values[[NSString stringWithFormat:@"%@-%@", viewName, CKKSAnalyticsNumberOfTLKShares]] = [CKKSAnalytics fuzzyNumber:tlkShareCount]; 285 } 286 287 NSError* syncKeyCountError = nil; 288 NSNumber* syncKeyCount = [CKKSKey counts:view.zoneID error:&syncKeyCountError]; 289 if(syncKeyCountError || !syncKeyCount) { 290 ckkserror_global("manager", "couldn't fetch CKKSKey counts for %@: %@", view.zoneID, syncKeyCountError); 291 } else { 292 ckksnotice("metrics", view, "View has %@ sync keys", syncKeyCount); 293 values[[NSString stringWithFormat:@"%@-%@", viewName, CKKSAnalyticsNumberOfSyncKeys]] = syncKeyCount; 294 } 295 296 BOOL hasTLKs = [view.stateMachine.currentState isEqualToString:SecCKKSZoneKeyStateReady] || [view.stateMachine.currentState isEqualToString:SecCKKSZoneKeyStateReadyPendingUnlock]; 297 /* only synced recently if between [0...7, ie within 7 days */ 298 BOOL syncedClassARecently = fuzzyDaysSinceClassASync >= 0 && fuzzyDaysSinceClassASync < 7; 299 BOOL syncedClassCRecently = fuzzyDaysSinceClassCSync >= 0 && fuzzyDaysSinceClassCSync < 7; 300 BOOL incomingQueueIsErrorFree = view.lastIncomingQueueOperation.error == nil; 301 BOOL outgoingQueueIsErrorFree = view.lastOutgoingQueueOperation.error == nil; 302 303 NSString* hasTLKsKey = [NSString stringWithFormat:@"%@-%@", viewName, CKKSAnalyticsHasTLKs]; 304 NSString* syncedClassARecentlyKey = [NSString stringWithFormat:@"%@-%@", viewName, CKKSAnalyticsSyncedClassARecently]; 305 NSString* syncedClassCRecentlyKey = [NSString stringWithFormat:@"%@-%@", viewName, CKKSAnalyticsSyncedClassCRecently]; 306 NSString* incomingQueueIsErrorFreeKey = [NSString stringWithFormat:@"%@-%@", viewName, CKKSAnalyticsIncomingQueueIsErrorFree]; 307 NSString* outgoingQueueIsErrorFreeKey = [NSString stringWithFormat:@"%@-%@", viewName, CKKSAnalyticsOutgoingQueueIsErrorFree]; 308 309 values[hasTLKsKey] = @(hasTLKs); 310 values[syncedClassARecentlyKey] = @(syncedClassARecently); 311 values[syncedClassCRecentlyKey] = @(syncedClassCRecently); 312 values[incomingQueueIsErrorFreeKey] = @(incomingQueueIsErrorFree); 313 values[outgoingQueueIsErrorFreeKey] = @(outgoingQueueIsErrorFree); 314 315 BOOL weThinkWeAreInSync = inCircle && hasTLKs && syncedClassARecently && syncedClassCRecently && incomingQueueIsErrorFree && outgoingQueueIsErrorFree; 316 NSString* inSyncKey = [NSString stringWithFormat:@"%@-%@", viewName, CKKSAnalyticsInSync]; 317 values[inSyncKey] = @(weThinkWeAreInSync); 318 319 return values; 320 }]; 321 } 322 } 323 324 -(void)dealloc { 325 [self clearAllViews]; 326 } 327 328 dispatch_queue_t globalZoneStateQueue = NULL; 329 dispatch_once_t globalZoneStateQueueOnce; 330 331 // 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). 332 // Lazy-load it here. 333 - (CKKSRateLimiter*)getGlobalRateLimiter { 334 dispatch_once(&globalZoneStateQueueOnce, ^{ 335 globalZoneStateQueue = dispatch_queue_create("CKKS global zone state", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL); 336 }); 337 338 if(_globalRateLimiter != nil) { 339 return _globalRateLimiter; 340 } 341 342 __block CKKSRateLimiter* blocklimit = nil; 343 344 dispatch_sync(globalZoneStateQueue, ^{ 345 NSError* error = nil; 346 347 // Special object containing state for all zones. Currently, just the rate limiter. 348 CKKSZoneStateEntry* allEntry = [CKKSZoneStateEntry tryFromDatabase: @"all" error:&error]; 349 350 if(error) { 351 ckkserror_global("manager", " couldn't load global zone state: %@", error); 352 } 353 354 if(!error && allEntry.rateLimiter) { 355 blocklimit = allEntry.rateLimiter; 356 } else { 357 blocklimit = [[CKKSRateLimiter alloc] init]; 358 } 359 }); 360 _globalRateLimiter = blocklimit; 361 return _globalRateLimiter; 362 } 363 364 - (void)lockStateChangeNotification:(bool)unlocked 365 { 366 if (unlocked) { 367 [[CKKSAnalytics logger] setDateProperty:[NSDate date] forKey:CKKSAnalyticsLastUnlock]; 368 } 369 } 370 371 #pragma mark - View List handling 372 373 - (NSSet<NSString*>*)defaultViewList { 374 NSSet<NSString*>* fullList = [OTSOSActualAdapter sosCKKSViewList]; 375 376 // Not a great hack: if this platform is an aTV or a HomePod, then filter its list of views 377 bool filter = false; 378 379 #if TARGET_OS_TV 380 filter = true; 381 #elif TARGET_OS_WATCH 382 filter = false; 383 #elif TARGET_OS_IOS 384 filter = !OctagonPlatformSupportsSOS(); 385 #elif TARGET_OS_OSX 386 filter = false; 387 #endif 388 389 if(filter) { 390 // For now, use a hardcoded allow list for TV/HomePod 391 NSMutableSet<NSString*>* allowList = [NSMutableSet setWithArray:@[@"Home", @"LimitedPeersAllowed"]]; 392 [allowList intersectSet:fullList]; 393 return allowList; 394 } 395 396 return fullList; 397 } 398 399 - (NSSet<NSString*>*)viewList { 400 return [self.views.allKeys copy]; 401 } 402 403 - (BOOL)setCurrentSyncingPolicy:(TPSyncingPolicy* _Nullable)syncingPolicy 404 { 405 return [self setCurrentSyncingPolicy:syncingPolicy policyIsFresh:NO]; 406 } 407 408 - (BOOL)setCurrentSyncingPolicy:(TPSyncingPolicy* _Nullable)syncingPolicy policyIsFresh:(BOOL)policyIsFresh 409 { 410 if(syncingPolicy == nil) { 411 ckksnotice_global("ckks-policy", "Nil syncing policy presented; ignoring"); 412 return NO; 413 } 414 415 NSSet<NSString*>* viewNames = syncingPolicy.viewList; 416 ckksnotice_global("ckks-policy", "New syncing policy: %@ views: %@", syncingPolicy, viewNames); 417 418 if(![self useCKKSViewsFromPolicy]) { 419 // Thanks, but no thanks. 420 viewNames = [self defaultViewList]; 421 ckksnotice_global("ckks-policy", "Reverting to default view list: %@", viewNames); 422 } 423 424 if(self.viewAllowList) { 425 ckksnotice_global("ckks-policy", "Intersecting view list with allow list: %@", self.viewAllowList); 426 NSMutableSet<NSString*>* set = [viewNames mutableCopy]; 427 [set intersectSet:self.viewAllowList]; 428 429 viewNames = set; 430 ckksnotice_global("ckks-policy", "Final list: %@", viewNames); 431 } 432 433 // We need to not be synchronized on self.views before issuing commands to CKKS views. 434 // So, store the pointers for use after the critical section. 435 NSArray<CKKSKeychainView*>* activeViews = nil; 436 BOOL scanAll = NO; 437 BOOL viewsChanged = NO; 438 439 @synchronized(self.views) { 440 self.policy = syncingPolicy; 441 442 NSArray* previousViewNames = [self.views.allKeys copy]; 443 444 // First, shut down any views that are no longer in the set 445 for(NSString* viewName in previousViewNames) { 446 if(![viewNames containsObject:viewName]) { 447 ckksnotice_global("ckks-policy", "Stopping old view %@", viewName); 448 [self clearView:viewName]; 449 viewsChanged = YES; 450 } 451 } 452 453 for(NSString* viewName in viewNames) { 454 CKKSKeychainView* view = nil; 455 456 if([previousViewNames containsObject:viewName]) { 457 view = [self findView:viewName]; 458 ckksinfo_global("ckks-policy", "Already have view %@", view); 459 460 } else { 461 view = [self findOrCreateView:viewName]; 462 ckksnotice_global("ckks-policy", "Created new view %@", view); 463 viewsChanged = YES; 464 } 465 } 466 467 activeViews = [self.views.allValues copy]; 468 469 if(self.itemModificationsBeforePolicyLoaded) { 470 ckksnotice_global("ckks-policy", "Issuing scan suggestions to handle missed items"); 471 scanAll = YES; 472 self.itemModificationsBeforePolicyLoaded = NO; 473 } 474 } 475 476 for(CKKSKeychainView* view in activeViews) { 477 [view setCurrentSyncingPolicy:self.policy policyIsFresh:policyIsFresh]; 478 479 if(scanAll) { 480 [view scanLocalItems:@"item-added-before-policy"]; 481 } 482 } 483 484 // Retrigger the analytics setup, so that our views will report status 485 [self setupAnalytics]; 486 487 // The policy is considered loaded once the views have been created 488 [self.policyLoaded fulfill]; 489 return viewsChanged; 490 } 491 492 - (void)setSyncingViewsAllowList:(NSSet<NSString*>*)viewNames 493 { 494 self.viewAllowList = viewNames; 495 } 496 497 - (void)resetSyncingPolicy 498 { 499 ckksnotice_global("ckks-policy", "Setting policy to nil"); 500 self.policy = nil; 501 self.policyLoaded = [[CKKSCondition alloc] init]; 502 503 self.startCKOperationAtViewCreation = NO; 504 } 505 506 #pragma mark - View Handling 507 508 - (void)setView: (CKKSKeychainView*) obj { 509 CKKSKeychainView* kcv = nil; 510 511 @synchronized(self.views) { 512 kcv = self.views[obj.zoneName]; 513 self.views[obj.zoneName] = obj; 514 } 515 516 if(kcv) { 517 [kcv cancelAllOperations]; 518 } 519 } 520 521 - (void)clearAllViews { 522 NSArray<CKKSKeychainView*>* tempviews = nil; 523 @synchronized(self.views) { 524 tempviews = [self.views.allValues copy]; 525 [self.views removeAllObjects]; 526 527 self.startCKOperationAtViewCreation = NO; 528 } 529 530 for(CKKSKeychainView* view in tempviews) { 531 [view cancelAllOperations]; 532 } 533 } 534 535 - (void)clearView:(NSString*) viewName { 536 CKKSKeychainView* kcv = nil; 537 @synchronized(self.views) { 538 kcv = self.views[viewName]; 539 self.views[viewName] = nil; 540 } 541 542 if(kcv) { 543 [kcv cancelAllOperations]; 544 } 545 } 546 547 - (CKKSKeychainView*)findView:(NSString*)viewName { 548 if(!viewName) { 549 return nil; 550 } 551 552 @synchronized(self.views) { 553 return self.views[viewName]; 554 } 555 } 556 557 - (CKKSKeychainView* _Nullable)findView:(NSString*)viewName error:(NSError**)error 558 { 559 if([self.policyLoaded wait:5*NSEC_PER_SEC] != 0) { 560 ckkserror_global("ckks", "Haven't yet received a syncing policy; expect failure finding views"); 561 562 if([self useCKKSViewsFromPolicy]) { 563 if(error) { 564 *error = [NSError errorWithDomain:CKKSErrorDomain 565 code:CKKSErrorPolicyNotLoaded 566 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"CKKS syncing policy not yet loaded; cannot find view '%@'", viewName]}]; 567 568 } 569 return nil; 570 } 571 } 572 573 @synchronized(self.views) { 574 CKKSKeychainView* view = self.views[viewName]; 575 if(!view) { 576 if(error) { 577 *error = [NSError errorWithDomain:CKKSErrorDomain 578 code:CKKSNoSuchView 579 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"No syncing view for '%@'", viewName]}]; 580 } 581 return nil; 582 } 583 584 return view; 585 } 586 } 587 588 - (CKKSKeychainView*)findOrCreateView:(NSString*)viewName { 589 @synchronized(self.views) { 590 CKKSKeychainView* kcv = self.views[viewName]; 591 if(kcv) { 592 return kcv; 593 } 594 595 self.views[viewName] = [[CKKSKeychainView alloc] initWithContainer: self.container 596 zoneName: viewName 597 accountTracker: self.accountTracker 598 lockStateTracker: self.lockStateTracker 599 reachabilityTracker: self.reachabilityTracker 600 changeFetcher:self.zoneChangeFetcher 601 zoneModifier:self.zoneModifier 602 savedTLKNotifier: self.savedTLKNotifier 603 cloudKitClassDependencies:self.cloudKitClassDependencies]; 604 605 if(self.startCKOperationAtViewCreation) { 606 [self.views[viewName] beginCloudKitOperation]; 607 } 608 return self.views[viewName]; 609 } 610 } 611 612 - (NSSet<CKKSKeychainView*>*)currentViews 613 { 614 @synchronized (self.views) { 615 NSMutableSet<CKKSKeychainView*>* viewObjects = [NSMutableSet set]; 616 for(NSString* viewName in self.views) { 617 [viewObjects addObject:self.views[viewName]]; 618 } 619 return viewObjects; 620 } 621 } 622 623 - (void)createViews 624 { 625 if(![self useCKKSViewsFromPolicy]) { 626 // In the future, the CKKSViewManager needs to persist its policy property through daemon restarts 627 // and load it here, before creating whatever views it was told to (in a previous daemon lifetime) 628 for (NSString* viewName in [self defaultViewList]) { 629 CKKSKeychainView* view = [self findOrCreateView:viewName]; 630 (void)view; 631 } 632 } else { 633 ckksnotice_global("ckks-views", "Not loading default view list due to enabled CKKS4All"); 634 } 635 } 636 637 - (void)beginCloudKitOperationOfAllViews 638 { 639 self.startCKOperationAtViewCreation = YES; 640 641 for (CKKSKeychainView* view in self.views.allValues) { 642 [view beginCloudKitOperation]; 643 } 644 } 645 646 - (void)haltZone:(NSString*)viewName 647 { 648 @synchronized(self.views) { 649 CKKSKeychainView* view = self.views[viewName]; 650 [view halt]; 651 [view cancelAllOperations]; 652 self.views[viewName] = nil; 653 } 654 } 655 656 - (CKKSKeychainView*)restartZone:(NSString*)viewName { 657 [self haltZone:viewName]; 658 CKKSKeychainView* view = [self findOrCreateView: viewName]; 659 660 [view setCurrentSyncingPolicy:self.policy policyIsFresh:NO]; 661 662 return view; 663 } 664 665 - (NSString*)viewNameForViewHint: (NSString*) viewHint { 666 // For now, choose view based on viewhints. 667 if(viewHint && ![viewHint isEqual: [NSNull null]]) { 668 return viewHint; 669 } 670 671 // If there isn't a provided view hint, use the "keychain" view if we're testing. Otherwise, nil. 672 if(SecCKKSTestsEnabled()) { 673 return @"keychain"; 674 } else { 675 return nil; 676 } 677 } 678 679 - (void) setOverrideCKKSViewsFromPolicy:(BOOL)value { 680 _overrideCKKSViewsFromPolicy = YES; 681 _valueCKKSViewsFromPolicy = value; 682 } 683 684 - (BOOL)useCKKSViewsFromPolicy { 685 if (self.overrideCKKSViewsFromPolicy) { 686 return self.valueCKKSViewsFromPolicy; 687 } else { 688 BOOL viewsFromPolicy = os_feature_enabled(Security, CKKSViewsFromPolicy); 689 690 static dispatch_once_t onceToken; 691 dispatch_once(&onceToken, ^{ 692 ckksnotice_global("ckks", "ViewsFromPolicy feature flag: %@", viewsFromPolicy ? @"on" : @"off"); 693 }); 694 return viewsFromPolicy; 695 } 696 } 697 698 - (NSString* _Nullable)viewNameForItem:(SecDbItemRef)item 699 { 700 if ([self useCKKSViewsFromPolicy]) { 701 CFErrorRef cferror = NULL; 702 NSMutableDictionary *dict = (__bridge_transfer NSMutableDictionary*) SecDbItemCopyPListWithMask(item, kSecDbSyncFlag, &cferror); 703 704 if(cferror) { 705 ckkserror_global("ckks", "Couldn't fetch attributes from item: %@", cferror); 706 CFReleaseNull(cferror); 707 return nil; 708 } 709 710 // Ensure that we've added the class name, because SecDbItemCopyPListWithMask doesn't do that for some reason. 711 dict[(__bridge NSString*)kSecClass] = (__bridge NSString*)item->class->name; 712 713 NSString* view = [self.policy mapDictionaryToView:dict]; 714 if (view == nil) { 715 ckkserror_global("ckks", "No view returned from policy (%@): %@", self.policy, item); 716 return nil; 717 } 718 719 return view; 720 } else { 721 CFErrorRef cferror = NULL; 722 NSString* viewHint = (__bridge NSString*) SecDbItemGetValue(item, &v7vwht, &cferror); 723 724 if(cferror) { 725 ckkserror_global("ckks", "Couldn't fetch the viewhint for some reason: %@", cferror); 726 CFReleaseNull(cferror); 727 viewHint = nil; 728 } 729 730 return [self viewNameForViewHint: viewHint]; 731 } 732 } 733 734 - (void)registerSyncStatusCallback: (NSString*) uuid callback: (SecBoolNSErrorCallback) callback { 735 // Someone is requesting future notification of this item. 736 @synchronized(self.pendingSyncCallbacks) { 737 ckksnotice_global("ckkscallback", "registered callback for UUID: %@", uuid); 738 self.pendingSyncCallbacks[uuid] = callback; 739 } 740 } 741 742 - (SecBoolNSErrorCallback _Nullable)claimCallbackForUUID:(NSString* _Nullable)uuid 743 { 744 if(!uuid) { 745 return nil; 746 } 747 748 @synchronized(self.pendingSyncCallbacks) { 749 SecBoolNSErrorCallback callback = self.pendingSyncCallbacks[uuid]; 750 751 if(callback) { 752 ckksnotice_global("ckkscallback", "fetched UUID: %@", uuid); 753 } 754 755 self.pendingSyncCallbacks[uuid] = nil; 756 return callback; 757 } 758 } 759 760 - (NSSet<NSString*>*)pendingCallbackUUIDs 761 { 762 @synchronized(self.pendingSyncCallbacks) { 763 return [[self.pendingSyncCallbacks allKeys] copy]; 764 } 765 } 766 767 - (void)cloudkitAccountStateChange:(CKAccountInfo* _Nullable)oldAccountInfo to:(CKAccountInfo*)currentAccountInfo 768 { 769 if(currentAccountInfo.accountStatus == CKAccountStatusAvailable && currentAccountInfo.hasValidCredentials) { 770 // Account is okay! 771 } else { 772 @synchronized(self.pendingSyncCallbacks) { 773 if(self.pendingSyncCallbacks.count > 0) { 774 ckksnotice_global("ckkscallback", "No CK account; failing all pending sync callbacks"); 775 776 for(NSString* uuid in [self.pendingSyncCallbacks allKeys]) { 777 [CKKSViewManager callSyncCallbackWithErrorNoAccount:self.pendingSyncCallbacks[uuid]]; 778 } 779 780 [self.pendingSyncCallbacks removeAllObjects]; 781 } 782 } 783 } 784 } 785 786 + (void)callSyncCallbackWithErrorNoAccount:(SecBoolNSErrorCallback)syncCallback 787 { 788 // I don't love using this domain, but PCS depends on it 789 syncCallback(false, [NSError errorWithDomain:@"securityd" 790 code:errSecNotLoggedIn 791 description:@"No iCloud account available; item is not expected to sync"]); 792 } 793 794 - (void) handleKeychainEventDbConnection: (SecDbConnectionRef) dbconn source:(SecDbTransactionSource)txionSource added: (SecDbItemRef) added deleted: (SecDbItemRef) deleted { 795 796 SecDbItemRef modified = added ? added : deleted; 797 798 NSString* keyViewName = [CKKSKey isItemKeyForKeychainView: modified]; 799 800 if(keyViewName) { 801 // This might be some key material for this view! Poke it. 802 CKKSKeychainView* view = [self findView: keyViewName]; 803 804 if(!SecCKKSTestDisableKeyNotifications()) { 805 ckksnotice("ckks", view, "Potential new key material from %@ (source %lu)", 806 keyViewName, (unsigned long)txionSource); 807 [view keyStateMachineRequestProcess]; 808 } else { 809 ckksnotice("ckks", view, "Ignoring potential new key material from %@ (source %lu)", 810 keyViewName, (unsigned long)txionSource); 811 } 812 return; 813 } 814 815 bool addedSync = added && SecDbItemIsSyncable(added); 816 bool deletedSync = deleted && SecDbItemIsSyncable(deleted); 817 818 if(!addedSync && !deletedSync) { 819 // Local-only change. Skip with prejudice. 820 ckksinfo_global("ckks", "skipping sync of non-sync item (%d, %d)", addedSync, deletedSync); 821 return; 822 } 823 824 NSString* viewName = nil; 825 826 @synchronized(self.views) { 827 if([self useCKKSViewsFromPolicy] && !self.policy) { 828 ckkserror_global("ckks", "No policy configured(%@). Skipping item: %@", self.policy, modified); 829 self.itemModificationsBeforePolicyLoaded = YES; 830 831 return; 832 } 833 } 834 835 viewName = [self viewNameForItem:modified]; 836 837 if(!viewName) { 838 ckksnotice_global("ckks", "No intended CKKS view for item; skipping: %@", modified); 839 return; 840 } 841 842 // Looks like a normal item. Proceed! 843 CKKSKeychainView* view = [self findView:viewName]; 844 845 if(!view) { 846 ckksnotice_global("ckks", "No CKKS view for %@, skipping: %@", viewName, modified); 847 848 NSString* uuid = (__bridge NSString*) SecDbItemGetValue(modified, &v10itemuuid, NULL); 849 SecBoolNSErrorCallback syncCallback = [self claimCallbackForUUID:uuid]; 850 851 if(syncCallback) { 852 syncCallback(false, [NSError errorWithDomain:CKKSErrorDomain 853 code:CKKSNoSuchView 854 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"No syncing view for '%@'", viewName]}]); 855 } 856 return; 857 } 858 859 ckksnotice("ckks", view, "Routing item to zone %@: %@", viewName, modified); 860 [view handleKeychainEventDbConnection:dbconn 861 source:txionSource 862 added:added 863 deleted:deleted 864 rateLimiter:self.globalRateLimiter]; 865 } 866 867 -(void)setCurrentItemForAccessGroup:(NSData* _Nonnull)newItemPersistentRef 868 hash:(NSData*)newItemSHA1 869 accessGroup:(NSString*)accessGroup 870 identifier:(NSString*)identifier 871 viewHint:(NSString*)viewHint 872 replacing:(NSData* _Nullable)oldCurrentItemPersistentRef 873 hash:(NSData*)oldItemSHA1 874 complete:(void (^) (NSError* operror)) complete 875 { 876 NSError* viewError = nil; 877 CKKSKeychainView* view = [self findView:viewHint error:&viewError]; 878 879 if(!view) { 880 ckksnotice_global("ckks", "No CKKS view for %@, skipping setcurrent request: %@", viewHint, viewError); 881 complete(viewError); 882 return; 883 } 884 885 [view setCurrentItemForAccessGroup:newItemPersistentRef 886 hash:newItemSHA1 887 accessGroup:accessGroup 888 identifier:identifier 889 replacing:oldCurrentItemPersistentRef 890 hash:oldItemSHA1 891 complete:complete]; 892 } 893 894 -(void)getCurrentItemForAccessGroup:(NSString*)accessGroup 895 identifier:(NSString*)identifier 896 viewHint:(NSString*)viewHint 897 fetchCloudValue:(bool)fetchCloudValue 898 complete:(void (^) (NSString* uuid, NSError* operror)) complete 899 { 900 NSError* viewError = nil; 901 CKKSKeychainView* view = [self findView:viewHint error:&viewError]; 902 if(!view) { 903 ckksnotice_global("ckks", "No CKKS view for %@, skipping current fetch request: %@", viewHint, viewError); 904 complete(NULL, viewError); 905 return; 906 } 907 908 [view getCurrentItemForAccessGroup:accessGroup 909 identifier:identifier 910 fetchCloudValue:fetchCloudValue 911 complete:complete]; 912 } 913 914 + (instancetype)manager 915 { 916 return [OTManager manager].viewManager; 917 } 918 919 - (void)cancelPendingOperations { 920 [self.savedTLKNotifier cancel]; 921 } 922 923 -(void)notifyNewTLKsInKeychain { 924 // Why two functions here? Limitation of OCMock, unfortunately: can't stub and expect the same method 925 ckksnotice_global("ckksbackup", "New TLKs have arrived"); 926 [self syncBackupAndNotifyAboutSync]; 927 } 928 929 - (void)syncBackupAndNotifyAboutSync { 930 SOSAccount* account = (__bridge SOSAccount*)SOSKeychainAccountGetSharedAccount(); 931 932 if(!account) { 933 ckksnotice_global("ckks", "Failed to get account object"); 934 return; 935 } 936 937 [account performTransaction:^(SOSAccountTransaction * _Nonnull txn) { 938 CFErrorRef error = NULL; 939 NSSet* ignore = CFBridgingRelease(SOSAccountCopyBackupPeersAndForceSync(txn, &error)); 940 (void)ignore; 941 942 if(error) { 943 ckkserror_global("backup", "Couldn't process sync with backup peers: %@", error); 944 } else { 945 ckksnotice_global("ckksbackup", "telling CloudServices about TLK arrival"); 946 notify_post(kSecItemBackupNotification); 947 }; 948 }]; 949 } 950 951 952 953 - (NSArray<CKKSKeychainBackedKey*>* _Nullable)currentTLKsFilteredByPolicy:(BOOL)restrictToPolicy error:(NSError**)error 954 { 955 NSError* localError = nil; 956 NSArray<CKKSKeychainView*>* actualViews = [self views:nil operation:@"current TLKs" error:&localError]; 957 if(localError) { 958 ckkserror_global("ckks", "Error getting views: %@", localError); 959 if(error) { 960 *error = localError; 961 } 962 return nil; 963 } 964 965 NSMutableArray<CKKSResultOperation<CKKSKeySetProviderOperationProtocol>*>* keyFetchOperations = [NSMutableArray array]; 966 for (CKKSKeychainView* view in actualViews) { 967 if(restrictToPolicy && [self useCKKSViewsFromPolicy] && ![self.policy.viewsToPiggybackTLKs containsObject:view.zoneName]) { 968 continue; 969 } 970 971 CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* op = [view findKeySet:NO]; 972 [op timeout:10*NSEC_PER_SEC]; 973 [keyFetchOperations addObject:op]; 974 } 975 976 NSMutableArray<CKKSKeychainBackedKey*>* tlks = [NSMutableArray array]; 977 978 for(CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* op in keyFetchOperations) { 979 [op waitUntilFinished]; 980 981 if(op.error) { 982 ckkserror_global("ckks", "Error getting keyset: %@", op.error); 983 if(error) { 984 *error = op.error; 985 } 986 } else { 987 if(op.keyset.tlk) { 988 // Keys provided by this function must have the key material loaded 989 NSError* loadError = nil; 990 [op.keyset.tlk ensureKeyLoaded:&loadError]; 991 if(loadError) { 992 ckkserror_global("ckks", "Error loading key: %@", loadError); 993 if(error) { 994 *error = loadError; 995 } 996 } else { 997 [tlks addObject:[op.keyset.tlk.keycore copy]]; 998 } 999 } else { 1000 ckkserror_global("ckks", "Do not have TLK: %@", op.keyset); 1001 } 1002 } 1003 } 1004 1005 return tlks; 1006 } 1007 1008 #pragma mark - RPCs to manage and report state 1009 1010 - (void)performanceCounters:(void(^)(NSDictionary <NSString *, NSNumber *> *counter))reply { 1011 reply(@{}); 1012 } 1013 1014 - (NSArray<CKKSKeychainView*>*)views:(NSString*)viewName operation:(NSString*)opName error:(NSError**)error 1015 { 1016 return [self views:viewName operation:opName errorOnPolicyMissing:YES error:error]; 1017 } 1018 1019 - (NSArray<CKKSKeychainView*>*)views:(NSString*)viewName 1020 operation:(NSString*)opName 1021 errorOnPolicyMissing:(BOOL)errorOnPolicyMissing 1022 error:(NSError**)error 1023 { 1024 NSArray* actualViews = nil; 1025 1026 // Ensure we've actually set up, but don't wait too long. Clients get impatient. 1027 if([self.completedSecCKKSInitialize wait:5*NSEC_PER_SEC]) { 1028 ckkserror_global("ckks", "Haven't yet initialized zones; expect failure fetching views"); 1029 } 1030 1031 // If the caller doesn't mind if the policy is missing, wait some, but not the full 5s 1032 BOOL policyLoaded = [self.policyLoaded wait:(errorOnPolicyMissing ? 5 : 0.5)*NSEC_PER_SEC] == 0; 1033 if(!policyLoaded) { 1034 ckkserror_global("ckks", "Haven't yet received a policy; expect failure fetching views"); 1035 } 1036 1037 if(viewName) { 1038 CKKSKeychainView* view = errorOnPolicyMissing ? [self findView:viewName error:error] : [self findView:viewName]; 1039 ckksnotice_global("ckks", "Received a %@ request for zone %@ (%@)", opName, viewName, view); 1040 1041 if(!view) { 1042 return nil; 1043 } 1044 1045 actualViews = @[view]; 1046 1047 } else { 1048 if(!policyLoaded && [self useCKKSViewsFromPolicy] && errorOnPolicyMissing) { 1049 if(error) { 1050 if(error) { 1051 *error = [NSError errorWithDomain:CKKSErrorDomain 1052 code:CKKSErrorPolicyNotLoaded 1053 userInfo:@{NSLocalizedDescriptionKey: @"CKKS syncing policy not yet loaded; cannot list all views"}]; 1054 } 1055 } 1056 return nil; 1057 } 1058 1059 @synchronized(self.views) { 1060 actualViews = [self.views.allValues copy]; 1061 ckksnotice_global("ckks", "Received a %@ request for all zones: %@", opName, actualViews); 1062 } 1063 } 1064 actualViews = [actualViews sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"zoneName" ascending:YES]]]; 1065 return actualViews; 1066 } 1067 1068 - (void)rpcResetLocal:(NSString*)viewName reply: (void(^)(NSError* result)) reply { 1069 NSError* localError = nil; 1070 NSArray* actualViews = [self views:viewName operation:@"local reset" error:&localError]; 1071 if(localError) { 1072 ckkserror_global("ckks", "Error getting view %@: %@", viewName, localError); 1073 reply(localError); 1074 return; 1075 } 1076 1077 CKKSResultOperation* op = [CKKSResultOperation named:@"local-reset-zones-waiter" withBlockTakingSelf:^(CKKSResultOperation * _Nonnull strongOp) { 1078 if(!strongOp.error) { 1079 ckksnotice_global("ckksreset", "Completed rpcResetLocal"); 1080 } else { 1081 ckksnotice_global("ckks", "Completed rpcResetLocal with error: %@", strongOp.error); 1082 } 1083 reply(CKXPCSuitableError(strongOp.error)); 1084 }]; 1085 1086 for(CKKSKeychainView* view in actualViews) { 1087 ckksnotice("ckksreset", view, "Beginning local reset for %@", view); 1088 [op addSuccessDependency:[view resetLocalData]]; 1089 } 1090 1091 [op timeout:120*NSEC_PER_SEC]; 1092 [self.operationQueue addOperation: op]; 1093 } 1094 1095 - (void)rpcResetCloudKit:(NSString*)viewName reason:(NSString *)reason reply:(void(^)(NSError* result)) reply { 1096 NSError* localError = nil; 1097 NSArray* actualViews = [self views:viewName operation:@"CloudKit reset" error:&localError]; 1098 if(localError) { 1099 ckkserror_global("ckks", "Error getting view %@: %@", viewName, localError); 1100 reply(localError); 1101 return; 1102 } 1103 1104 CKKSResultOperation* op = [CKKSResultOperation named:@"cloudkit-reset-zones-waiter" withBlock:^() {}]; 1105 1106 // Use the completion block instead of the operation block, so that it runs even if the cancel fires 1107 __weak __typeof(op) weakOp = op; 1108 [op setCompletionBlock:^{ 1109 __strong __typeof(op) strongOp = weakOp; 1110 if(!strongOp.error) { 1111 ckksnotice_global("ckksreset", "Completed rpcResetCloudKit"); 1112 } else { 1113 ckksnotice_global("ckksreset", "Completed rpcResetCloudKit with error: %@", strongOp.error); 1114 } 1115 reply(CKXPCSuitableError(strongOp.error)); 1116 }]; 1117 1118 for(CKKSKeychainView* view in actualViews) { 1119 NSString *operationGroupName = [NSString stringWithFormat:@"api-reset-%@", reason]; 1120 ckksnotice("ckksreset", view, "Beginning CloudKit reset for %@: %@", view, reason); 1121 [op addSuccessDependency:[view resetCloudKitZone:[CKOperationGroup CKKSGroupWithName:operationGroupName]]]; 1122 } 1123 1124 [op timeout:120*NSEC_PER_SEC]; 1125 [self.operationQueue addOperation: op]; 1126 } 1127 1128 - (void)rpcResync:(NSString*)viewName reply: (void(^)(NSError* result)) reply { 1129 NSError* localError = nil; 1130 NSArray* actualViews = [self views:viewName operation:@"CloudKit resync" error:&localError]; 1131 if(localError) { 1132 ckkserror_global("ckks", "Error getting view %@: %@", viewName, localError); 1133 reply(localError); 1134 return; 1135 } 1136 1137 CKKSResultOperation* op = [[CKKSResultOperation alloc] init]; 1138 op.name = @"rpc-resync-cloudkit"; 1139 __weak __typeof(op) weakOp = op; 1140 1141 // Use the completion block instead of the operation block, so that it runs even if the cancel fires 1142 [op setCompletionBlock:^{ 1143 __strong __typeof(op) strongOp = weakOp; 1144 ckksnotice_global("ckks", "Ending rsync-CloudKit rpc with %@", strongOp.error); 1145 reply(CKXPCSuitableError(strongOp.error)); 1146 }]; 1147 1148 for(CKKSKeychainView* view in actualViews) { 1149 ckksnotice("ckksresync", view, "Beginning resync (CloudKit) for %@", view); 1150 1151 CKKSSynchronizeOperation* resyncOp = [view resyncWithCloud]; 1152 [op addSuccessDependency:resyncOp]; 1153 } 1154 1155 [op timeout:120*NSEC_PER_SEC]; 1156 [self.operationQueue addOperation:op]; 1157 } 1158 1159 - (void)rpcResyncLocal:(NSString*)viewName reply:(void(^)(NSError* result))reply { 1160 NSError* localError = nil; 1161 NSArray* actualViews = [self views:viewName operation:@"local resync" error:&localError]; 1162 if(localError) { 1163 ckkserror_global("ckks", "Error getting view %@: %@", viewName, localError); 1164 reply(localError); 1165 return; 1166 } 1167 1168 CKKSResultOperation* op = [[CKKSResultOperation alloc] init]; 1169 op.name = @"rpc-resync-local"; 1170 __weak __typeof(op) weakOp = op; 1171 // Use the completion block instead of the operation block, so that it runs even if the cancel fires 1172 [op setCompletionBlock:^{ 1173 __strong __typeof(op) strongOp = weakOp; 1174 ckksnotice_global("ckks", "Ending rsync-local rpc with %@", strongOp.error); 1175 reply(CKXPCSuitableError(strongOp.error)); 1176 }]; 1177 1178 for(CKKSKeychainView* view in actualViews) { 1179 ckksnotice("ckksresync", view, "Beginning resync (local) for %@", view); 1180 1181 CKKSLocalSynchronizeOperation* resyncOp = [view resyncLocal]; 1182 [op addSuccessDependency:resyncOp]; 1183 } 1184 1185 [op timeout:120*NSEC_PER_SEC]; 1186 } 1187 1188 - (void)rpcStatus: (NSString*)viewName 1189 fast:(bool)fast 1190 reply:(void(^)(NSArray<NSDictionary*>* result, NSError* error)) reply 1191 { 1192 NSMutableArray* a = [[NSMutableArray alloc] init]; 1193 1194 // Now, query the views about their status. Don't wait for the policy to be loaded 1195 NSError* error = nil; 1196 NSArray* actualViews = [self views:viewName operation:@"status" errorOnPolicyMissing:NO error:&error]; 1197 if(!actualViews || error) { 1198 reply(nil, error); 1199 return; 1200 } 1201 1202 WEAKIFY(self); 1203 CKKSResultOperation* statusOp = [CKKSResultOperation named:@"status-rpc" withBlock:^{ 1204 STRONGIFY(self); 1205 1206 if (fast == false) { 1207 // Get account state, even wait for it a little 1208 [self.accountTracker.ckdeviceIDInitialized wait:1*NSEC_PER_SEC]; 1209 NSString *deviceID = self.accountTracker.ckdeviceID; 1210 NSError *deviceIDError = self.accountTracker.ckdeviceIDError; 1211 NSDate *lastCKKSPush = [[CKKSAnalytics logger] datePropertyForKey:CKKSAnalyticsLastCKKSPush]; 1212 1213 #define stringify(obj) CKKSNilToNSNull([obj description]) 1214 NSDictionary* global = @{ 1215 @"view": @"global", 1216 @"reachability": self.reachabilityTracker.currentReachability ? @"network" : @"no-network", 1217 @"ckdeviceID": CKKSNilToNSNull(deviceID), 1218 @"ckdeviceIDError": CKKSNilToNSNull(deviceIDError), 1219 @"lockstatetracker": stringify(self.lockStateTracker), 1220 @"cloudkitRetryAfter": stringify(self.zoneModifier.cloudkitRetryAfter), 1221 @"lastCKKSPush": CKKSNilToNSNull(lastCKKSPush), 1222 @"policy": stringify(self.policy), 1223 @"viewsFromPolicy": [self useCKKSViewsFromPolicy] ? @"yes" : @"no", 1224 }; 1225 [a addObject: global]; 1226 } 1227 1228 for(CKKSKeychainView* view in actualViews) { 1229 NSDictionary* status = nil; 1230 ckksnotice("ckks", view, "Fetching status for %@", view.zoneName); 1231 if (fast) { 1232 status = [view fastStatus]; 1233 } else { 1234 status = [view status]; 1235 } 1236 ckksinfo("ckks", view, "Status is %@", status); 1237 if(status) { 1238 [a addObject: status]; 1239 } 1240 } 1241 reply(a, nil); 1242 }]; 1243 1244 // 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) 1245 if([self.accountTracker.ckAccountInfoInitialized wait:2*NSEC_PER_SEC]) { 1246 ckkserror_global("account", "Haven't yet figured out cloudkit account state"); 1247 } 1248 1249 if(self.accountTracker.currentCKAccountInfo.accountStatus == CKAccountStatusAvailable) { 1250 if (![self waitForTrustReady]) { 1251 ckkserror_global("trust", "Haven't yet figured out trust status"); 1252 } 1253 1254 for(CKKSKeychainView* view in actualViews) { 1255 OctagonStateMultiStateArrivalWatcher* waitForTransient = [[OctagonStateMultiStateArrivalWatcher alloc] initNamed:@"rpc-watcher" 1256 serialQueue:view.queue 1257 states:CKKSKeyStateNonTransientStates()]; 1258 [waitForTransient timeout:2*NSEC_PER_SEC]; 1259 [view.stateMachine registerMultiStateArrivalWatcher:waitForTransient]; 1260 1261 [statusOp addDependency:waitForTransient.result]; 1262 } 1263 } 1264 [self.operationQueue addOperation:statusOp]; 1265 1266 return; 1267 } 1268 1269 - (void)rpcStatus:(NSString*)viewName reply:(void (^)(NSArray<NSDictionary*>* result, NSError* error))reply 1270 { 1271 [self rpcStatus:viewName fast:false reply:reply]; 1272 } 1273 1274 - (void)rpcFastStatus:(NSString*)viewName reply:(void (^)(NSArray<NSDictionary*>* result, NSError* error))reply 1275 { 1276 [self rpcStatus:viewName fast:true reply:reply]; 1277 } 1278 1279 - (void)rpcFetchAndProcessChanges:(NSString*)viewName reply: (void(^)(NSError* result))reply { 1280 [self rpcFetchAndProcessChanges:viewName classA:false reply: (void(^)(NSError* result))reply]; 1281 } 1282 1283 - (void)rpcFetchAndProcessClassAChanges:(NSString*)viewName reply: (void(^)(NSError* result))reply { 1284 [self rpcFetchAndProcessChanges:viewName classA:true reply:(void(^)(NSError* result))reply]; 1285 } 1286 1287 - (void)rpcFetchAndProcessChanges:(NSString*)viewName classA:(bool)classAError reply: (void(^)(NSError* result)) reply { 1288 NSError* error = nil; 1289 NSArray* actualViews = [self views:viewName operation:@"fetch" error:&error]; 1290 if(!actualViews || error) { 1291 reply(error); 1292 return; 1293 } 1294 1295 CKKSResultOperation* blockOp = [[CKKSResultOperation alloc] init]; 1296 blockOp.name = @"rpc-fetch-and-process-result"; 1297 __weak __typeof(blockOp) weakBlockOp = blockOp; 1298 // Use the completion block instead of the operation block, so that it runs even if the cancel fires 1299 [blockOp setCompletionBlock:^{ 1300 __strong __typeof(blockOp) strongBlockOp = weakBlockOp; 1301 [strongBlockOp allDependentsSuccessful]; 1302 reply(CKXPCSuitableError(strongBlockOp.error)); 1303 }]; 1304 1305 for(CKKSKeychainView* view in actualViews) { 1306 ckksnotice("ckks", view, "Beginning fetch for %@", view); 1307 1308 CKKSResultOperation* op = [view processIncomingQueue:classAError after:[view.zoneChangeFetcher requestSuccessfulFetch: CKKSFetchBecauseAPIFetchRequest]]; 1309 [blockOp addSuccessDependency:op]; 1310 } 1311 1312 [self.operationQueue addOperation: [blockOp timeout:(SecCKKSTestsEnabled() ? NSEC_PER_SEC * 5 : NSEC_PER_SEC * 120)]]; 1313 } 1314 1315 - (void)rpcPushOutgoingChanges:(NSString*)viewName reply: (void(^)(NSError* result))reply { 1316 NSError* error = nil; 1317 NSArray* actualViews = [self views:viewName operation:@"push" error:&error]; 1318 if(!actualViews || error) { 1319 reply(error); 1320 return; 1321 } 1322 1323 CKKSResultOperation* blockOp = [[CKKSResultOperation alloc] init]; 1324 blockOp.name = @"rpc-push"; 1325 __weak __typeof(blockOp) weakBlockOp = blockOp; 1326 // Use the completion block instead of the operation block, so that it runs even if the cancel fires 1327 [blockOp setCompletionBlock:^{ 1328 __strong __typeof(blockOp) strongBlockOp = weakBlockOp; 1329 [strongBlockOp allDependentsSuccessful]; 1330 reply(CKXPCSuitableError(strongBlockOp.error)); 1331 }]; 1332 1333 for(CKKSKeychainView* view in actualViews) { 1334 ckksnotice("ckks-rpc", view, "Beginning push for %@", view); 1335 1336 CKKSResultOperation* op = [view processOutgoingQueue: [CKOperationGroup CKKSGroupWithName:@"rpc-push"]]; 1337 [blockOp addSuccessDependency:op]; 1338 } 1339 1340 [self.operationQueue addOperation: [blockOp timeout:(SecCKKSTestsEnabled() ? NSEC_PER_SEC * 2 : NSEC_PER_SEC * 120)]]; 1341 } 1342 1343 - (void)rpcGetCKDeviceIDWithReply:(void (^)(NSString *))reply { 1344 reply(self.accountTracker.ckdeviceID); 1345 } 1346 1347 - (void)rpcCKMetric:(NSString *)eventName attributes:(NSDictionary *)attributes reply:(void (^)(NSError *))reply 1348 { 1349 if (eventName == NULL) { 1350 reply([NSError errorWithDomain:CKKSErrorDomain 1351 code:CKKSNoMetric 1352 description:@"No metric name"]); 1353 return; 1354 } 1355 SecEventMetric *metric = [[SecEventMetric alloc] initWithEventName:eventName]; 1356 [attributes enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull __unused stop) { 1357 metric[key] = obj; 1358 }]; 1359 [[SecMetrics managerObject] submitEvent:metric]; 1360 reply(NULL); 1361 } 1362 1363 -(void)xpc24HrNotification { 1364 // XPC has poked us and said we should do some cleanup! 1365 ckksnotice_global("ckks", "Received a 24hr notification from XPC"); 1366 1367 if (![self waitForTrustReady]) { 1368 ckksnotice_global("ckks", "Trust not ready, still going ahead"); 1369 } 1370 1371 [[CKKSAnalytics logger] dailyCoreAnalyticsMetrics:@"com.apple.security.CKKSHealthSummary"]; 1372 1373 // For now, poke the views and tell them to update their device states if they'd like 1374 NSArray* actualViews = nil; 1375 @synchronized(self.views) { 1376 // Can't safely iterate a mutable collection, so copy it. 1377 actualViews = self.views.allValues; 1378 } 1379 1380 CKOperationGroup* group = [CKOperationGroup CKKSGroupWithName:@"periodic-device-state-update"]; 1381 for(CKKSKeychainView* view in actualViews) { 1382 ckksnotice("ckks", view, "Starting device state XPC update"); 1383 // Let the update know it should rate-limit itself 1384 [view updateDeviceState:true waitForKeyHierarchyInitialization:30*NSEC_PER_SEC ckoperationGroup:group]; 1385 [view xpc24HrNotification]; 1386 } 1387 } 1388 1389 - (void)haltAll 1390 { 1391 @synchronized(self.views) { 1392 for(CKKSKeychainView* view in self.views.allValues) { 1393 [view halt]; 1394 } 1395 } 1396 1397 [self.zoneModifier halt]; 1398 } 1399 1400 #endif // OCTAGON 1401 @end