]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSZone.m
Security-59306.140.5.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/CKKSAccountStateTracker.h"
31 #import "keychain/ckks/CloudKitCategories.h"
32 #import "keychain/categories/NSError+UsefulConstructors.h"
33 #import <CloudKit/CloudKit.h>
34 #import <CloudKit/CloudKit_Private.h>
35
36 #import "keychain/ot/ObjCImprovements.h"
37
38 #import "CKKSKeychainView.h"
39 #import "CKKSZone.h"
40
41 #include <utilities/debugging.h>
42
43 @interface CKKSZone()
44
45 @property CKDatabaseOperation<CKKSModifyRecordZonesOperation>* zoneCreationOperation;
46 @property CKDatabaseOperation<CKKSModifyRecordZonesOperation>* zoneDeletionOperation;
47 @property CKDatabaseOperation<CKKSModifySubscriptionsOperation>* zoneSubscriptionOperation;
48
49 @property NSOperationQueue* operationQueue;
50 @property CKKSResultOperation* accountLoggedInDependency;
51
52 @property NSHashTable<NSOperation*>* accountOperations;
53
54 // Make writable
55 @property bool halted;
56 @end
57
58 @implementation CKKSZone
59
60 - (instancetype)initWithContainer:(CKContainer*)container
61 zoneName:(NSString*)zoneName
62 accountTracker:(CKKSAccountStateTracker*)accountTracker
63 reachabilityTracker:(CKKSReachabilityTracker*)reachabilityTracker
64 zoneModifier:(CKKSZoneModifier*)zoneModifier
65 cloudKitClassDependencies:(CKKSCloudKitClassDependencies*)cloudKitClassDependencies
66 {
67 if(self = [super init]) {
68 _container = container;
69 _zoneName = zoneName;
70 _accountTracker = accountTracker;
71 _reachabilityTracker = reachabilityTracker;
72
73 _zoneModifier = zoneModifier;
74
75 _halted = false;
76
77 _database = [_container privateCloudDatabase];
78 _zone = [[CKRecordZone alloc] initWithZoneID: [[CKRecordZoneID alloc] initWithZoneName:zoneName ownerName:CKCurrentUserDefaultName]];
79
80 _accountStatus = CKKSAccountStatusUnknown;
81
82 _accountLoggedInDependency = [self createAccountLoggedInDependency:@"CloudKit account logged in."];
83
84 _accountOperations = [NSHashTable weakObjectsHashTable];
85
86 _cloudKitClassDependencies = cloudKitClassDependencies;
87
88 _queue = dispatch_queue_create([[NSString stringWithFormat:@"CKKSQueue.%@.zone.%@", container.containerIdentifier, zoneName] UTF8String], DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
89 _operationQueue = [[NSOperationQueue alloc] init];
90 }
91 return self;
92 }
93
94 - (CKKSResultOperation*)createAccountLoggedInDependency:(NSString*)message {
95 WEAKIFY(self);
96 CKKSResultOperation* accountLoggedInDependency = [CKKSResultOperation named:@"account-logged-in-dependency" withBlock:^{
97 STRONGIFY(self);
98 ckksnotice("ckkszone", self, "%@", message);
99 }];
100 accountLoggedInDependency.descriptionErrorCode = CKKSResultDescriptionPendingAccountLoggedIn;
101 return accountLoggedInDependency;
102 }
103
104 - (void)beginCloudKitOperation {
105 [self.accountTracker registerForNotificationsOfCloudKitAccountStatusChange:self];
106 }
107
108 - (void)resetSetup {
109 self.zoneCreated = false;
110 self.zoneSubscribed = false;
111 self.zoneCreatedError = nil;
112 self.zoneSubscribedError = nil;
113
114 self.zoneCreationOperation = nil;
115 self.zoneSubscriptionOperation = nil;
116 self.zoneDeletionOperation = nil;
117 }
118
119 - (CKRecordZoneID*)zoneID {
120 return [self.zone zoneID];
121 }
122
123 - (CKKSAccountStatus)accountStatusFromCKAccountInfo:(CKAccountInfo*)info
124 {
125 if(!info) {
126 return CKKSAccountStatusUnknown;
127 }
128 if(info.accountStatus == CKAccountStatusAvailable &&
129 info.hasValidCredentials) {
130 return CKKSAccountStatusAvailable;
131 } else {
132 return CKKSAccountStatusNoAccount;
133 }
134 }
135
136
137 - (void)cloudkitAccountStateChange:(CKAccountInfo* _Nullable)oldAccountInfo to:(CKAccountInfo*)currentAccountInfo
138 {
139 ckksnotice("ckkszone", self, "%@ Received notification of CloudKit account status change, moving from %@ to %@",
140 self.zoneID.zoneName,
141 oldAccountInfo,
142 currentAccountInfo);
143
144 // Filter for device2device encryption and cloudkit grey mode
145 CKKSAccountStatus oldStatus = [self accountStatusFromCKAccountInfo:oldAccountInfo];
146 CKKSAccountStatus currentStatus = [self accountStatusFromCKAccountInfo:currentAccountInfo];
147
148 if(oldStatus == currentStatus) {
149 ckksnotice("ckkszone", self, "Computed status of new CK account info is same as old status: %@", [CKKSAccountStateTracker stringFromAccountStatus:currentStatus]);
150 return;
151 }
152
153 switch(currentStatus) {
154 case CKKSAccountStatusAvailable: {
155 ckksnotice("ckkszone", self, "Logged into iCloud.");
156 [self handleCKLogin];
157
158 if(self.accountLoggedInDependency) {
159 [self.operationQueue addOperation:self.accountLoggedInDependency];
160 self.accountLoggedInDependency = nil;
161 };
162 }
163 break;
164
165 case CKKSAccountStatusNoAccount: {
166 ckksnotice("ckkszone", self, "Logging out of iCloud. Shutting down.");
167
168 if(!self.accountLoggedInDependency) {
169 self.accountLoggedInDependency = [self createAccountLoggedInDependency:@"CloudKit account logged in again."];
170 }
171
172 [self handleCKLogout];
173 }
174 break;
175
176 case CKKSAccountStatusUnknown: {
177 // We really don't expect to receive this as a notification, but, okay!
178 ckksnotice("ckkszone", self, "Account status has become undetermined. Pausing for %@", self.zoneID.zoneName);
179
180 if(!self.accountLoggedInDependency) {
181 self.accountLoggedInDependency = [self createAccountLoggedInDependency:@"CloudKit account logged in again."];
182 }
183
184 [self handleCKLogout];
185 }
186 break;
187 }
188 }
189
190 - (CKKSResultOperation*)handleCKLogin:(bool)zoneCreated zoneSubscribed:(bool)zoneSubscribed {
191 if(!SecCKKSIsEnabled()) {
192 ckksinfo("ckkszone", self, "Skipping CloudKit registration due to disabled CKKS");
193 return nil;
194 }
195
196 // If we've already started set up and that hasn't finished, complain
197 if([self.zoneSetupOperation isPending] || [self.zoneSetupOperation isExecuting]) {
198 ckksnotice("ckkszone", self, "Asked to handleCKLogin, but zoneSetupOperation appears to not be complete? %@ Continuing anyway", self.zoneSetupOperation);
199 }
200
201 self.zoneSetupOperation = [[CKKSGroupOperation alloc] init];
202 self.zoneSetupOperation.name = [NSString stringWithFormat:@"zone-setup-operation-%@", self.zoneName];
203
204 self.zoneCreated = zoneCreated;
205 self.zoneSubscribed = zoneSubscribed;
206
207 ckksnotice("ckkszone", self, "Setting up zone %@", self.zoneName);
208
209 WEAKIFY(self);
210
211 // First, check the account status. If it's sufficient, add the necessary CloudKit operations to this operation
212 __weak CKKSGroupOperation* weakZoneSetupOperation = self.zoneSetupOperation;
213 [self.zoneSetupOperation runBeforeGroupFinished:[CKKSResultOperation named:[NSString stringWithFormat:@"zone-setup-%@", self.zoneName] withBlock:^{
214 STRONGIFY(self);
215 __strong __typeof(self.zoneSetupOperation) zoneSetupOperation = weakZoneSetupOperation;
216 if(!self || !zoneSetupOperation) {
217 ckkserror("ckkszone", self, "received callback for released object");
218 return;
219 }
220
221 if(self.accountStatus != CKKSAccountStatusAvailable) {
222 ckkserror("ckkszone", self, "Zone doesn't believe it's logged in; quitting setup");
223 return;
224 }
225
226 NSBlockOperation* setupCompleteOperation = [NSBlockOperation blockOperationWithBlock:^{
227 STRONGIFY(self);
228 if(!self) {
229 secerror("ckkszone: received callback for released object");
230 return;
231 }
232
233 ckksnotice("ckkszone", self, "%@: Setup complete", self.zoneName);
234 }];
235 setupCompleteOperation.name = @"zone-setup-complete-operation";
236
237 // We have an account, so fetch the push environment and bring up APS
238 [self.container serverPreferredPushEnvironmentWithCompletionHandler: ^(NSString *apsPushEnvString, NSError *error) {
239 STRONGIFY(self);
240 if(!self) {
241 secerror("ckkszone: received callback for released object");
242 return;
243 }
244
245 if(error || (apsPushEnvString == nil)) {
246 ckkserror("ckkszone", self, "Received error fetching preferred push environment (%@). Keychain syncing is highly degraded: %@", apsPushEnvString, error);
247 } else {
248 OctagonAPSReceiver* aps = [OctagonAPSReceiver receiverForEnvironment:apsPushEnvString
249 namedDelegatePort:SecCKKSAPSNamedPort
250 apsConnectionClass:self.cloudKitClassDependencies.apsConnectionClass];
251 [aps registerReceiver:self forZoneID:self.zoneID];
252 }
253 }];
254
255 if(!zoneCreated || !zoneSubscribed) {
256 ckksnotice("ckkszone", self, "Asking to create and subscribe to CloudKit zone '%@'", self.zoneName);
257 CKKSZoneModifyOperations* zoneOps = [self.zoneModifier createZone:self.zone];
258
259 CKKSResultOperation* handleModificationsOperation = [CKKSResultOperation named:@"handle-modification" withBlock:^{
260 STRONGIFY(self);
261 if([zoneOps.savedRecordZones containsObject:self.zone]) {
262 ckksnotice("ckkszone", self, "Successfully created '%@'", self.zoneName);
263 self.zoneCreated = true;
264 } else {
265 ckksnotice("ckkszone", self, "Failed to create '%@'", self.zoneName);
266 self.zoneCreatedError = zoneOps.zoneModificationOperation.error;
267 }
268
269 bool createdSubscription = false;
270 for(CKSubscription* subscription in zoneOps.savedSubscriptions) {
271 if([subscription.zoneID isEqual:self.zoneID]) {
272 createdSubscription = true;
273 break;
274 }
275 }
276
277 if(createdSubscription) {
278 ckksnotice("ckkszone", self, "Successfully subscribed '%@'", self.zoneName);
279 self.zoneSubscribed = true;
280 } else {
281 ckksnotice("ckkszone", self, "Failed to subscribe to '%@'", self.zoneName);
282 self.zoneSubscribedError = zoneOps.zoneSubscriptionOperation.error;
283 }
284 }];
285 [setupCompleteOperation addDependency:zoneOps.zoneModificationOperation];
286 [handleModificationsOperation addDependency:zoneOps.zoneModificationOperation];
287 [handleModificationsOperation addDependency:zoneOps.zoneSubscriptionOperation];
288 [zoneSetupOperation runBeforeGroupFinished:handleModificationsOperation];
289 } else {
290 ckksnotice("ckkszone", self, "no need to create or subscribe to the zone '%@'", self.zoneName);
291 }
292
293 [self.zoneSetupOperation runBeforeGroupFinished:setupCompleteOperation];
294 }]];
295
296 [self scheduleAccountStatusOperation:self.zoneSetupOperation];
297 return self.zoneSetupOperation;
298 }
299
300
301 - (CKKSResultOperation*)deleteCloudKitZoneOperation:(CKOperationGroup* _Nullable)ckoperationGroup {
302 if(!SecCKKSIsEnabled()) {
303 ckksnotice("ckkszone", self, "Skipping CloudKit reset due to disabled CKKS");
304 return nil;
305 }
306
307 WEAKIFY(self);
308
309 // We want to delete this zone and this subscription from CloudKit.
310
311 // Step 1: cancel setup operations (if they exist)
312 [self.accountLoggedInDependency cancel];
313 [self.zoneSetupOperation cancel];
314 [self.zoneCreationOperation cancel];
315 [self.zoneSubscriptionOperation cancel];
316
317 // Step 2: Try to delete the zone
318
319 CKKSZoneModifyOperations* zoneOps = [self.zoneModifier deleteZone:self.zoneID];
320
321 CKKSResultOperation* afterModification = [CKKSResultOperation named:@"after-modification" withBlockTakingSelf:^(CKKSResultOperation * _Nonnull op) {
322 STRONGIFY(self);
323
324 bool fatalError = false;
325
326 NSError* operationError = zoneOps.zoneModificationOperation.error;
327 bool removed = [zoneOps.deletedRecordZoneIDs containsObject:self.zoneID];
328
329 if(!removed && operationError) {
330 // Okay, but if this error is either 'ZoneNotFound' or 'UserDeletedZone', that's fine by us: the zone is deleted.
331 NSDictionary* partialErrors = operationError.userInfo[CKPartialErrorsByItemIDKey];
332 if([operationError.domain isEqualToString:CKErrorDomain] && operationError.code == CKErrorPartialFailure && partialErrors) {
333 for(CKRecordZoneID* errorZoneID in partialErrors.allKeys) {
334 NSError* errorZone = partialErrors[errorZoneID];
335
336 if(errorZone && [errorZone.domain isEqualToString:CKErrorDomain] &&
337 (errorZone.code == CKErrorZoneNotFound || errorZone.code == CKErrorUserDeletedZone)) {
338 ckksnotice("ckkszone", self, "Attempted to delete zone %@, but it's already missing. This is okay: %@", errorZoneID, errorZone);
339 } else {
340 fatalError = true;
341 }
342 }
343
344 } else {
345 fatalError = true;
346 }
347
348 ckksnotice("ckkszone", self, "deletion of record zone %@ completed with error: %@", self.zoneID, operationError);
349
350 if(fatalError) {
351 op.error = operationError;
352 }
353 } else {
354 ckksnotice("ckkszone", self, "deletion of record zone %@ completed successfully", self.zoneID);
355 }
356 }];
357
358 [afterModification addDependency:zoneOps.zoneModificationOperation];
359 return afterModification;
360 }
361
362 - (void)notifyZoneChange: (CKRecordZoneNotification*) notification {
363 ckksnotice("ckkszone", self, "received a notification for CK zone change, ignoring");
364 }
365
366 - (void)handleCKLogin {
367 ckksinfo("ckkszone", self, "received a notification of CK login");
368 self.accountStatus = CKKSAccountStatusAvailable;
369 }
370
371 - (void)handleCKLogout {
372 ckksinfo("ckkszone", self, "received a notification of CK logout");
373 self.accountStatus = CKKSAccountStatusNoAccount;
374 [self resetSetup];
375 }
376
377 - (bool)scheduleOperation: (NSOperation*) op {
378 if(self.halted) {
379 ckkserror("ckkszone", self, "attempted to schedule an operation on a halted zone, ignoring");
380 return false;
381 }
382
383 [op addNullableDependency:self.accountLoggedInDependency];
384
385 [self.operationQueue addOperation: op];
386 return true;
387 }
388
389 - (void)cancelAllOperations {
390 [self.operationQueue cancelAllOperations];
391 }
392
393 - (void)waitUntilAllOperationsAreFinished {
394 [self.operationQueue waitUntilAllOperationsAreFinished];
395 }
396
397 - (void)waitForOperationsOfClass:(Class) operationClass {
398 NSArray* operations = [self.operationQueue.operations copy];
399 for(NSOperation* op in operations) {
400 if([op isKindOfClass:operationClass]) {
401 [op waitUntilFinished];
402 }
403 }
404 }
405
406 - (bool)scheduleAccountStatusOperation: (NSOperation*) op {
407 if(self.halted) {
408 ckkserror("ckkszone", self, "attempted to schedule an account operation on a halted zone, ignoring");
409 return false;
410 }
411
412 // Always succeed. But, account status operations should always proceed in-order.
413 [op linearDependencies:self.accountOperations];
414 [self.operationQueue addOperation: op];
415 return true;
416 }
417
418 // to be used rarely, if at all
419 - (bool)scheduleOperationWithoutDependencies:(NSOperation*)op {
420 if(self.halted) {
421 ckkserror("ckkszone", self, "attempted to schedule an non-dependent operation on a halted zone, ignoring");
422 return false;
423 }
424
425 [self.operationQueue addOperation: op];
426 return true;
427 }
428
429 - (void) dispatchSync: (bool (^)(void)) block {
430 // important enough to block this thread.
431 __block bool ok = false;
432 dispatch_sync(self.queue, ^{
433 if(self.halted) {
434 ckkserror("ckkszone", self, "CKKSZone not dispatchSyncing a block (due to being halted)");
435 return;
436 }
437
438 ok = block();
439 if(!ok) {
440 ckkserror("ckkszone", self, "CKKSZone block returned false");
441 }
442 });
443 }
444
445 - (void)halt {
446 // Synchronously set the 'halted' bit
447 dispatch_sync(self.queue, ^{
448 self.halted = true;
449 });
450
451 // Bring all operations down, too
452 [self cancelAllOperations];
453
454 // And now, wait for all operations that are running
455 for(NSOperation* op in self.operationQueue.operations) {
456 if(op.isExecuting) {
457 [op waitUntilFinished];
458 }
459 }
460 }
461
462 @end
463
464 #endif /* OCTAGON */
465