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 <CloudKit/CloudKit.h>
32 #import <CloudKit/CloudKit_Private.h>
35 #import "CKKSKeychainView.h"
38 #include <utilities/debugging.h>
43 @property CKDatabaseOperation<CKKSModifyRecordZonesOperation>* zoneCreationOperation;
44 @property CKDatabaseOperation<CKKSModifyRecordZonesOperation>* zoneDeletionOperation;
45 @property CKDatabaseOperation<CKKSModifySubscriptionsOperation>* zoneSubscriptionOperation;
47 @property bool acceptingNewOperations;
48 @property NSOperationQueue* operationQueue;
49 @property NSOperation* accountLoggedInDependency;
51 @property NSHashTable<NSOperation*>* accountOperations;
55 @implementation CKKSZone
59 - (instancetype)initWithContainer: (CKContainer*) container
60 zoneName: (NSString*) zoneName
61 accountTracker:(CKKSCKAccountStateTracker*) tracker
62 fetchRecordZoneChangesOperationClass: (Class<CKKSFetchRecordZoneChangesOperation>) fetchRecordZoneChangesOperationClass
63 modifySubscriptionsOperationClass: (Class<CKKSModifySubscriptionsOperation>) modifySubscriptionsOperationClass
64 modifyRecordZonesOperationClass: (Class<CKKSModifyRecordZonesOperation>) modifyRecordZonesOperationClass
65 apsConnectionClass: (Class<CKKSAPSConnection>) apsConnectionClass
67 if(self = [super init]) {
68 _container = container;
70 _accountTracker = tracker;
72 _database = [_container privateCloudDatabase];
73 _zone = [[CKRecordZone alloc] initWithZoneID: [[CKRecordZoneID alloc] initWithZoneName:zoneName ownerName:CKCurrentUserDefaultName]];
75 // Every subclass must set up call beginSetup at least once.
76 _accountStatus = CKKSAccountStatusUnknown;
79 _accountOperations = [NSHashTable weakObjectsHashTable];
81 _fetchRecordZoneChangesOperationClass = fetchRecordZoneChangesOperationClass;
82 _modifySubscriptionsOperationClass = modifySubscriptionsOperationClass;
83 _modifyRecordZonesOperationClass = modifyRecordZonesOperationClass;
84 _apsConnectionClass = apsConnectionClass;
86 _queue = dispatch_queue_create([[NSString stringWithFormat:@"CKKSQueue.%@.zone.%@", container.containerIdentifier, zoneName] UTF8String], DISPATCH_QUEUE_SERIAL);
87 _operationQueue = [[NSOperationQueue alloc] init];
88 _acceptingNewOperations = true;
93 // Initialize this object so that we can call beginSetup again
95 self.setupStarted = false;
96 self.setupComplete = false;
98 if([self.zoneSetupOperation isPending]) {
99 // Nothing to do here: there's already an existing zoneSetupOperation
101 self.zoneSetupOperation = [[CKKSGroupOperation alloc] init];
102 self.zoneSetupOperation.name = @"zone-setup-operation";
105 if([self.accountLoggedInDependency isPending]) {
106 // Nothing to do here: there's already an existing accountLoggedInDependency
108 __weak __typeof(self) weakSelf = self;
109 self.accountLoggedInDependency = [NSBlockOperation blockOperationWithBlock:^{
110 ckksnotice("ckkszone", weakSelf, "CloudKit account logged in.");
112 self.accountLoggedInDependency.name = @"account-logged-in-dependency";
115 self.zoneCreated = false;
116 self.zoneSubscribed = false;
117 self.zoneCreatedError = nil;
118 self.zoneSubscribedError = nil;
120 self.zoneCreationOperation = nil;
121 self.zoneSubscriptionOperation = nil;
122 self.zoneDeletionOperation = nil;
125 - (CKRecordZoneID*)zoneID {
126 return [self.zone zoneID];
130 -(void)ckAccountStatusChange: (CKKSAccountStatus)oldStatus to:(CKKSAccountStatus)currentStatus {
132 // dispatch this on a serial queue, so we get each transition in order
133 [self dispatchSync: ^bool {
134 ckksnotice("ckkszone", self, "%@ Received notification of CloudKit account status change, moving from %@ to %@",
135 self.zoneID.zoneName,
136 [CKKSCKAccountStateTracker stringFromAccountStatus: self.accountStatus],
137 [CKKSCKAccountStateTracker stringFromAccountStatus: currentStatus]);
138 CKKSAccountStatus oldStatus = self.accountStatus;
139 self.accountStatus = currentStatus;
141 switch(currentStatus) {
142 case CKKSAccountStatusAvailable: {
144 ckksinfo("ckkszone", self, "logging in while setup started: %d and complete: %d", self.setupStarted, self.setupComplete);
146 // This is only a login if we're not in the middle of setup, and the previous state was not logged in
147 if(!(self.setupStarted ^ self.setupComplete) && oldStatus != CKKSAccountStatusAvailable) {
149 [self handleCKLogin];
152 if(self.accountLoggedInDependency) {
153 [self.operationQueue addOperation:self.accountLoggedInDependency];
154 self.accountLoggedInDependency = nil;
159 case CKKSAccountStatusNoAccount: {
160 ckksnotice("ckkszone", self, "Logging out of iCloud. Shutting down.");
162 self.accountLoggedInDependency = [NSBlockOperation blockOperationWithBlock:^{
163 ckksnotice("ckkszone", self, "CloudKit account logged in again.");
165 self.accountLoggedInDependency.name = @"account-logged-in-dependency";
167 [self.operationQueue cancelAllOperations];
168 [self handleCKLogout];
170 // now we're in a logged out state. Optimistically prepare for a log in!
175 case CKKSAccountStatusUnknown: {
176 // We really don't expect to receive this as a notification, but, okay!
177 ckksnotice("ckkszone", self, "Account status has become undetermined. Pausing for %@", self.zoneID.zoneName);
179 self.accountLoggedInDependency = [NSBlockOperation blockOperationWithBlock:^{
180 ckksnotice("ckkszone", self, "CloudKit account restored from 'unknown'.");
182 self.accountLoggedInDependency.name = @"account-logged-in-dependency";
184 [self.operationQueue cancelAllOperations];
194 - (NSOperation*) createSetupOperation: (bool) zoneCreated zoneSubscribed: (bool) zoneSubscribed {
195 if(!SecCKKSIsEnabled()) {
196 ckksinfo("ckkszone", self, "Skipping CloudKit registration due to disabled CKKS");
200 // If we've already started set up, skip doing it again.
201 if(self.setupStarted) {
202 ckksinfo("ckkszone", self, "skipping startup: it's already started");
203 return self.zoneSetupOperation;
206 if(self.zoneSetupOperation == nil) {
207 ckkserror("ckkszone", self, "trying to set up but the setup operation is gone; what happened?");
211 self.zoneCreated = zoneCreated;
212 self.zoneSubscribed = zoneSubscribed;
214 // Zone setups and teardowns are due to either 1) first CKKS launch or 2) the user logging in to iCloud.
215 // Therefore, they're QoS UserInitiated.
216 self.zoneSetupOperation.queuePriority = NSOperationQueuePriorityNormal;
217 self.zoneSetupOperation.qualityOfService = NSQualityOfServiceUserInitiated;
219 ckksnotice("ckkszone", self, "Setting up zone %@", self.zoneName);
220 self.setupStarted = true;
222 __weak __typeof(self) weakSelf = self;
224 // First, check the account status. If it's sufficient, add the necessary CloudKit operations to this operation
225 NSBlockOperation* doSetup = [NSBlockOperation blockOperationWithBlock:^{
226 __strong __typeof(weakSelf) strongSelf = weakSelf;
228 ckkserror("ckkszone", strongSelf, "received callback for released object");
232 __block bool ret = false;
233 [strongSelf dispatchSync: ^bool {
234 strongSelf.accountStatus = [strongSelf.accountTracker currentCKAccountStatusAndNotifyOnChange:strongSelf];
236 switch(strongSelf.accountStatus) {
237 case CKKSAccountStatusNoAccount:
238 ckkserror("ckkszone", strongSelf, "No CloudKit account; quitting setup for %@", strongSelf.zoneID.zoneName);
239 [strongSelf handleCKLogout];
242 case CKKSAccountStatusAvailable:
243 if(strongSelf.accountLoggedInDependency) {
244 [strongSelf.operationQueue addOperation: strongSelf.accountLoggedInDependency];
245 strongSelf.accountLoggedInDependency = nil;
248 case CKKSAccountStatusUnknown:
249 ckkserror("ckkszone", strongSelf, "CloudKit account status currently unknown; stopping setup for %@", strongSelf.zoneID.zoneName);
257 NSBlockOperation* setupCompleteOperation = [NSBlockOperation blockOperationWithBlock:^{
258 __strong __typeof(weakSelf) strongSelf = weakSelf;
260 secerror("ckkszone: received callback for released object");
264 ckksinfo("ckkszone", strongSelf, "%@: Setup complete", strongSelf.zoneName);
265 strongSelf.setupComplete = true;
267 setupCompleteOperation.name = @"zone-setup-complete-operation";
269 // If we don't have an CloudKit account, don't bother continuing
271 [strongSelf.zoneSetupOperation runBeforeGroupFinished:setupCompleteOperation];
275 // We have an account, so fetch the push environment and bring up APS
276 [strongSelf.container serverPreferredPushEnvironmentWithCompletionHandler: ^(NSString *apsPushEnvString, NSError *error) {
277 __strong __typeof(weakSelf) strongSelf = weakSelf;
279 secerror("ckkszone: received callback for released object");
283 if(error || (apsPushEnvString == nil)) {
284 ckkserror("ckkszone", strongSelf, "Received error fetching preferred push environment (%@). Keychain syncing is highly degraded: %@", apsPushEnvString, error);
286 CKKSAPSReceiver* aps = [CKKSAPSReceiver receiverForEnvironment:apsPushEnvString
287 namedDelegatePort:SecCKKSAPSNamedPort
288 apsConnectionClass:strongSelf.apsConnectionClass];
289 [aps register:strongSelf forZoneID:strongSelf.zoneID];
293 NSBlockOperation* modifyRecordZonesCompleteOperation = nil;
295 ckksnotice("ckkszone", strongSelf, "Creating CloudKit zone '%@'", strongSelf.zoneName);
296 CKDatabaseOperation<CKKSModifyRecordZonesOperation>* zoneCreationOperation = [[strongSelf.modifyRecordZonesOperationClass alloc] initWithRecordZonesToSave: @[strongSelf.zone] recordZoneIDsToDelete: nil];
297 zoneCreationOperation.queuePriority = NSOperationQueuePriorityNormal;
298 zoneCreationOperation.qualityOfService = NSQualityOfServiceUserInitiated;
299 zoneCreationOperation.database = strongSelf.database;
300 zoneCreationOperation.name = @"zone-creation-operation";
302 // Completion blocks don't count for dependencies. Use this intermediate operation hack instead.
303 modifyRecordZonesCompleteOperation = [[NSBlockOperation alloc] init];
304 modifyRecordZonesCompleteOperation.name = @"zone-creation-finished";
306 zoneCreationOperation.modifyRecordZonesCompletionBlock = ^(NSArray<CKRecordZone *> *savedRecordZones, NSArray<CKRecordZoneID *> *deletedRecordZoneIDs, NSError *operationError) {
307 __strong __typeof(weakSelf) strongSelf = weakSelf;
309 secerror("ckkszone: received callback for released object");
313 __strong __typeof(weakSelf) strongSubSelf = weakSelf;
315 if(!operationError) {
316 ckksnotice("ckkszone", strongSubSelf, "Successfully created zone %@", strongSubSelf.zoneName);
317 strongSubSelf.zoneCreated = true;
319 ckkserror("ckkszone", strongSubSelf, "Couldn't create zone %@; %@", strongSubSelf.zoneName, operationError);
321 strongSubSelf.zoneCreatedError = operationError;
323 [strongSubSelf.operationQueue addOperation: modifyRecordZonesCompleteOperation];
326 ckksnotice("ckkszone", strongSelf, "Adding CKKSModifyRecordZonesOperation: %@ %@", zoneCreationOperation, zoneCreationOperation.dependencies);
327 strongSelf.zoneCreationOperation = zoneCreationOperation;
328 [setupCompleteOperation addDependency: modifyRecordZonesCompleteOperation];
329 [strongSelf.zoneSetupOperation runBeforeGroupFinished: zoneCreationOperation];
330 [strongSelf.zoneSetupOperation dependOnBeforeGroupFinished: modifyRecordZonesCompleteOperation];
332 ckksinfo("ckkszone", strongSelf, "no need to create the zone '%@'", strongSelf.zoneName);
335 if(!zoneSubscribed) {
336 ckksnotice("ckkszone", strongSelf, "Creating CloudKit record zone subscription for %@", strongSelf.zoneName);
337 CKRecordZoneSubscription* subscription = [[CKRecordZoneSubscription alloc] initWithZoneID: strongSelf.zoneID subscriptionID:[@"zone:" stringByAppendingString: strongSelf.zoneName]];
338 CKNotificationInfo* notificationInfo = [[CKNotificationInfo alloc] init];
340 notificationInfo.shouldSendContentAvailable = false;
341 subscription.notificationInfo = notificationInfo;
343 CKDatabaseOperation<CKKSModifySubscriptionsOperation>* zoneSubscriptionOperation = [[strongSelf.modifySubscriptionsOperationClass alloc] initWithSubscriptionsToSave: @[subscription] subscriptionIDsToDelete: nil];
345 zoneSubscriptionOperation.queuePriority = NSOperationQueuePriorityNormal;
346 zoneSubscriptionOperation.qualityOfService = NSQualityOfServiceUserInitiated;
347 zoneSubscriptionOperation.database = strongSelf.database;
348 zoneSubscriptionOperation.name = @"zone-subscription-operation";
350 // Completion blocks don't count for dependencies. Use this intermediate operation hack instead.
351 NSBlockOperation* zoneSubscriptionCompleteOperation = [[NSBlockOperation alloc] init];
352 zoneSubscriptionCompleteOperation.name = @"zone-subscription-complete";
353 zoneSubscriptionOperation.modifySubscriptionsCompletionBlock = ^(NSArray<CKSubscription *> * _Nullable savedSubscriptions, NSArray<NSString *> * _Nullable deletedSubscriptionIDs, NSError * _Nullable operationError) {
354 __strong __typeof(weakSelf) strongSubSelf = weakSelf;
356 ckkserror("ckkszone", strongSubSelf, "received callback for released object");
360 if(!operationError) {
361 ckksnotice("ckkszone", strongSubSelf, "Successfully subscribed to %@", savedSubscriptions);
363 // Success; write that down. TODO: actually ensure that the saved subscription matches what we asked for
364 for(CKSubscription* sub in savedSubscriptions) {
365 ckksnotice("ckkszone", strongSubSelf, "Successfully subscribed to %@", sub.subscriptionID);
366 strongSubSelf.zoneSubscribed = true;
369 ckkserror("ckkszone", strongSubSelf, "Couldn't create cloudkit zone subscription; keychain syncing is severely degraded: %@", operationError);
372 strongSubSelf.zoneSubscribedError = operationError;
373 strongSubSelf.zoneSubscriptionOperation = nil;
375 [strongSubSelf.operationQueue addOperation: zoneSubscriptionCompleteOperation];
378 if(modifyRecordZonesCompleteOperation) {
379 [zoneSubscriptionOperation addDependency:modifyRecordZonesCompleteOperation];
381 strongSelf.zoneSubscriptionOperation = zoneSubscriptionOperation;
382 [setupCompleteOperation addDependency: zoneSubscriptionCompleteOperation];
383 [strongSelf.zoneSetupOperation runBeforeGroupFinished:zoneSubscriptionOperation];
384 [strongSelf.zoneSetupOperation dependOnBeforeGroupFinished: zoneSubscriptionCompleteOperation];
386 ckksinfo("ckkszone", strongSelf, "no need to create database subscription");
389 [strongSelf.zoneSetupOperation runBeforeGroupFinished:setupCompleteOperation];
391 doSetup.name = @"begin-zone-setup";
393 [self.zoneSetupOperation runBeforeGroupFinished:doSetup];
395 return self.zoneSetupOperation;
399 - (CKKSResultOperation*)beginResetCloudKitZoneOperation {
400 if(!SecCKKSIsEnabled()) {
401 ckksinfo("ckkszone", self, "Skipping CloudKit reset due to disabled CKKS");
405 // We want to delete this zone and this subscription from CloudKit.
407 // Step 1: cancel setup operations (if they exist)
408 [self.accountLoggedInDependency cancel];
409 [self.zoneSetupOperation cancel];
410 [self.zoneCreationOperation cancel];
411 [self.zoneSubscriptionOperation cancel];
413 // Step 2: Try to delete the zone
414 CKDatabaseOperation<CKKSModifyRecordZonesOperation>* zoneDeletionOperation = [[self.modifyRecordZonesOperationClass alloc] initWithRecordZonesToSave: nil recordZoneIDsToDelete: @[self.zoneID]];
415 zoneDeletionOperation.queuePriority = NSOperationQueuePriorityNormal;
416 zoneDeletionOperation.qualityOfService = NSQualityOfServiceUserInitiated;
417 zoneDeletionOperation.database = self.database;
419 CKKSResultOperation* doneOp = [CKKSResultOperation named:@"zone-reset-watcher" withBlock:^{}];
421 __weak __typeof(self) weakSelf = self;
423 zoneDeletionOperation.modifyRecordZonesCompletionBlock = ^(NSArray<CKRecordZone *> *savedRecordZones, NSArray<CKRecordZoneID *> *deletedRecordZoneIDs, NSError *operationError) {
424 __strong __typeof(weakSelf) strongSelf = weakSelf;
426 ckkserror("ckkszone", strongSelf, "received callback for released object");
430 ckksinfo("ckkszone", strongSelf, "record zones deletion %@ completed with error: %@", deletedRecordZoneIDs, operationError);
431 [strongSelf resetSetup];
433 doneOp.error = operationError;
434 [strongSelf.operationQueue addOperation: doneOp];
437 // If the zone creation operation is still pending, wait for it to complete before attempting zone deletion
438 [zoneDeletionOperation addNullableDependency: self.zoneCreationOperation];
440 ckksinfo("ckkszone", self, "deleting zone with %@ %@", zoneDeletionOperation, zoneDeletionOperation.dependencies);
441 // Don't use scheduleOperation: zone deletions should be attempted even if we're "logged out"
442 [self.operationQueue addOperation: zoneDeletionOperation];
443 self.zoneDeletionOperation = zoneDeletionOperation;
447 - (void)notifyZoneChange: (CKRecordZoneNotification*) notification {
448 ckksnotice("ckkszone", self, "received a notification for CK zone change, ignoring");
451 - (void)handleCKLogin {
452 ckksinfo("ckkszone", self, "received a notification of CK login, ignoring");
455 - (void)handleCKLogout {
456 ckksinfo("ckkszone", self, "received a notification of CK logout, ignoring");
459 - (bool)scheduleOperation: (NSOperation*) op {
460 if(!self.acceptingNewOperations) {
461 ckksdebug("ckkszone", self, "attempted to schedule an operation on a cancelled zone, ignoring");
465 if(self.accountLoggedInDependency) {
466 [op addDependency: self.accountLoggedInDependency];
469 [self.operationQueue addOperation: op];
473 - (void)cancelAllOperations {
474 [self.operationQueue cancelAllOperations];
477 - (void)waitUntilAllOperationsAreFinished {
478 [self.operationQueue waitUntilAllOperationsAreFinished];
481 - (void)waitForOperationsOfClass:(Class) operationClass {
482 NSArray* operations = [self.operationQueue.operations copy];
483 for(NSOperation* op in operations) {
484 if([op isKindOfClass:operationClass]) {
485 [op waitUntilFinished];
490 - (bool)scheduleAccountStatusOperation: (NSOperation*) op {
491 // Always succeed. But, account status operations should always proceed in-order.
492 [op linearDependencies:self.accountOperations];
493 [self.operationQueue addOperation: op];
497 // to be used rarely, if at all
498 - (bool)scheduleOperationWithoutDependencies:(NSOperation*)op {
499 [self.operationQueue addOperation: op];
503 - (void) dispatchSync: (bool (^)(void)) block {
504 // important enough to block this thread.
505 __block bool ok = false;
506 dispatch_sync(self.queue, ^{
509 ckkserror("ckkszone", self, "CKKSZone block returned false");