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 #include <AssertMacros.h>
26 #import <Foundation/Foundation.h>
29 #import "CloudKitDependencies.h"
30 #import "keychain/ckks/CKKSCKAccountStateTracker.h"
31 #import "keychain/ckks/CloudKitCategories.h"
32 #import <CloudKit/CloudKit.h>
33 #import <CloudKit/CloudKit_Private.h>
35 #import "CKKSKeychainView.h"
38 #include <utilities/debugging.h>
42 @property CKDatabaseOperation<CKKSModifyRecordZonesOperation>* zoneCreationOperation;
43 @property CKDatabaseOperation<CKKSModifyRecordZonesOperation>* zoneDeletionOperation;
44 @property CKDatabaseOperation<CKKSModifySubscriptionsOperation>* zoneSubscriptionOperation;
46 @property NSOperationQueue* operationQueue;
47 @property CKKSResultOperation* accountLoggedInDependency;
49 @property NSHashTable<NSOperation*>* accountOperations;
52 @property bool halted;
53 @property bool zoneCreateNetworkFailure;
54 @property bool zoneSubscriptionNetworkFailure;
57 @implementation CKKSZone
59 - (instancetype)initWithContainer: (CKContainer*) container
60 zoneName: (NSString*) zoneName
61 accountTracker:(CKKSCKAccountStateTracker*) accountTracker
62 reachabilityTracker:(CKKSReachabilityTracker *) reachabilityTracker
63 fetchRecordZoneChangesOperationClass: (Class<CKKSFetchRecordZoneChangesOperation>) fetchRecordZoneChangesOperationClass
64 fetchRecordsOperationClass: (Class<CKKSFetchRecordsOperation>)fetchRecordsOperationClass
65 queryOperationClass:(Class<CKKSQueryOperation>)queryOperationClass
66 modifySubscriptionsOperationClass: (Class<CKKSModifySubscriptionsOperation>) modifySubscriptionsOperationClass
67 modifyRecordZonesOperationClass: (Class<CKKSModifyRecordZonesOperation>) modifyRecordZonesOperationClass
68 apsConnectionClass: (Class<CKKSAPSConnection>) apsConnectionClass
70 if(self = [super init]) {
71 _container = container;
73 _accountTracker = accountTracker;
74 _reachabilityTracker = reachabilityTracker;
78 _database = [_container privateCloudDatabase];
79 _zone = [[CKRecordZone alloc] initWithZoneID: [[CKRecordZoneID alloc] initWithZoneName:zoneName ownerName:CKCurrentUserDefaultName]];
81 _accountStatus = CKKSAccountStatusUnknown;
83 _accountLoggedInDependency = [self createAccountLoggedInDependency:@"CloudKit account logged in."];
85 _accountOperations = [NSHashTable weakObjectsHashTable];
87 _fetchRecordZoneChangesOperationClass = fetchRecordZoneChangesOperationClass;
88 _fetchRecordsOperationClass = fetchRecordsOperationClass;
89 _queryOperationClass = queryOperationClass;
90 _modifySubscriptionsOperationClass = modifySubscriptionsOperationClass;
91 _modifyRecordZonesOperationClass = modifyRecordZonesOperationClass;
92 _apsConnectionClass = apsConnectionClass;
94 _queue = dispatch_queue_create([[NSString stringWithFormat:@"CKKSQueue.%@.zone.%@", container.containerIdentifier, zoneName] UTF8String], DISPATCH_QUEUE_SERIAL);
95 _operationQueue = [[NSOperationQueue alloc] init];
100 - (CKKSResultOperation*)createAccountLoggedInDependency:(NSString*)message {
101 __weak __typeof(self) weakSelf = self;
102 CKKSResultOperation* accountLoggedInDependency = [CKKSResultOperation named:@"account-logged-in-dependency" withBlock:^{
103 ckksnotice("ckkszone", weakSelf, "%@", message);
105 accountLoggedInDependency.descriptionErrorCode = CKKSResultDescriptionPendingAccountLoggedIn;
106 return accountLoggedInDependency;
109 - (void)initializeZone {
110 [self.accountTracker notifyOnAccountStatusChange:self];
114 self.zoneCreated = false;
115 self.zoneSubscribed = false;
116 self.zoneCreatedError = nil;
117 self.zoneSubscribedError = nil;
119 self.zoneCreationOperation = nil;
120 self.zoneSubscriptionOperation = nil;
121 self.zoneDeletionOperation = nil;
124 - (CKRecordZoneID*)zoneID {
125 return [self.zone zoneID];
129 -(void)ckAccountStatusChange:(CKKSAccountStatus)oldStatus to:(CKKSAccountStatus)currentStatus {
130 ckksnotice("ckkszone", self, "%@ Received notification of CloudKit account status change, moving from %@ to %@",
131 self.zoneID.zoneName,
132 [CKKSCKAccountStateTracker stringFromAccountStatus: oldStatus],
133 [CKKSCKAccountStateTracker stringFromAccountStatus: currentStatus]);
135 switch(currentStatus) {
136 case CKKSAccountStatusAvailable: {
137 ckksnotice("ckkszone", self, "Logged into iCloud.");
138 [self handleCKLogin];
140 if(self.accountLoggedInDependency) {
141 [self.operationQueue addOperation:self.accountLoggedInDependency];
142 self.accountLoggedInDependency = nil;
147 case CKKSAccountStatusNoAccount: {
148 ckksnotice("ckkszone", self, "Logging out of iCloud. Shutting down.");
150 self.accountLoggedInDependency = [self createAccountLoggedInDependency:@"CloudKit account logged in again."];
152 [self handleCKLogout];
156 case CKKSAccountStatusUnknown: {
157 // We really don't expect to receive this as a notification, but, okay!
158 ckksnotice("ckkszone", self, "Account status has become undetermined. Pausing for %@", self.zoneID.zoneName);
160 self.accountLoggedInDependency = [self createAccountLoggedInDependency:@"CloudKit account return from 'unknown'."];
162 [self handleCKLogout];
168 - (CKKSResultOperation*)handleCKLogin:(bool)zoneCreated zoneSubscribed:(bool)zoneSubscribed {
169 if(!SecCKKSIsEnabled()) {
170 ckksinfo("ckkszone", self, "Skipping CloudKit registration due to disabled CKKS");
174 // If we've already started set up and that hasn't finished, complain
175 if([self.zoneSetupOperation isPending] || [self.zoneSetupOperation isExecuting]) {
176 ckksnotice("ckkszone", self, "Asked to handleCKLogin, but zoneSetupOperation appears to not be complete? %@ Continuing anyway", self.zoneSetupOperation);
179 self.zoneSetupOperation = [[CKKSGroupOperation alloc] init];
180 self.zoneSetupOperation.name = [NSString stringWithFormat:@"zone-setup-operation-%@", self.zoneName];
182 self.zoneCreated = zoneCreated;
183 self.zoneSubscribed = zoneSubscribed;
185 // Zone setups and teardowns are due to either 1) first CKKS launch or 2) the user logging in to iCloud.
186 // Therefore, they're QoS UserInitiated.
187 self.zoneSetupOperation.queuePriority = NSOperationQueuePriorityNormal;
188 self.zoneSetupOperation.qualityOfService = NSQualityOfServiceUserInitiated;
190 ckksnotice("ckkszone", self, "Setting up zone %@", self.zoneName);
192 __weak __typeof(self) weakSelf = self;
194 // First, check the account status. If it's sufficient, add the necessary CloudKit operations to this operation
195 __weak CKKSGroupOperation* weakZoneSetupOperation = self.zoneSetupOperation;
196 [self.zoneSetupOperation runBeforeGroupFinished:[CKKSResultOperation named:[NSString stringWithFormat:@"zone-setup-%@", self.zoneName] withBlock:^{
197 __strong __typeof(weakSelf) strongSelf = weakSelf;
198 __strong __typeof(self.zoneSetupOperation) zoneSetupOperation = weakZoneSetupOperation;
199 __strong __typeof(self.reachabilityTracker) reachabilityTracker = self.reachabilityTracker;
200 if(!strongSelf || !zoneSetupOperation) {
201 ckkserror("ckkszone", strongSelf, "received callback for released object");
205 if(strongSelf.accountStatus != CKKSAccountStatusAvailable) {
206 ckkserror("ckkszone", strongSelf, "Zone doesn't believe it's logged in; quitting setup");
210 NSBlockOperation* setupCompleteOperation = [NSBlockOperation blockOperationWithBlock:^{
211 __strong __typeof(weakSelf) strongSelf = weakSelf;
213 secerror("ckkszone: received callback for released object");
217 ckksnotice("ckkszone", strongSelf, "%@: Setup complete", strongSelf.zoneName);
219 setupCompleteOperation.name = @"zone-setup-complete-operation";
221 // We have an account, so fetch the push environment and bring up APS
222 [strongSelf.container serverPreferredPushEnvironmentWithCompletionHandler: ^(NSString *apsPushEnvString, NSError *error) {
223 __strong __typeof(weakSelf) strongSelf = weakSelf;
225 secerror("ckkszone: received callback for released object");
229 if(error || (apsPushEnvString == nil)) {
230 ckkserror("ckkszone", strongSelf, "Received error fetching preferred push environment (%@). Keychain syncing is highly degraded: %@", apsPushEnvString, error);
232 CKKSAPSReceiver* aps = [CKKSAPSReceiver receiverForEnvironment:apsPushEnvString
233 namedDelegatePort:SecCKKSAPSNamedPort
234 apsConnectionClass:strongSelf.apsConnectionClass];
235 [aps registerReceiver:strongSelf forZoneID:strongSelf.zoneID];
239 NSBlockOperation* modifyRecordZonesCompleteOperation = nil;
241 ckksnotice("ckkszone", strongSelf, "Creating CloudKit zone '%@'", strongSelf.zoneName);
242 CKDatabaseOperation<CKKSModifyRecordZonesOperation>* zoneCreationOperation = [[strongSelf.modifyRecordZonesOperationClass alloc] initWithRecordZonesToSave: @[strongSelf.zone] recordZoneIDsToDelete: nil];
243 zoneCreationOperation.queuePriority = NSOperationQueuePriorityNormal;
244 zoneCreationOperation.qualityOfService = NSQualityOfServiceUserInitiated;
245 zoneCreationOperation.database = strongSelf.database;
246 zoneCreationOperation.name = @"zone-creation-operation";
247 zoneCreationOperation.group = strongSelf.zoneSetupOperationGroup ?: [CKOperationGroup CKKSGroupWithName:@"zone-creation"];;
249 // Completion blocks don't count for dependencies. Use this intermediate operation hack instead.
250 modifyRecordZonesCompleteOperation = [[NSBlockOperation alloc] init];
251 modifyRecordZonesCompleteOperation.name = @"zone-creation-finished";
253 zoneCreationOperation.modifyRecordZonesCompletionBlock = ^(NSArray<CKRecordZone *> *savedRecordZones, NSArray<CKRecordZoneID *> *deletedRecordZoneIDs, NSError *operationError) {
254 __strong __typeof(weakSelf) strongSelf = weakSelf;
256 secerror("ckkszone: received callback for released object");
260 __strong __typeof(weakSelf) strongSubSelf = weakSelf;
262 if(!operationError) {
263 ckksnotice("ckkszone", strongSubSelf, "Successfully created zone %@", strongSubSelf.zoneName);
264 strongSubSelf.zoneCreated = true;
265 strongSubSelf.zoneSetupOperationGroup = nil;
267 ckkserror("ckkszone", strongSubSelf, "Couldn't create zone %@; %@", strongSubSelf.zoneName, operationError);
269 strongSubSelf.zoneCreatedError = operationError;
270 if ([reachabilityTracker isNetworkError:operationError]){
271 strongSelf.zoneCreateNetworkFailure = true;
273 [strongSubSelf.operationQueue addOperation: modifyRecordZonesCompleteOperation];
276 if (strongSelf.zoneCreateNetworkFailure) {
277 [zoneCreationOperation addNullableDependency:reachabilityTracker.reachablityDependency];
278 strongSelf.zoneCreateNetworkFailure = false;
280 ckksnotice("ckkszone", strongSelf, "Adding CKKSModifyRecordZonesOperation: %@ %@", zoneCreationOperation, zoneCreationOperation.dependencies);
281 strongSelf.zoneCreationOperation = zoneCreationOperation;
282 [setupCompleteOperation addDependency: modifyRecordZonesCompleteOperation];
283 [zoneSetupOperation runBeforeGroupFinished: zoneCreationOperation];
284 [zoneSetupOperation dependOnBeforeGroupFinished: modifyRecordZonesCompleteOperation];
286 ckksnotice("ckkszone", strongSelf, "no need to create the zone '%@'", strongSelf.zoneName);
289 if(!zoneSubscribed) {
290 ckksnotice("ckkszone", strongSelf, "Creating CloudKit record zone subscription for %@", strongSelf.zoneName);
291 CKRecordZoneSubscription* subscription = [[CKRecordZoneSubscription alloc] initWithZoneID: strongSelf.zoneID subscriptionID:[@"zone:" stringByAppendingString: strongSelf.zoneName]];
292 CKNotificationInfo* notificationInfo = [[CKNotificationInfo alloc] init];
294 notificationInfo.shouldSendContentAvailable = false;
295 subscription.notificationInfo = notificationInfo;
297 CKDatabaseOperation<CKKSModifySubscriptionsOperation>* zoneSubscriptionOperation = [[strongSelf.modifySubscriptionsOperationClass alloc] initWithSubscriptionsToSave: @[subscription] subscriptionIDsToDelete: nil];
299 zoneSubscriptionOperation.queuePriority = NSOperationQueuePriorityNormal;
300 zoneSubscriptionOperation.qualityOfService = NSQualityOfServiceUserInitiated;
301 zoneSubscriptionOperation.database = strongSelf.database;
302 zoneSubscriptionOperation.name = @"zone-subscription-operation";
304 // Completion blocks don't count for dependencies. Use this intermediate operation hack instead.
305 NSBlockOperation* zoneSubscriptionCompleteOperation = [[NSBlockOperation alloc] init];
306 zoneSubscriptionCompleteOperation.name = @"zone-subscription-complete";
307 zoneSubscriptionOperation.modifySubscriptionsCompletionBlock = ^(NSArray<CKSubscription *> * _Nullable savedSubscriptions, NSArray<NSString *> * _Nullable deletedSubscriptionIDs, NSError * _Nullable operationError) {
308 __strong __typeof(weakSelf) strongSubSelf = weakSelf;
310 ckkserror("ckkszone", strongSubSelf, "received callback for released object");
314 if(!operationError) {
315 ckksnotice("ckkszone", strongSubSelf, "Successfully subscribed to %@", savedSubscriptions);
317 // Success; write that down. TODO: actually ensure that the saved subscription matches what we asked for
318 for(CKSubscription* sub in savedSubscriptions) {
319 ckksnotice("ckkszone", strongSubSelf, "Successfully subscribed to %@", sub.subscriptionID);
320 strongSubSelf.zoneSubscribed = true;
323 ckkserror("ckkszone", strongSubSelf, "Couldn't create cloudkit zone subscription; keychain syncing is severely degraded: %@", operationError);
326 strongSubSelf.zoneSubscribedError = operationError;
327 strongSubSelf.zoneSubscriptionOperation = nil;
328 if ([reachabilityTracker isNetworkError:operationError]){
329 strongSelf.zoneSubscriptionNetworkFailure = true;
332 [strongSubSelf.operationQueue addOperation: zoneSubscriptionCompleteOperation];
335 if (strongSelf.zoneSubscriptionNetworkFailure) {
336 [zoneSubscriptionOperation addNullableDependency:reachabilityTracker.reachablityDependency];
337 strongSelf.zoneSubscriptionNetworkFailure = false;
339 [zoneSubscriptionOperation addNullableDependency:modifyRecordZonesCompleteOperation];
340 strongSelf.zoneSubscriptionOperation = zoneSubscriptionOperation;
341 [setupCompleteOperation addDependency: zoneSubscriptionCompleteOperation];
342 [zoneSetupOperation runBeforeGroupFinished:zoneSubscriptionOperation];
343 [zoneSetupOperation dependOnBeforeGroupFinished: zoneSubscriptionCompleteOperation];
345 ckksnotice("ckkszone", strongSelf, "no need to create database subscription");
348 [strongSelf.zoneSetupOperation runBeforeGroupFinished:setupCompleteOperation];
351 [self scheduleAccountStatusOperation:self.zoneSetupOperation];
352 return self.zoneSetupOperation;
356 - (CKKSResultOperation*)deleteCloudKitZoneOperation:(CKOperationGroup* _Nullable)ckoperationGroup {
357 if(!SecCKKSIsEnabled()) {
358 ckksnotice("ckkszone", self, "Skipping CloudKit reset due to disabled CKKS");
362 // We want to delete this zone and this subscription from CloudKit.
364 // Step 1: cancel setup operations (if they exist)
365 [self.accountLoggedInDependency cancel];
366 [self.zoneSetupOperation cancel];
367 [self.zoneCreationOperation cancel];
368 [self.zoneSubscriptionOperation cancel];
370 // Step 2: Try to delete the zone
372 CKDatabaseOperation<CKKSModifyRecordZonesOperation>* zoneDeletionOperation = [[self.modifyRecordZonesOperationClass alloc] initWithRecordZonesToSave: nil recordZoneIDsToDelete: @[self.zoneID]];
373 zoneDeletionOperation.queuePriority = NSOperationQueuePriorityNormal;
374 zoneDeletionOperation.qualityOfService = NSQualityOfServiceUserInitiated;
375 zoneDeletionOperation.database = self.database;
376 zoneDeletionOperation.group = ckoperationGroup;
378 CKKSGroupOperation* zoneDeletionGroupOperation = [[CKKSGroupOperation alloc] init];
379 zoneDeletionGroupOperation.name = [NSString stringWithFormat:@"cloudkit-zone-delete-%@", self.zoneName];
381 CKKSResultOperation* doneOp = [CKKSResultOperation named:@"zone-reset-watcher" withBlock:^{}];
382 [zoneDeletionGroupOperation dependOnBeforeGroupFinished:doneOp];
384 __weak __typeof(self) weakSelf = self;
386 zoneDeletionOperation.modifyRecordZonesCompletionBlock = ^(NSArray<CKRecordZone *> *savedRecordZones, NSArray<CKRecordZoneID *> *deletedRecordZoneIDs, NSError *operationError) {
387 __strong __typeof(weakSelf) strongSelf = weakSelf;
389 ckkserror("ckkszone", strongSelf, "received callback for released object");
393 bool fatalError = false;
395 // Okay, but if this error is either 'ZoneNotFound' or 'UserDeletedZone', that's fine by us: the zone is deleted.
396 NSDictionary* partialErrors = operationError.userInfo[CKPartialErrorsByItemIDKey];
397 if([operationError.domain isEqualToString:CKErrorDomain] && operationError.code == CKErrorPartialFailure && partialErrors) {
398 for(CKRecordZoneID* errorZoneID in partialErrors.allKeys) {
399 NSError* errorZone = partialErrors[errorZoneID];
401 if(errorZone && [errorZone.domain isEqualToString:CKErrorDomain] &&
402 (errorZone.code == CKErrorZoneNotFound || errorZone.code == CKErrorUserDeletedZone)) {
403 ckksnotice("ckkszone", strongSelf, "Attempted to delete zone %@, but it's already missing. This is okay: %@", errorZoneID, errorZone);
415 ckksnotice("ckkszone", strongSelf, "deletion of record zones %@ completed with error: %@", deletedRecordZoneIDs, operationError);
417 ckksnotice("ckkszone", strongSelf, "deletion of record zones %@ completed successfully", deletedRecordZoneIDs);
420 if(operationError && fatalError) {
421 // If the error wasn't actually a problem, don't report it upward.
422 doneOp.error = operationError;
424 [zoneDeletionGroupOperation runBeforeGroupFinished:doneOp];
427 // If the zone creation operation is still pending, wait for it to complete before attempting zone deletion
428 [zoneDeletionOperation addNullableDependency: self.zoneCreationOperation];
429 [zoneDeletionGroupOperation runBeforeGroupFinished:zoneDeletionOperation];
431 [zoneDeletionGroupOperation runBeforeGroupFinished:[CKKSResultOperation named:@"print-log-message" withBlock:^{
432 __strong __typeof(weakSelf) strongSelf = weakSelf;
433 ckksnotice("ckkszone", strongSelf, "deleting zones %@ with dependencies %@", zoneDeletionOperation.recordZoneIDsToDelete, zoneDeletionOperation.dependencies);
435 return zoneDeletionGroupOperation;
438 - (void)notifyZoneChange: (CKRecordZoneNotification*) notification {
439 ckksnotice("ckkszone", self, "received a notification for CK zone change, ignoring");
442 - (void)handleCKLogin {
443 ckksinfo("ckkszone", self, "received a notification of CK login");
444 self.accountStatus = CKKSAccountStatusAvailable;
447 - (void)handleCKLogout {
448 ckksinfo("ckkszone", self, "received a notification of CK logout");
449 self.accountStatus = CKKSAccountStatusNoAccount;
453 - (bool)scheduleOperation: (NSOperation*) op {
455 ckkserror("ckkszone", self, "attempted to schedule an operation on a halted zone, ignoring");
459 if(self.accountLoggedInDependency) {
460 [op addDependency: self.accountLoggedInDependency];
463 [self.operationQueue addOperation: op];
467 - (void)cancelAllOperations {
468 [self.operationQueue cancelAllOperations];
471 - (void)waitUntilAllOperationsAreFinished {
472 [self.operationQueue waitUntilAllOperationsAreFinished];
475 - (void)waitForOperationsOfClass:(Class) operationClass {
476 NSArray* operations = [self.operationQueue.operations copy];
477 for(NSOperation* op in operations) {
478 if([op isKindOfClass:operationClass]) {
479 [op waitUntilFinished];
484 - (bool)scheduleAccountStatusOperation: (NSOperation*) op {
486 ckkserror("ckkszone", self, "attempted to schedule an account operation on a halted zone, ignoring");
490 // Always succeed. But, account status operations should always proceed in-order.
491 [op linearDependencies:self.accountOperations];
492 [self.operationQueue addOperation: op];
496 // to be used rarely, if at all
497 - (bool)scheduleOperationWithoutDependencies:(NSOperation*)op {
499 ckkserror("ckkszone", self, "attempted to schedule an non-dependent operation on a halted zone, ignoring");
503 [self.operationQueue addOperation: op];
507 - (void) dispatchSync: (bool (^)(void)) block {
508 // important enough to block this thread.
509 __block bool ok = false;
510 dispatch_sync(self.queue, ^{
512 ckkserror("ckkszone", self, "CKKSZone not dispatchSyncing a block (due to being halted)");
518 ckkserror("ckkszone", self, "CKKSZone block returned false");
524 // Synchronously set the 'halted' bit
525 dispatch_sync(self.queue, ^{
529 // Bring all operations down, too
530 [self cancelAllOperations];