]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSZone.m
Security-58286.1.32.tar.gz
[apple/security.git] / keychain / ckks / CKKSZone.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 #include <AssertMacros.h>
25
26 #import <Foundation/Foundation.h>
27
28 #if OCTAGON
29 #import "CloudKitDependencies.h"
30 #import "keychain/ckks/CKKSCKAccountStateTracker.h"
31 #import <CloudKit/CloudKit.h>
32 #import <CloudKit/CloudKit_Private.h>
33 #endif
34
35 #import "CKKSKeychainView.h"
36 #import "CKKSZone.h"
37
38 #include <utilities/debugging.h>
39
40 @interface CKKSZone()
41 #if OCTAGON
42
43 @property CKDatabaseOperation<CKKSModifyRecordZonesOperation>* zoneCreationOperation;
44 @property CKDatabaseOperation<CKKSModifyRecordZonesOperation>* zoneDeletionOperation;
45 @property CKDatabaseOperation<CKKSModifySubscriptionsOperation>* zoneSubscriptionOperation;
46
47 @property bool acceptingNewOperations;
48 @property NSOperationQueue* operationQueue;
49 @property NSOperation* accountLoggedInDependency;
50
51 @property NSHashTable<NSOperation*>* accountOperations;
52 #endif
53 @end
54
55 @implementation CKKSZone
56
57 #if OCTAGON
58
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
66 {
67 if(self = [super init]) {
68 _container = container;
69 _zoneName = zoneName;
70 _accountTracker = tracker;
71
72 _database = [_container privateCloudDatabase];
73 _zone = [[CKRecordZone alloc] initWithZoneID: [[CKRecordZoneID alloc] initWithZoneName:zoneName ownerName:CKCurrentUserDefaultName]];
74
75 // Every subclass must set up call beginSetup at least once.
76 _accountStatus = CKKSAccountStatusUnknown;
77 [self resetSetup];
78
79 _accountOperations = [NSHashTable weakObjectsHashTable];
80
81 _fetchRecordZoneChangesOperationClass = fetchRecordZoneChangesOperationClass;
82 _modifySubscriptionsOperationClass = modifySubscriptionsOperationClass;
83 _modifyRecordZonesOperationClass = modifyRecordZonesOperationClass;
84 _apsConnectionClass = apsConnectionClass;
85
86 _queue = dispatch_queue_create([[NSString stringWithFormat:@"CKKSQueue.%@.zone.%@", container.containerIdentifier, zoneName] UTF8String], DISPATCH_QUEUE_SERIAL);
87 _operationQueue = [[NSOperationQueue alloc] init];
88 _acceptingNewOperations = true;
89 }
90 return self;
91 }
92
93 // Initialize this object so that we can call beginSetup again
94 - (void)resetSetup {
95 self.setupStarted = false;
96 self.setupComplete = false;
97
98 if([self.zoneSetupOperation isPending]) {
99 // Nothing to do here: there's already an existing zoneSetupOperation
100 } else {
101 self.zoneSetupOperation = [[CKKSGroupOperation alloc] init];
102 self.zoneSetupOperation.name = @"zone-setup-operation";
103 }
104
105 if([self.accountLoggedInDependency isPending]) {
106 // Nothing to do here: there's already an existing accountLoggedInDependency
107 } else {
108 __weak __typeof(self) weakSelf = self;
109 self.accountLoggedInDependency = [NSBlockOperation blockOperationWithBlock:^{
110 ckksnotice("ckkszone", weakSelf, "CloudKit account logged in.");
111 }];
112 self.accountLoggedInDependency.name = @"account-logged-in-dependency";
113 }
114
115 self.zoneCreated = false;
116 self.zoneSubscribed = false;
117 self.zoneCreatedError = nil;
118 self.zoneSubscribedError = nil;
119
120 self.zoneCreationOperation = nil;
121 self.zoneSubscriptionOperation = nil;
122 self.zoneDeletionOperation = nil;
123 }
124
125 - (CKRecordZoneID*)zoneID {
126 return [self.zone zoneID];
127 }
128
129
130 -(void)ckAccountStatusChange: (CKKSAccountStatus)oldStatus to:(CKKSAccountStatus)currentStatus {
131
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;
140
141 switch(currentStatus) {
142 case CKKSAccountStatusAvailable: {
143
144 ckksinfo("ckkszone", self, "logging in while setup started: %d and complete: %d", self.setupStarted, self.setupComplete);
145
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) {
148 [self resetSetup];
149 [self handleCKLogin];
150 }
151
152 if(self.accountLoggedInDependency) {
153 [self.operationQueue addOperation:self.accountLoggedInDependency];
154 self.accountLoggedInDependency = nil;
155 };
156 }
157 break;
158
159 case CKKSAccountStatusNoAccount: {
160 ckksnotice("ckkszone", self, "Logging out of iCloud. Shutting down.");
161
162 self.accountLoggedInDependency = [NSBlockOperation blockOperationWithBlock:^{
163 ckksnotice("ckkszone", self, "CloudKit account logged in again.");
164 }];
165 self.accountLoggedInDependency.name = @"account-logged-in-dependency";
166
167 [self.operationQueue cancelAllOperations];
168 [self handleCKLogout];
169
170 // now we're in a logged out state. Optimistically prepare for a log in!
171 [self resetSetup];
172 }
173 break;
174
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);
178
179 self.accountLoggedInDependency = [NSBlockOperation blockOperationWithBlock:^{
180 ckksnotice("ckkszone", self, "CloudKit account restored from 'unknown'.");
181 }];
182 self.accountLoggedInDependency.name = @"account-logged-in-dependency";
183
184 [self.operationQueue cancelAllOperations];
185 [self resetSetup];
186 }
187 break;
188 }
189
190 return true;
191 }];
192 }
193
194 - (NSOperation*) createSetupOperation: (bool) zoneCreated zoneSubscribed: (bool) zoneSubscribed {
195 if(!SecCKKSIsEnabled()) {
196 ckksinfo("ckkszone", self, "Skipping CloudKit registration due to disabled CKKS");
197 return nil;
198 }
199
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;
204 }
205
206 if(self.zoneSetupOperation == nil) {
207 ckkserror("ckkszone", self, "trying to set up but the setup operation is gone; what happened?");
208 return nil;
209 }
210
211 self.zoneCreated = zoneCreated;
212 self.zoneSubscribed = zoneSubscribed;
213
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;
218
219 ckksnotice("ckkszone", self, "Setting up zone %@", self.zoneName);
220 self.setupStarted = true;
221
222 __weak __typeof(self) weakSelf = self;
223
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;
227 if(!strongSelf) {
228 ckkserror("ckkszone", strongSelf, "received callback for released object");
229 return;
230 }
231
232 __block bool ret = false;
233 [strongSelf dispatchSync: ^bool {
234 strongSelf.accountStatus = [strongSelf.accountTracker currentCKAccountStatusAndNotifyOnChange:strongSelf];
235
236 switch(strongSelf.accountStatus) {
237 case CKKSAccountStatusNoAccount:
238 ckkserror("ckkszone", strongSelf, "No CloudKit account; quitting setup for %@", strongSelf.zoneID.zoneName);
239 [strongSelf handleCKLogout];
240 ret = true;
241 break;
242 case CKKSAccountStatusAvailable:
243 if(strongSelf.accountLoggedInDependency) {
244 [strongSelf.operationQueue addOperation: strongSelf.accountLoggedInDependency];
245 strongSelf.accountLoggedInDependency = nil;
246 }
247 break;
248 case CKKSAccountStatusUnknown:
249 ckkserror("ckkszone", strongSelf, "CloudKit account status currently unknown; stopping setup for %@", strongSelf.zoneID.zoneName);
250 ret = true;
251 break;
252 }
253
254 return true;
255 }];
256
257 NSBlockOperation* setupCompleteOperation = [NSBlockOperation blockOperationWithBlock:^{
258 __strong __typeof(weakSelf) strongSelf = weakSelf;
259 if(!strongSelf) {
260 secerror("ckkszone: received callback for released object");
261 return;
262 }
263
264 ckksinfo("ckkszone", strongSelf, "%@: Setup complete", strongSelf.zoneName);
265 strongSelf.setupComplete = true;
266 }];
267 setupCompleteOperation.name = @"zone-setup-complete-operation";
268
269 // If we don't have an CloudKit account, don't bother continuing
270 if(ret) {
271 [strongSelf.zoneSetupOperation runBeforeGroupFinished:setupCompleteOperation];
272 return;
273 }
274
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;
278 if(!strongSelf) {
279 secerror("ckkszone: received callback for released object");
280 return;
281 }
282
283 if(error || (apsPushEnvString == nil)) {
284 ckkserror("ckkszone", strongSelf, "Received error fetching preferred push environment (%@). Keychain syncing is highly degraded: %@", apsPushEnvString, error);
285 } else {
286 CKKSAPSReceiver* aps = [CKKSAPSReceiver receiverForEnvironment:apsPushEnvString
287 namedDelegatePort:SecCKKSAPSNamedPort
288 apsConnectionClass:strongSelf.apsConnectionClass];
289 [aps register:strongSelf forZoneID:strongSelf.zoneID];
290 }
291 }];
292
293 NSBlockOperation* modifyRecordZonesCompleteOperation = nil;
294 if(!zoneCreated) {
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";
301
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";
305
306 zoneCreationOperation.modifyRecordZonesCompletionBlock = ^(NSArray<CKRecordZone *> *savedRecordZones, NSArray<CKRecordZoneID *> *deletedRecordZoneIDs, NSError *operationError) {
307 __strong __typeof(weakSelf) strongSelf = weakSelf;
308 if(!strongSelf) {
309 secerror("ckkszone: received callback for released object");
310 return;
311 }
312
313 __strong __typeof(weakSelf) strongSubSelf = weakSelf;
314
315 if(!operationError) {
316 ckksnotice("ckkszone", strongSubSelf, "Successfully created zone %@", strongSubSelf.zoneName);
317 strongSubSelf.zoneCreated = true;
318 } else {
319 ckkserror("ckkszone", strongSubSelf, "Couldn't create zone %@; %@", strongSubSelf.zoneName, operationError);
320 }
321 strongSubSelf.zoneCreatedError = operationError;
322
323 [strongSubSelf.operationQueue addOperation: modifyRecordZonesCompleteOperation];
324 };
325
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];
331 } else {
332 ckksinfo("ckkszone", strongSelf, "no need to create the zone '%@'", strongSelf.zoneName);
333 }
334
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];
339
340 notificationInfo.shouldSendContentAvailable = false;
341 subscription.notificationInfo = notificationInfo;
342
343 CKDatabaseOperation<CKKSModifySubscriptionsOperation>* zoneSubscriptionOperation = [[strongSelf.modifySubscriptionsOperationClass alloc] initWithSubscriptionsToSave: @[subscription] subscriptionIDsToDelete: nil];
344
345 zoneSubscriptionOperation.queuePriority = NSOperationQueuePriorityNormal;
346 zoneSubscriptionOperation.qualityOfService = NSQualityOfServiceUserInitiated;
347 zoneSubscriptionOperation.database = strongSelf.database;
348 zoneSubscriptionOperation.name = @"zone-subscription-operation";
349
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;
355 if(!strongSubSelf) {
356 ckkserror("ckkszone", strongSubSelf, "received callback for released object");
357 return;
358 }
359
360 if(!operationError) {
361 ckksnotice("ckkszone", strongSubSelf, "Successfully subscribed to %@", savedSubscriptions);
362
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;
367 }
368 } else {
369 ckkserror("ckkszone", strongSubSelf, "Couldn't create cloudkit zone subscription; keychain syncing is severely degraded: %@", operationError);
370 }
371
372 strongSubSelf.zoneSubscribedError = operationError;
373 strongSubSelf.zoneSubscriptionOperation = nil;
374
375 [strongSubSelf.operationQueue addOperation: zoneSubscriptionCompleteOperation];
376 };
377
378 if(modifyRecordZonesCompleteOperation) {
379 [zoneSubscriptionOperation addDependency:modifyRecordZonesCompleteOperation];
380 }
381 strongSelf.zoneSubscriptionOperation = zoneSubscriptionOperation;
382 [setupCompleteOperation addDependency: zoneSubscriptionCompleteOperation];
383 [strongSelf.zoneSetupOperation runBeforeGroupFinished:zoneSubscriptionOperation];
384 [strongSelf.zoneSetupOperation dependOnBeforeGroupFinished: zoneSubscriptionCompleteOperation];
385 } else {
386 ckksinfo("ckkszone", strongSelf, "no need to create database subscription");
387 }
388
389 [strongSelf.zoneSetupOperation runBeforeGroupFinished:setupCompleteOperation];
390 }];
391 doSetup.name = @"begin-zone-setup";
392
393 [self.zoneSetupOperation runBeforeGroupFinished:doSetup];
394
395 return self.zoneSetupOperation;
396 }
397
398
399 - (CKKSResultOperation*)beginResetCloudKitZoneOperation {
400 if(!SecCKKSIsEnabled()) {
401 ckksinfo("ckkszone", self, "Skipping CloudKit reset due to disabled CKKS");
402 return nil;
403 }
404
405 // We want to delete this zone and this subscription from CloudKit.
406
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];
412
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;
418
419 CKKSResultOperation* doneOp = [CKKSResultOperation named:@"zone-reset-watcher" withBlock:^{}];
420
421 __weak __typeof(self) weakSelf = self;
422
423 zoneDeletionOperation.modifyRecordZonesCompletionBlock = ^(NSArray<CKRecordZone *> *savedRecordZones, NSArray<CKRecordZoneID *> *deletedRecordZoneIDs, NSError *operationError) {
424 __strong __typeof(weakSelf) strongSelf = weakSelf;
425 if(!strongSelf) {
426 ckkserror("ckkszone", strongSelf, "received callback for released object");
427 return;
428 }
429
430 ckksinfo("ckkszone", strongSelf, "record zones deletion %@ completed with error: %@", deletedRecordZoneIDs, operationError);
431 [strongSelf resetSetup];
432
433 doneOp.error = operationError;
434 [strongSelf.operationQueue addOperation: doneOp];
435 };
436
437 // If the zone creation operation is still pending, wait for it to complete before attempting zone deletion
438 [zoneDeletionOperation addNullableDependency: self.zoneCreationOperation];
439
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;
444 return doneOp;
445 }
446
447 - (void)notifyZoneChange: (CKRecordZoneNotification*) notification {
448 ckksnotice("ckkszone", self, "received a notification for CK zone change, ignoring");
449 }
450
451 - (void)handleCKLogin {
452 ckksinfo("ckkszone", self, "received a notification of CK login, ignoring");
453 }
454
455 - (void)handleCKLogout {
456 ckksinfo("ckkszone", self, "received a notification of CK logout, ignoring");
457 }
458
459 - (bool)scheduleOperation: (NSOperation*) op {
460 if(!self.acceptingNewOperations) {
461 ckksdebug("ckkszone", self, "attempted to schedule an operation on a cancelled zone, ignoring");
462 return false;
463 }
464
465 if(self.accountLoggedInDependency) {
466 [op addDependency: self.accountLoggedInDependency];
467 }
468
469 [self.operationQueue addOperation: op];
470 return true;
471 }
472
473 - (void)cancelAllOperations {
474 [self.operationQueue cancelAllOperations];
475 }
476
477 - (void)waitUntilAllOperationsAreFinished {
478 [self.operationQueue waitUntilAllOperationsAreFinished];
479 }
480
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];
486 }
487 }
488 }
489
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];
494 return true;
495 }
496
497 // to be used rarely, if at all
498 - (bool)scheduleOperationWithoutDependencies:(NSOperation*)op {
499 [self.operationQueue addOperation: op];
500 return true;
501 }
502
503 - (void) dispatchSync: (bool (^)(void)) block {
504 // important enough to block this thread.
505 __block bool ok = false;
506 dispatch_sync(self.queue, ^{
507 ok = block();
508 if(!ok) {
509 ckkserror("ckkszone", self, "CKKSZone block returned false");
510 }
511 });
512 }
513
514
515 #endif /* OCTAGON */
516 @end
517
518
519