]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSKeychainView.m
Security-59306.101.1.tar.gz
[apple/security.git] / keychain / ckks / CKKSKeychainView.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 "CKKSKeychainView.h"
25
26
27
28 #if OCTAGON
29 #import "CloudKitDependencies.h"
30 #import <CloudKit/CloudKit.h>
31 #import <CloudKit/CloudKit_Private.h>
32 #endif
33
34 #import "CKKS.h"
35 #import "OctagonAPSReceiver.h"
36 #import "CKKSIncomingQueueEntry.h"
37 #import "CKKSOutgoingQueueEntry.h"
38 #import "CKKSCurrentKeyPointer.h"
39 #import "CKKSKey.h"
40 #import "CKKSMirrorEntry.h"
41 #import "CKKSZoneStateEntry.h"
42 #import "CKKSItemEncrypter.h"
43 #import "CKKSIncomingQueueOperation.h"
44 #import "CKKSNewTLKOperation.h"
45 #import "CKKSProcessReceivedKeysOperation.h"
46 #import "CKKSZone.h"
47 #import "CKKSFetchAllRecordZoneChangesOperation.h"
48 #import "CKKSHealKeyHierarchyOperation.h"
49 #import "CKKSReencryptOutgoingItemsOperation.h"
50 #import "CKKSScanLocalItemsOperation.h"
51 #import "CKKSSynchronizeOperation.h"
52 #import "CKKSRateLimiter.h"
53 #import "CKKSManifest.h"
54 #import "CKKSManifestLeafRecord.h"
55 #import "CKKSZoneChangeFetcher.h"
56 #import "CKKSAnalytics.h"
57 #import "keychain/analytics/CKKSLaunchSequence.h"
58 #import "keychain/ckks/CKKSCloudKitClassDependencies.h"
59 #import "keychain/ckks/CKKSDeviceStateEntry.h"
60 #import "keychain/ckks/CKKSNearFutureScheduler.h"
61 #import "keychain/ckks/CKKSCurrentItemPointer.h"
62 #import "keychain/ckks/CKKSUpdateCurrentItemPointerOperation.h"
63 #import "keychain/ckks/CKKSUpdateDeviceStateOperation.h"
64 #import "keychain/ckks/CKKSNotifier.h"
65 #import "keychain/ckks/CloudKitCategories.h"
66 #import "keychain/ckks/CKKSTLKShareRecord.h"
67 #import "keychain/ckks/CKKSHealTLKSharesOperation.h"
68 #import "keychain/ckks/CKKSLocalSynchronizeOperation.h"
69 #import "keychain/ckks/CKKSPeerProvider.h"
70 #import "keychain/categories/NSError+UsefulConstructors.h"
71
72 #import "keychain/ot/OTConstants.h"
73 #import "keychain/ot/OTDefines.h"
74 #import "keychain/ot/OctagonCKKSPeerAdapter.h"
75 #import "keychain/ot/ObjCImprovements.h"
76
77 #include <utilities/SecCFWrappers.h>
78 #include <utilities/SecTrace.h>
79 #include <utilities/SecDb.h>
80 #include "keychain/securityd/SecDbItem.h"
81 #include "keychain/securityd/SecItemDb.h"
82 #include "keychain/securityd/SecItemSchema.h"
83 #include "keychain/securityd/SecItemServer.h"
84 #include <utilities/debugging.h>
85 #include <Security/SecItemPriv.h>
86 #include "keychain/SecureObjectSync/SOSAccountTransaction.h"
87 #include <utilities/SecADWrapper.h>
88 #include <utilities/SecPLWrappers.h>
89 #include <os/transaction_private.h>
90
91 #if OCTAGON
92
93 @interface CKKSKeychainView()
94 @property bool keyStateFetchRequested;
95 @property bool keyStateProcessRequested;
96 @property bool trustedPeersSetChanged;
97
98 @property bool keyStateCloudKitDeleteRequested;
99 @property NSHashTable<CKKSResultOperation*>* cloudkitDeleteZoneOperations;
100
101 @property bool keyStateLocalResetRequested;
102 @property NSHashTable<CKKSResultOperation*>* localResetOperations;
103
104 @property bool tlkCreationRequested;
105 @property NSHashTable<CKKSResultOperation<CKKSKeySetProviderOperationProtocol>*>* keysetProviderOperations;
106
107
108 @property (atomic) NSString *activeTLK;
109
110 @property (readonly) Class<CKKSNotifier> notifierClass;
111
112 // Slows down all outgoing queue operations
113 @property CKKSNearFutureScheduler* outgoingQueueOperationScheduler;
114
115 @property CKKSResultOperation* processIncomingQueueAfterNextUnlockOperation;
116 @property CKKSResultOperation* resultsOfNextIncomingQueueOperationOperation;
117
118 // An extra queue for semaphore-waiting-based NSOperations
119 @property NSOperationQueue* waitingQueue;
120
121 // Scratch space for resyncs
122 @property (nullable) NSMutableSet<NSString*>* resyncRecordsSeen;
123
124 // Make these readwrite
125 @property NSArray<id<CKKSPeerProvider>>* currentPeerProviders;
126 @property NSArray<CKKSPeerProviderState*>* currentTrustStates;
127
128 @end
129 #endif
130
131 @implementation CKKSKeychainView
132 #if OCTAGON
133
134 - (instancetype)initWithContainer:(CKContainer*)container
135 zoneName:(NSString*)zoneName
136 accountTracker:(CKKSAccountStateTracker*)accountTracker
137 lockStateTracker:(CKKSLockStateTracker*)lockStateTracker
138 reachabilityTracker:(CKKSReachabilityTracker*)reachabilityTracker
139 changeFetcher:(CKKSZoneChangeFetcher*)fetcher
140 zoneModifier:(CKKSZoneModifier*)zoneModifier
141 savedTLKNotifier:(CKKSNearFutureScheduler*)savedTLKNotifier
142 cloudKitClassDependencies:(CKKSCloudKitClassDependencies*)cloudKitClassDependencies
143 {
144
145 if(self = [super initWithContainer:container
146 zoneName:zoneName
147 accountTracker:accountTracker
148 reachabilityTracker:reachabilityTracker
149 zoneModifier:zoneModifier
150 cloudKitClassDependencies:cloudKitClassDependencies]) {
151 WEAKIFY(self);
152
153 _loggedIn = [[CKKSCondition alloc] init];
154 _loggedOut = [[CKKSCondition alloc] init];
155 _accountStateKnown = [[CKKSCondition alloc] init];
156
157 _trustStatus = CKKSAccountStatusUnknown;
158 _trustDependency = [CKKSResultOperation named:@"wait-for-trust" withBlock:^{}];
159
160 _incomingQueueOperations = [NSHashTable weakObjectsHashTable];
161 _outgoingQueueOperations = [NSHashTable weakObjectsHashTable];
162 _scanLocalItemsOperations = [NSHashTable weakObjectsHashTable];
163 _cloudkitDeleteZoneOperations = [NSHashTable weakObjectsHashTable];
164 _localResetOperations = [NSHashTable weakObjectsHashTable];
165 _keysetProviderOperations = [NSHashTable weakObjectsHashTable];
166
167 _currentPeerProviders = @[];
168 _currentTrustStates = @[];
169
170 _launch = [[CKKSLaunchSequence alloc] initWithRocketName:@"com.apple.security.ckks.launch"];
171 [_launch addAttribute:@"view" value:zoneName];
172
173 _zoneChangeFetcher = fetcher;
174 [fetcher registerClient:self];
175
176 _resyncRecordsSeen = nil;
177
178 _notifierClass = cloudKitClassDependencies.notifierClass;
179 _notifyViewChangedScheduler = [[CKKSNearFutureScheduler alloc] initWithName:[NSString stringWithFormat: @"%@-notify-scheduler", self.zoneName]
180 initialDelay:250*NSEC_PER_MSEC
181 continuingDelay:1*NSEC_PER_SEC
182 keepProcessAlive:true
183 dependencyDescriptionCode:CKKSResultDescriptionPendingViewChangedScheduling
184 block:^{
185 STRONGIFY(self);
186 [self.notifierClass post:[NSString stringWithFormat:@"com.apple.security.view-change.%@", self.zoneName]];
187
188 // Ugly, but: the Manatee and Engram views need to send a fake 'PCS' view change.
189 // TODO: make this data-driven somehow
190 if([self.zoneName isEqualToString:@"Manatee"] ||
191 [self.zoneName isEqualToString:@"Engram"] ||
192 [self.zoneName isEqualToString:@"ApplePay"] ||
193 [self.zoneName isEqualToString:@"Home"] ||
194 [self.zoneName isEqualToString:@"LimitedPeersAllowed"]) {
195 [self.notifierClass post:@"com.apple.security.view-change.PCS"];
196 }
197 }];
198
199 _notifyViewReadyScheduler = [[CKKSNearFutureScheduler alloc] initWithName:[NSString stringWithFormat: @"%@-ready-scheduler", self.zoneName]
200 initialDelay:250*NSEC_PER_MSEC
201 continuingDelay:120*NSEC_PER_SEC
202 keepProcessAlive:true
203 dependencyDescriptionCode:CKKSResultDescriptionPendingViewChangedScheduling
204 block:^{
205 STRONGIFY(self);
206 NSDistributedNotificationCenter *center = [self.cloudKitClassDependencies.nsdistributednotificationCenterClass defaultCenter];
207
208 [center postNotificationName:@"com.apple.security.view-become-ready"
209 object:nil
210 userInfo:@{ @"view" : self.zoneName ?: @"unknown" }
211 options:0];
212 }];
213
214
215 _lockStateTracker = lockStateTracker;
216 _savedTLKNotifier = savedTLKNotifier;
217
218 _keyHierarchyConditions = [[NSMutableDictionary alloc] init];
219 [CKKSZoneKeyStateMap() enumerateKeysAndObjectsUsingBlock:^(CKKSZoneKeyState * _Nonnull key, NSNumber * _Nonnull obj, BOOL * _Nonnull stop) {
220 [self.keyHierarchyConditions setObject: [[CKKSCondition alloc] init] forKey:key];
221 }];
222
223 // Use the keyHierarchyState setter to modify the zone key state map
224 self.keyHierarchyState = SecCKKSZoneKeyStateLoggedOut;
225
226 _keyHierarchyError = nil;
227 _keyHierarchyOperationGroup = nil;
228 _keyStateMachineOperation = nil;
229 _keyStateFetchRequested = false;
230 _keyStateProcessRequested = false;
231 _tlkCreationRequested = false;
232
233 _waitingQueue = [[NSOperationQueue alloc] init];
234 _waitingQueue.maxConcurrentOperationCount = 5;
235
236 _keyStateReadyDependency = [self createKeyStateReadyDependency: @"Key state has become ready for the first time." ckoperationGroup:[CKOperationGroup CKKSGroupWithName:@"initial-key-state-ready-scan"]];
237
238 _keyStateNonTransientDependency = [self createKeyStateNontransientDependency];
239
240 dispatch_time_t initialOutgoingQueueDelay = SecCKKSReduceRateLimiting() ? NSEC_PER_MSEC * 200 : NSEC_PER_SEC * 1;
241 dispatch_time_t continuingOutgoingQueueDelay = SecCKKSReduceRateLimiting() ? NSEC_PER_MSEC * 200 : NSEC_PER_SEC * 30;
242 _outgoingQueueOperationScheduler = [[CKKSNearFutureScheduler alloc] initWithName:[NSString stringWithFormat: @"%@-outgoing-queue-scheduler", self.zoneName]
243 initialDelay:initialOutgoingQueueDelay
244 continuingDelay:continuingOutgoingQueueDelay
245 keepProcessAlive:false
246 dependencyDescriptionCode:CKKSResultDescriptionPendingOutgoingQueueScheduling
247 block:^{}];
248
249
250 dispatch_time_t initialKeyHierachyPokeDelay = SecCKKSReduceRateLimiting() ? NSEC_PER_MSEC * 100 : NSEC_PER_MSEC * 500;
251 dispatch_time_t continuingKeyHierachyPokeDelay = SecCKKSReduceRateLimiting() ? NSEC_PER_MSEC * 200 : NSEC_PER_SEC * 5;
252 _pokeKeyStateMachineScheduler = [[CKKSNearFutureScheduler alloc] initWithName:[NSString stringWithFormat: @"%@-reprocess-scheduler", self.zoneName]
253 initialDelay:initialKeyHierachyPokeDelay
254 continuingDelay:continuingKeyHierachyPokeDelay
255 keepProcessAlive:true
256 dependencyDescriptionCode:CKKSResultDescriptionPendingKeyHierachyPokeScheduling
257 block:^{
258 STRONGIFY(self);
259 [self dispatchSyncWithAccountKeys: ^bool{
260 STRONGIFY(self);
261
262 [self _onqueueAdvanceKeyStateMachineToState:nil withError:nil];
263 return true;
264 }];
265 }];
266 }
267 return self;
268 }
269
270 - (NSString*)description {
271 return [NSString stringWithFormat:@"<%@: %@ (%@)>", NSStringFromClass([self class]), self.zoneName, self.keyHierarchyState];
272 }
273
274 - (NSString*)debugDescription {
275 return [NSString stringWithFormat:@"<%@: %@ (%@) %p>", NSStringFromClass([self class]), self.zoneName, self.keyHierarchyState, self];
276 }
277
278 - (CKKSZoneKeyState*)keyHierarchyState {
279 return _keyHierarchyState;
280 }
281
282 - (void)setKeyHierarchyState:(CKKSZoneKeyState *)keyHierarchyState {
283 if((keyHierarchyState == nil && _keyHierarchyState == nil) || ([keyHierarchyState isEqualToString:_keyHierarchyState])) {
284 // No change, do nothing.
285 } else {
286 // Fixup the condition variables as part of setting this state
287 if(_keyHierarchyState) {
288 self.keyHierarchyConditions[_keyHierarchyState] = [[CKKSCondition alloc] init];
289 }
290
291 _keyHierarchyState = keyHierarchyState;
292
293 if(keyHierarchyState) {
294 [self.keyHierarchyConditions[keyHierarchyState] fulfill];
295 }
296 }
297 }
298
299 - (NSString *)lastActiveTLKUUID
300 {
301 return self.activeTLK;
302 }
303
304 - (void)_onqueueResetSetup:(CKKSZoneKeyState*)newState resetMessage:(NSString*)resetMessage ckoperationGroup:(CKOperationGroup*)group {
305 [super resetSetup];
306
307 self.keyHierarchyState = newState;
308 self.keyHierarchyError = nil;
309
310 [self.keyStateMachineOperation cancel];
311 self.keyStateMachineOperation = nil;
312
313 self.keyStateFetchRequested = false;
314 self.keyStateProcessRequested = false;
315
316 self.keyHierarchyOperationGroup = group;
317
318 [self ensureKeyStateReadyDependency:resetMessage];
319
320 NSOperation* oldKSNTD = self.keyStateNonTransientDependency;
321 self.keyStateNonTransientDependency = [self createKeyStateNontransientDependency];
322 if(oldKSNTD) {
323 [oldKSNTD addDependency:self.keyStateNonTransientDependency];
324 [self.waitingQueue addOperation:oldKSNTD];
325 }
326 }
327
328 - (void)ensureKeyStateReadyDependency:(NSString*)resetMessage {
329 NSOperation* oldKSRD = self.keyStateReadyDependency;
330 self.keyStateReadyDependency = [self createKeyStateReadyDependency:resetMessage ckoperationGroup:self.keyHierarchyOperationGroup];
331 if(oldKSRD) {
332 [oldKSRD addDependency:self.keyStateReadyDependency];
333 [self.waitingQueue addOperation:oldKSRD];
334 }
335 }
336
337 - (CKKSResultOperation*)createPendingInitializationOperation {
338
339 WEAKIFY(self);
340 CKKSResultOperation* initializationOp = [CKKSGroupOperation named:@"view-initialization" withBlockTakingSelf:^(CKKSGroupOperation * _Nonnull strongOp) {
341 STRONGIFY(self);
342
343 __block CKKSResultOperation* zoneCreationOperation = nil;
344 [self dispatchSync:^bool {
345 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state: self.zoneName];
346 zoneCreationOperation = [self handleCKLogin:ckse.ckzonecreated zoneSubscribed:ckse.ckzonesubscribed];
347 return true;
348 }];
349
350 CKKSResultOperation* viewInitializationOperation = [CKKSResultOperation named:@"view-initialization" withBlockTakingSelf:^(CKKSResultOperation * _Nonnull strongInternalOp) {
351 STRONGIFY(self);
352 if(!self) {
353 ckkserror("ckks", self, "received callback for released object");
354 return;
355 }
356
357 [self dispatchSyncWithAccountKeys: ^bool {
358 ckksnotice("ckks", self, "Zone setup progress: %@ %d %@ %d %@",
359 [CKKSAccountStateTracker stringFromAccountStatus:self.accountStatus],
360 self.zoneCreated, self.zoneCreatedError, self.zoneSubscribed, self.zoneSubscribedError);
361
362 NSError* error = nil;
363 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state: self.zoneName];
364 ckse.ckzonecreated = self.zoneCreated;
365 ckse.ckzonesubscribed = self.zoneSubscribed;
366
367 // Although, if the zone subscribed error says there's no zone, mark down that there's no zone
368 if(self.zoneSubscribedError &&
369 [self.zoneSubscribedError.domain isEqualToString:CKErrorDomain] && self.zoneSubscribedError.code == CKErrorPartialFailure) {
370 NSError* subscriptionError = self.zoneSubscribedError.userInfo[CKPartialErrorsByItemIDKey][self.zoneID];
371 if(subscriptionError && [subscriptionError.domain isEqualToString:CKErrorDomain] && subscriptionError.code == CKErrorZoneNotFound) {
372
373 ckkserror("ckks", self, "zone subscription error appears to say the zone doesn't exist, fixing status: %@", self.zoneSubscribedError);
374 ckse.ckzonecreated = false;
375 }
376 }
377
378 [ckse saveToDatabase: &error];
379 if(error) {
380 ckkserror("ckks", self, "couldn't save zone creation status for %@: %@", self.zoneName, error);
381 }
382
383 if(!self.zoneCreated || !self.zoneSubscribed) {
384 // Go into 'zonecreationfailed'
385 strongInternalOp.error = self.zoneCreatedError ? self.zoneCreatedError : self.zoneSubscribedError;
386 [self _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateZoneCreationFailed withError:strongInternalOp.error];
387
388 return true;
389 } else {
390 [self _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateInitialized withError:nil];
391 }
392
393 return true;
394 }];
395 }];
396
397 [viewInitializationOperation addDependency:zoneCreationOperation];
398 [strongOp runBeforeGroupFinished:viewInitializationOperation];
399 }];
400
401 return initializationOp;
402 }
403
404 - (void)_onqueuePerformKeyStateInitialized:(CKKSZoneStateEntry*)ckse {
405 CKKSOutgoingQueueOperation* outgoingOperation = nil;
406 NSOperation* initialProcess = nil;
407
408 // Check if we believe we've synced this zone before.
409 if(ckse.changeToken == nil) {
410 self.keyHierarchyOperationGroup = [CKOperationGroup CKKSGroupWithName:@"initial-setup"];
411
412 ckksnotice("ckks", self, "No existing change token; going to try to match local items with CloudKit ones.");
413
414 // Onboard this keychain: there's likely items in it that we haven't synced yet.
415 // But, there might be items in The Cloud that correspond to these items, with UUIDs that we don't know yet.
416 // First, fetch all remote items.
417 CKKSResultOperation* fetch = [self.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseInitialStart];
418 fetch.name = @"initial-fetch";
419
420 // Next, try to process them (replacing local entries)
421 initialProcess = [self processIncomingQueue:true after:fetch];
422 initialProcess.name = @"initial-process-incoming-queue";
423
424 // If all that succeeds, iterate through all keychain items and find the ones which need to be uploaded
425 self.initialScanOperation = [self scanLocalItems:@"initial-scan-operation"
426 ckoperationGroup:self.keyHierarchyOperationGroup
427 after:initialProcess];
428
429 } else {
430 // Likely a restart of securityd!
431
432 // First off, are there any in-flight queue entries? If so, put them back into New.
433 // If they're truly in-flight, we'll "conflict" with ourselves, but that should be fine.
434 NSError* error = nil;
435 [self _onqueueResetAllInflightOQE:&error];
436 if(error) {
437 ckkserror("ckks", self, "Couldn't reset in-flight OQEs, bad behavior ahead: %@", error);
438 }
439
440 // Are there any fixups to run first?
441 self.lastFixupOperation = [CKKSFixups fixup:ckse.lastFixup for:self];
442 if(self.lastFixupOperation) {
443 ckksnotice("ckksfixup", self, "We have a fixup to perform: %@", self.lastFixupOperation);
444 [self scheduleOperation:self.lastFixupOperation];
445 }
446
447 self.keyHierarchyOperationGroup = [CKOperationGroup CKKSGroupWithName:@"restart-setup"];
448
449 if ([CKKSManifest shouldSyncManifests]) {
450 self.egoManifest = [CKKSEgoManifest tryCurrentEgoManifestForZone:self.zoneName];
451 }
452
453 // If it's been more than 24 hours since the last fetch, fetch and process everything.
454 // Or, if we think we were interrupted in the middle of fetching, fetch some more.
455 // Otherwise, just kick off the local queue processing.
456
457 NSDate* now = [NSDate date];
458 NSDateComponents* offset = [[NSDateComponents alloc] init];
459 [offset setHour:-24];
460 NSDate* deadline = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:now options:0];
461
462 if(ckse.lastFetchTime == nil ||
463 [ckse.lastFetchTime compare: deadline] == NSOrderedAscending ||
464 ckse.moreRecordsInCloudKit) {
465 initialProcess = [self fetchAndProcessCKChanges:CKKSFetchBecauseSecuritydRestart after:self.lastFixupOperation];
466
467 // Also, kick off a scan local items: it'll find any out-of-sync issues in the local keychain
468 self.initialScanOperation = [self scanLocalItems:@"24-hr-scan-operation"
469 ckoperationGroup:self.keyHierarchyOperationGroup
470 after:initialProcess];
471 } else {
472 initialProcess = [self processIncomingQueue:false after:self.lastFixupOperation];
473 }
474
475 if([CKKSManifest shouldSyncManifests]) {
476 if (!self.egoManifest && !self.initialScanOperation) {
477 ckksnotice("ckksmanifest", self, "No ego manifest on restart; rescanning");
478 self.initialScanOperation = [self scanLocalItems:@"initial-scan-operation"
479 ckoperationGroup:self.keyHierarchyOperationGroup
480 after:initialProcess];
481 }
482 }
483
484 // Process outgoing queue after re-start
485 outgoingOperation = [self processOutgoingQueueAfter:self.lastFixupOperation ckoperationGroup:self.keyHierarchyOperationGroup];
486 }
487
488 /*
489 * Launch time is determined by when the zone have:
490 * 1. keystate have become ready
491 * 2. scan local items (if needed)
492 * 3. processed all outgoing item (if needed)
493 */
494
495 WEAKIFY(self);
496 NSBlockOperation *seemReady = [NSBlockOperation named:[NSString stringWithFormat:@"seemsReadyForSyncing-%@", self.zoneName] withBlock:^void{
497 STRONGIFY(self);
498 NSError *error = nil;
499 ckksnotice("launch", self, "Launch complete");
500 NSNumber *zoneSize = [CKKSMirrorEntry counts:self.zoneID error:&error];
501 if (zoneSize) {
502 zoneSize = @(SecBucket1Significant([zoneSize longValue]));
503 [self.launch addAttribute:@"zonesize" value:zoneSize];
504 }
505 [self.launch launch];
506
507 /*
508 * Since we think we are ready, signal to CK that its to check for PCS identities again, and create the
509 * since before we completed this operation, we would probably have failed with a timeout because
510 * we where busy downloading items from CloudKit and then processing them.
511 */
512 [self.notifyViewReadyScheduler trigger];
513 }];
514
515 [seemReady addNullableDependency:self.keyStateReadyDependency];
516 [seemReady addNullableDependency:outgoingOperation];
517 [seemReady addNullableDependency:self.initialScanOperation];
518 [seemReady addNullableDependency:initialProcess];
519
520 [self scheduleOperation: seemReady];
521 }
522
523 - (bool)_onqueueResetLocalData: (NSError * __autoreleasing *) error {
524 dispatch_assert_queue(self.queue);
525
526 NSError* localerror = nil;
527 bool setError = false; // Ugly, but this is the only way to return the first error given
528
529 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state: self.zoneName];
530 ckse.ckzonecreated = false;
531 ckse.ckzonesubscribed = false; // I'm actually not sure about this: can you be subscribed to a non-existent zone?
532 ckse.changeToken = NULL;
533 [ckse saveToDatabase: &localerror];
534 if(localerror) {
535 ckkserror("ckks", self, "couldn't reset zone status for %@: %@", self.zoneName, localerror);
536 if(error && !setError) {
537 *error = localerror; setError = true;
538 }
539 }
540
541 [CKKSMirrorEntry deleteAll:self.zoneID error: &localerror];
542 if(localerror) {
543 ckkserror("ckks", self, "couldn't delete all CKKSMirrorEntry: %@", localerror);
544 if(error && !setError) {
545 *error = localerror; setError = true;
546 }
547 }
548
549 [CKKSOutgoingQueueEntry deleteAll:self.zoneID error: &localerror];
550 if(localerror) {
551 ckkserror("ckks", self, "couldn't delete all CKKSOutgoingQueueEntry: %@", localerror);
552 if(error && !setError) {
553 *error = localerror; setError = true;
554 }
555 }
556
557 [CKKSIncomingQueueEntry deleteAll:self.zoneID error: &localerror];
558 if(localerror) {
559 ckkserror("ckks", self, "couldn't delete all CKKSIncomingQueueEntry: %@", localerror);
560 if(error && !setError) {
561 *error = localerror; setError = true;
562 }
563 }
564
565 [CKKSKey deleteAll:self.zoneID error: &localerror];
566 if(localerror) {
567 ckkserror("ckks", self, "couldn't delete all CKKSKey: %@", localerror);
568 if(error && !setError) {
569 *error = localerror; setError = true;
570 }
571 }
572
573 [CKKSTLKShareRecord deleteAll:self.zoneID error: &localerror];
574 if(localerror) {
575 ckkserror("ckks", self, "couldn't delete all CKKSTLKShare: %@", localerror);
576 if(error && !setError) {
577 *error = localerror; setError = true;
578 }
579 }
580
581 [CKKSCurrentKeyPointer deleteAll:self.zoneID error: &localerror];
582 if(localerror) {
583 ckkserror("ckks", self, "couldn't delete all CKKSCurrentKeyPointer: %@", localerror);
584 if(error && !setError) {
585 *error = localerror; setError = true;
586 }
587 }
588
589 [CKKSCurrentItemPointer deleteAll:self.zoneID error: &localerror];
590 if(localerror) {
591 ckkserror("ckks", self, "couldn't delete all CKKSCurrentItemPointer: %@", localerror);
592 if(error && !setError) {
593 *error = localerror; setError = true;
594 }
595 }
596
597 [CKKSDeviceStateEntry deleteAll:self.zoneID error:&localerror];
598 if(localerror) {
599 ckkserror("ckks", self, "couldn't delete all CKKSDeviceStateEntry: %@", localerror);
600 if(error && !setError) {
601 *error = localerror; setError = true;
602 }
603 }
604
605 return (localerror == nil && !setError);
606 }
607
608 - (CKKSResultOperation*)createPendingResetLocalDataOperation {
609 @synchronized(self.localResetOperations) {
610 CKKSResultOperation* pendingResetLocalOperation = (CKKSResultOperation*) [self findFirstPendingOperation:self.localResetOperations];
611 if(!pendingResetLocalOperation) {
612 WEAKIFY(self);
613 pendingResetLocalOperation = [CKKSResultOperation named:@"reset-local" withBlockTakingSelf:^(CKKSResultOperation * _Nonnull strongOp) {
614 STRONGIFY(self);
615 __block NSError* error = nil;
616
617 [self dispatchSync: ^bool{
618 [self _onqueueResetLocalData: &error];
619 return true;
620 }];
621
622 strongOp.error = error;
623 }];
624 [pendingResetLocalOperation linearDependencies:self.localResetOperations];
625 }
626 return pendingResetLocalOperation;
627 }
628 }
629
630 - (CKKSResultOperation*)resetLocalData {
631 // Not overly thread-safe, but a single read is okay
632 CKKSAccountStatus accountStatus = self.accountStatus;
633 ckksnotice("ckksreset", self, "Requesting local data reset");
634
635 // If we're currently signed in, the reset operation will be handled by the CKKS key state machine, and a reset should end up in 'ready'
636 if(accountStatus == CKKSAccountStatusAvailable) {
637 WEAKIFY(self);
638 CKKSGroupOperation* resetOperationGroup = [CKKSGroupOperation named:@"local-reset" withBlockTakingSelf:^(CKKSGroupOperation *strongOp) {
639 STRONGIFY(self);
640
641 __block CKKSResultOperation* resetOperation = nil;
642
643 [self dispatchSyncWithAccountKeys:^bool {
644 self.keyStateLocalResetRequested = true;
645 resetOperation = [self createPendingResetLocalDataOperation];
646 [self _onqueueAdvanceKeyStateMachineToState:nil withError:nil];
647 return true;
648 }];
649
650 [strongOp dependOnBeforeGroupFinished:resetOperation];
651 }];
652 [self scheduleOperationWithoutDependencies:resetOperationGroup];
653
654 CKKSGroupOperation* viewReset = [CKKSGroupOperation named:@"local-data-reset" withBlockTakingSelf:^(CKKSGroupOperation *strongOp) {
655 STRONGIFY(self);
656 // Now that the local reset finished, wait for the key hierarchy state machine to churn
657 ckksnotice("ckksreset", self, "waiting for key hierarchy to become nontransient (after local reset)");
658 CKKSResultOperation* waitOp = [CKKSResultOperation named:@"waiting-for-local-reset" withBlock:^{}];
659 [waitOp timeout: 60*NSEC_PER_SEC];
660 [waitOp addNullableDependency:self.keyStateNonTransientDependency];
661
662 [strongOp runBeforeGroupFinished:waitOp];
663 }];
664 [viewReset addSuccessDependency:resetOperationGroup];
665
666 [self scheduleOperationWithoutDependencies:viewReset];
667 return viewReset;
668 } else {
669 // Since we're logged out, we must run the reset ourselves
670 WEAKIFY(self);
671 CKKSResultOperation* pendingResetLocalOperation = [CKKSResultOperation named:@"reset-local"
672 withBlockTakingSelf:^(CKKSResultOperation * _Nonnull strongOp) {
673 STRONGIFY(self);
674 __block NSError* error = nil;
675
676 [self dispatchSync: ^bool{
677 [self _onqueueResetLocalData: &error];
678 return true;
679 }];
680
681 strongOp.error = error;
682 }];
683 [self scheduleOperationWithoutDependencies:pendingResetLocalOperation];
684 return pendingResetLocalOperation;
685 }
686 }
687
688 - (CKKSResultOperation*)createPendingDeleteZoneOperation:(CKOperationGroup*)operationGroup {
689 @synchronized(self.cloudkitDeleteZoneOperations) {
690 CKKSResultOperation* pendingDeleteOperation = (CKKSResultOperation*) [self findFirstPendingOperation:self.cloudkitDeleteZoneOperations];
691 if(!pendingDeleteOperation) {
692 pendingDeleteOperation = [self deleteCloudKitZoneOperation:operationGroup];
693 [pendingDeleteOperation linearDependencies:self.cloudkitDeleteZoneOperations];
694 }
695 return pendingDeleteOperation;
696 }
697 }
698
699 - (CKKSResultOperation*)resetCloudKitZone:(CKOperationGroup*)operationGroup {
700 [self.accountStateKnown wait:(SecCKKSTestsEnabled() ? 1*NSEC_PER_SEC : 10*NSEC_PER_SEC)];
701
702 // Not overly thread-safe, but a single read is okay
703 if(self.accountStatus != CKKSAccountStatusAvailable) {
704 // No CK account? goodbye!
705 ckksnotice("ckksreset", self, "Requesting reset of CK zone, but no CK account exists");
706 CKKSResultOperation* errorOp = [CKKSResultOperation named:@"fail" withBlockTakingSelf:^(CKKSResultOperation * _Nonnull op) {
707 op.error = [NSError errorWithDomain:CKKSErrorDomain
708 code:CKKSNotLoggedIn
709 description:@"User is not signed into iCloud."];
710 }];
711
712 [self scheduleOperationWithoutDependencies:errorOp];
713 return errorOp;
714 }
715
716 // Actually running the delete operation will be handled by the CKKS key state machine
717 ckksnotice("ckksreset", self, "Requesting reset of CK zone (logged in)");
718
719 __block CKKSResultOperation* deleteOperation = nil;
720 [self dispatchSyncWithAccountKeys:^bool {
721 self.keyStateCloudKitDeleteRequested = true;
722 deleteOperation = [self createPendingDeleteZoneOperation:operationGroup];
723 [self _onqueueAdvanceKeyStateMachineToState:nil withError:nil];
724 return true;
725 }];
726
727 WEAKIFY(self);
728 CKKSGroupOperation* viewReset = [CKKSGroupOperation named:[NSString stringWithFormat:@"cloudkit-view-reset-%@", self.zoneName]
729 withBlockTakingSelf:^(CKKSGroupOperation *strongOp) {
730 STRONGIFY(self);
731 // Now that the delete finished, wait for the key hierarchy state machine
732 ckksnotice("ckksreset", self, "waiting for key hierarchy to become nontransient (after cloudkit reset)");
733 CKKSResultOperation* waitOp = [CKKSResultOperation named:@"waiting-for-reset" withBlock:^{}];
734 [waitOp timeout: 60*NSEC_PER_SEC];
735 [waitOp addNullableDependency:self.keyStateNonTransientDependency];
736
737 [strongOp runBeforeGroupFinished:waitOp];
738 }];
739
740 [viewReset timeout:30*NSEC_PER_SEC];
741 [viewReset addDependency:deleteOperation];
742 [self.waitingQueue addOperation:viewReset];
743
744 return viewReset;
745 }
746
747 - (void)_onqueueKeyStateMachineRequestFetch {
748 dispatch_assert_queue(self.queue);
749
750 // We're going to set this flag, then nudge the key state machine.
751 // If it was idle, then it should launch a fetch. If there was an active process, this flag will stay high
752 // and the fetch will be launched later.
753
754 self.keyStateFetchRequested = true;
755 [self _onqueueAdvanceKeyStateMachineToState: nil withError: nil];
756 }
757
758 - (void)keyStateMachineRequestProcess {
759 // Since bools are atomic, we don't need to get on-queue here
760 // Just set the flag high and hope
761 self.keyStateProcessRequested = true;
762 [self.pokeKeyStateMachineScheduler trigger];
763 }
764
765 - (void)_onqueueKeyStateMachineRequestProcess {
766 dispatch_assert_queue(self.queue);
767
768 // Set the request flag, then nudge the key state machine.
769 // If it was idle, then it should launch a process. If there was an active process, this flag will stay high
770 // and the process will be launched later.
771
772 self.keyStateProcessRequested = true;
773 [self _onqueueAdvanceKeyStateMachineToState: nil withError: nil];
774 }
775
776 - (CKKSResultOperation*)createKeyStateReadyDependency:(NSString*)message ckoperationGroup:(CKOperationGroup*)group {
777 WEAKIFY(self);
778 CKKSResultOperation* keyStateReadyDependency = [CKKSResultOperation operationWithBlock:^{
779 STRONGIFY(self);
780 if(!self) {
781 return;
782 }
783 ckksnotice("ckkskey", self, "%@", message);
784
785 [self dispatchSync:^bool {
786 if(self.droppedItems) {
787 // While we weren't in 'ready', keychain modifications might have come in and were dropped on the floor. Find them!
788 ckksnotice("ckkskey", self, "Launching scan operation for missed items");
789 [self scanLocalItems:@"ready-again-scan" ckoperationGroup:group after:nil];
790 }
791 return true;
792 }];
793 }];
794 keyStateReadyDependency.name = [NSString stringWithFormat: @"%@-key-state-ready", self.zoneName];
795 keyStateReadyDependency.descriptionErrorCode = CKKSResultDescriptionPendingKeyReady;
796 return keyStateReadyDependency;
797 }
798
799 - (CKKSResultOperation*)createKeyStateNontransientDependency {
800 WEAKIFY(self);
801 return [CKKSResultOperation named:[NSString stringWithFormat: @"%@-key-state-nontransient", self.zoneName] withBlock:^{
802 STRONGIFY(self);
803 ckksnotice("ckkskey", self, "Key state is now non-transient");
804 }];
805 }
806
807 // The operations suggested by this state machine should call _onqueueAdvanceKeyStateMachineToState once they are complete.
808 // At no other time should keyHierarchyState be modified.
809
810 // Note that this function cannot rely on doing any database work; it might get rolled back, especially in an error state
811 - (void)_onqueueAdvanceKeyStateMachineToState: (CKKSZoneKeyState*) state withError: (NSError*) error {
812 dispatch_assert_queue(self.queue);
813 WEAKIFY(self);
814
815 // Resetting back to 'loggedout' takes all precedence.
816 if([state isEqual:SecCKKSZoneKeyStateLoggedOut]) {
817 ckksnotice("ckkskey", self, "Resetting the key hierarchy state machine back to '%@'", state);
818
819 [self _onqueueResetSetup:SecCKKSZoneKeyStateLoggedOut
820 resetMessage:@"Key state has become ready for the first time (after reset)."
821 ckoperationGroup:[CKOperationGroup CKKSGroupWithName:@"key-state-after-logout"]];
822
823 [self _onqueueHandleKeyStateNonTransientDependency:nil];
824 self.launch = nil;
825 return;
826 }
827
828 [self.launch addEvent:state];
829
830 // Resetting back to 'initialized' also takes precedence
831 if([state isEqual:SecCKKSZoneKeyStateInitializing]) {
832 ckksnotice("ckkskey", self, "Resetting the key hierarchy state machine back to '%@'", state);
833
834 [self _onqueueResetSetup:SecCKKSZoneKeyStateInitializing
835 resetMessage:@"Key state has become ready for the first time (after re-initializing)."
836 ckoperationGroup:[CKOperationGroup CKKSGroupWithName:@"key-state-reset-to-initializing"]];
837
838 // Begin initialization, but rate-limit it
839 self.keyStateMachineOperation = [self createPendingInitializationOperation];
840 [self.keyStateMachineOperation addNullableDependency:self.zoneModifier.cloudkitRetryAfter.operationDependency];
841 [self.zoneModifier.cloudkitRetryAfter trigger];
842 [self scheduleOperation:self.keyStateMachineOperation];
843
844 [self _onqueueHandleKeyStateNonTransientDependency:nil];
845 return;
846 }
847
848 // Resetting to 'waitfortrust' also takes precedence
849 if([state isEqualToString:SecCKKSZoneKeyStateWaitForTrust]) {
850 if([self.keyHierarchyState isEqualToString:SecCKKSZoneKeyStateLoggedOut]) {
851 ckksnotice("ckks", self, "Asked to waitfortrust, but we're already in loggedout. Ignoring...");
852 return;
853 }
854
855 ckksnotice("ckks", self, "Entering waitfortrust");
856 self.keyHierarchyState = SecCKKSZoneKeyStateWaitForTrust;
857 self.keyHierarchyError = nil;
858 self.keyStateMachineOperation = nil;
859
860 [self ensureKeyStateReadyDependency:@"Key state has become ready for the first time (after lacking trust)."];
861
862 if(self.trustStatus == CKKSAccountStatusAvailable) {
863 // Note: we go to initialized here, since to enter waitfortrust CKKS has already gone through initializing
864 // initialized should refetch only if needed.
865 ckksnotice("ckks", self, "CKKS is trusted, moving to initialized");
866 self.keyStateMachineOperation = [self operationToEnterState:SecCKKSZoneKeyStateInitialized
867 keyStateError:nil
868 named:@"re-enter initialized"];
869 [self scheduleOperation:self.keyStateMachineOperation];
870 }
871
872 // In wait for trust, we might have a keyset. Who knows!
873 CKKSCurrentKeySet* keyset = [CKKSCurrentKeySet loadForZone:self.zoneID];
874 [self _onqueueHandleKeyStateNonTransientDependency:keyset];
875
876 return;
877 }
878
879 // Cancels and error states take precedence
880 if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateError] ||
881 [self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateCancelled] ||
882 self.keyHierarchyError != nil) {
883 // Error state: nowhere to go. Early-exit.
884 ckkserror("ckkskey", self, "Asked to advance state machine from non-exit state %@ (to %@): %@", self.keyHierarchyState, state, self.keyHierarchyError);
885 return;
886 }
887
888 if([state isEqual: SecCKKSZoneKeyStateError]) {
889 // But wait! Is this a "we're locked" error?
890 if(error && [self.lockStateTracker isLockedError:error]) {
891 ckkserror("ckkskey", self, "advised of 'keychain locked' error, ignoring: coming from state (%@): %@", self.keyHierarchyState, error);
892 // After the next unlock, fake that we received the last zone transition
893 CKKSZoneKeyState* lastState = self.keyHierarchyState;
894 self.keyStateMachineOperation = [NSBlockOperation named:@"key-state-after-unlock" withBlock:^{
895 STRONGIFY(self);
896 if(!self) {
897 return;
898 }
899 [self dispatchSyncWithAccountKeys:^bool{
900 [self _onqueueAdvanceKeyStateMachineToState:lastState withError:nil];
901 return true;
902 }];
903 }];
904 state = nil;
905
906 self.keyHierarchyState = SecCKKSZoneKeyStateWaitForUnlock;
907
908 [self.keyStateMachineOperation addNullableDependency:self.lockStateTracker.unlockDependency];
909 [self scheduleOperation:self.keyStateMachineOperation];
910
911 [self _onqueueHandleKeyStateNonTransientDependency:nil];
912 return;
913
914 } else {
915 // Error state: record the error and exit early
916 ckkserror("ckkskey", self, "advised of error: coming from state (%@): %@", self.keyHierarchyState, error);
917
918 [[CKKSAnalytics logger] logUnrecoverableError:error
919 forEvent:CKKSEventStateError
920 inView:self
921 withAttributes:@{ @"previousKeyHierarchyState" : self.keyHierarchyState }];
922
923
924 self.keyHierarchyState = SecCKKSZoneKeyStateError;
925 self.keyHierarchyError = error;
926
927 [self _onqueueHandleKeyStateNonTransientDependency:nil];
928 return;
929 }
930 }
931
932 if([state isEqual: SecCKKSZoneKeyStateCancelled]) {
933 ckkserror("ckkskey", self, "advised of cancel: coming from state (%@): %@", self.keyHierarchyState, error);
934 self.keyHierarchyState = SecCKKSZoneKeyStateCancelled;
935 self.keyHierarchyError = error;
936
937 // Cancel the key ready dependency. Strictly Speaking, this will cause errors down the line, but we're in a cancel state: those operations should be canceled anyway.
938 self.keyHierarchyOperationGroup = nil;
939 [self.keyStateReadyDependency cancel];
940 self.keyStateReadyDependency = nil;
941
942 [self.keyStateNonTransientDependency cancel];
943 self.keyStateNonTransientDependency = nil;
944 return;
945 }
946
947 // Now that the current or new state isn't an error or a cancel, proceed.
948 if(self.keyStateMachineOperation && ![self.keyStateMachineOperation isFinished]) {
949 if(state == nil) {
950 // we started this operation to move the state machine. Since you aren't asking for a state transition, and there's an active operation, no need to do anything
951 ckksnotice("ckkskey", self, "Not advancing state machine: waiting for %@", self.keyStateMachineOperation);
952 return;
953 }
954 }
955
956 if(state) {
957 ckksnotice("ckkskey", self, "Preparing to advance key hierarchy state machine from %@ to %@", self.keyHierarchyState, state);
958 self.keyStateMachineOperation = nil;
959 } else {
960 ckksnotice("ckkskey", self, "Key hierarchy state machine is being poked; currently %@", self.keyHierarchyState);
961 state = self.keyHierarchyState;
962 }
963
964 #if DEBUG
965 // During testing, keep the developer honest: this function should always have the self identities, unless the account has lost trust
966 // But, beginTrustedOperation is currently racy: if it acquires the queue and sets the trust bit between fetching the trust states and getting on the queue,
967 // this will fire. So, release SecCKKSZoneKeyStateInitialized from this check.
968 if(self.trustStatus == CKKSAccountStatusAvailable
969 && ![state isEqualToString:SecCKKSZoneKeyStateLoggedOut]
970 && ![state isEqualToString:SecCKKSZoneKeyStateInitialized]) {
971 bool hasSelfIdentities = false;
972 NSAssert(self.currentTrustStates.count > 0, @"Should have at least one trust state");
973 for(CKKSPeerProviderState* state in self.currentTrustStates) {
974 if(state.currentSelfPeersError == nil || state.currentSelfPeersError.code != CKKSNoPeersAvailable) {
975 hasSelfIdentities = true;
976 }
977 }
978
979 NSAssert(hasSelfIdentities, @"Must have viable (or errored) self peers to advance key state");
980 }
981 #endif
982
983 // Do any of these state transitions below want to change which state we're in?
984 CKKSZoneKeyState* nextState = nil;
985 NSError* nextError = nil;
986
987 // Any state that wants should fill this in; it'll be used at the end of this function as well
988 CKKSCurrentKeySet* keyset = nil;
989
990 #if !defined(NDEBUG)
991 {
992 NSError* localerror = nil;
993 NSError* allKeysError = nil;
994 NSArray<CKKSKey*>* allKeys = [CKKSKey allKeys:self.zoneID error:&allKeysError];
995
996 if(localerror) {
997 ckkserror("ckkskey", self, "couldn't fetch all keys from local database, entering error state: %@", allKeysError);
998 }
999 ckksdebug("ckkskey", self, "All keys: %@", allKeys);
1000 }
1001 #endif
1002
1003 NSError* hierarchyError = nil;
1004
1005 if(self.keyStateCloudKitDeleteRequested || [state isEqualToString:SecCKKSZoneKeyStateResettingZone]) {
1006 // CloudKit reset requests take precedence over all other state transitions
1007 ckksnotice("ckkskey", self, "Deleting the CloudKit Zone");
1008 CKKSGroupOperation* op = [[CKKSGroupOperation alloc] init];
1009
1010 CKKSResultOperation* deleteOp = [self createPendingDeleteZoneOperation:self.keyHierarchyOperationGroup];
1011 [op runBeforeGroupFinished: deleteOp];
1012
1013 NSOperation* nextStateOp = [CKKSResultOperation named:@"inspect-zone-delete" withBlockTakingSelf:^(CKKSResultOperation * _Nonnull op) {
1014 STRONGIFY(self);
1015 [self dispatchSyncWithAccountKeys:^bool {
1016 // Did the delete op succeed?
1017 if(deleteOp.error == nil) {
1018 ckksnotice("ckkskey", self, "Zone deletion operation complete! Proceeding to reset local data");
1019 [self _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateResettingLocalData withError:nil];
1020 return true;
1021 }
1022
1023 ckksnotice("ckkskey", self, "Zone deletion operation failed, will retry: %@", deleteOp.error);
1024 [self _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateResettingZone withError:nil];
1025
1026 return true;
1027 }];
1028 }];
1029
1030 [nextStateOp addDependency:deleteOp];
1031 [op runBeforeGroupFinished:nextStateOp];
1032
1033 self.keyStateMachineOperation = op;
1034 self.keyStateCloudKitDeleteRequested = false;
1035
1036 // Also, pending operations should be cancelled
1037 [self cancelPendingOperations];
1038
1039 } else if(self.keyStateLocalResetRequested || [state isEqualToString:SecCKKSZoneKeyStateResettingLocalData]) {
1040 // Local reset requests take precedence over all other state transitions
1041 ckksnotice("ckkskey", self, "Resetting local data");
1042 CKKSGroupOperation* op = [[CKKSGroupOperation alloc] init];
1043
1044 CKKSResultOperation* resetOp = [self createPendingResetLocalDataOperation];
1045 [op runBeforeGroupFinished: resetOp];
1046
1047 NSOperation* nextStateOp = [self operationToEnterState:SecCKKSZoneKeyStateInitializing keyStateError:nil named:@"state-resetting-initialize"];
1048 [nextStateOp addDependency:resetOp];
1049 [op runBeforeGroupFinished:nextStateOp];
1050
1051 self.keyStateMachineOperation = op;
1052 self.keyStateLocalResetRequested = false;
1053
1054 } else if([state isEqualToString:SecCKKSZoneKeyStateZoneCreationFailed]) {
1055 //Prepare to go back into initializing, as soon as the cloudkitRetryAfter is happy
1056 self.keyStateMachineOperation = [self operationToEnterState:SecCKKSZoneKeyStateInitializing keyStateError:nil named:@"recover-from-cloudkit-failure"];
1057 [self.keyStateMachineOperation addNullableDependency:self.zoneModifier.cloudkitRetryAfter.operationDependency];
1058 [self.zoneModifier.cloudkitRetryAfter trigger];
1059
1060 } else if([state isEqualToString:SecCKKSZoneKeyStateWaitForTrust]) {
1061 // Actually entering this state should have been handled above, so let's check if we can exit it here...
1062 if(self.trustStatus == CKKSAccountStatusAvailable) {
1063 ckksnotice("ckkskey", self, "Beginning trusted state machine operation");
1064 nextState = SecCKKSZoneKeyStateInitialized;
1065
1066 } else if (self.tlkCreationRequested) {
1067 ckksnotice("ckkskey", self, "No trust, but TLK creation is requested. Moving to fetchcomplete.");
1068 nextState = SecCKKSZoneKeyStateFetchComplete;
1069
1070 } else {
1071 ckksnotice("ckkskey", self, "Remaining in 'waitfortrust'");
1072 }
1073
1074 } else if([state isEqualToString: SecCKKSZoneKeyStateReady]) {
1075 NSError* localerror = nil;
1076 NSArray<CKKSKey*>* remoteKeys = [CKKSKey remoteKeys:self.zoneID error: &localerror];
1077
1078 if(remoteKeys == nil || localerror) {
1079 ckkserror("ckkskey", self, "couldn't fetch keys from local database, entering error state: %@", localerror);
1080 self.keyHierarchyState = SecCKKSZoneKeyStateError;
1081 self.keyHierarchyError = localerror;
1082 [self _onqueueHandleKeyStateNonTransientDependency:nil];
1083 return;
1084 }
1085
1086 if(self.keyStateProcessRequested || [remoteKeys count] > 0) {
1087 // We've either received some remote keys from the last fetch, or someone has requested a reprocess.
1088 ckksnotice("ckkskey", self, "Kicking off a key reprocess based on request:%d and remote key count %lu", self.keyStateProcessRequested, (unsigned long)[remoteKeys count]);
1089 nextState = SecCKKSZoneKeyStateProcess;
1090
1091 } else if(self.keyStateFullRefetchRequested) {
1092 // In ready, but someone has requested a full fetch. Kick it off.
1093 ckksnotice("ckkskey", self, "Kicking off a full key refetch based on request:%d", self.keyStateFullRefetchRequested);
1094 nextState = SecCKKSZoneKeyStateNeedFullRefetch;
1095
1096 } else if(self.keyStateFetchRequested) {
1097 // In ready, but someone has requested a fetch. Kick it off.
1098 ckksnotice("ckkskey", self, "Kicking off a key refetch based on request:%d", self.keyStateFetchRequested);
1099 nextState = SecCKKSZoneKeyStateFetch; // Don't go to 'ready', go to 'initialized', since we want to fetch again
1100 } else if (self.trustStatus != CKKSAccountStatusAvailable) {
1101 ckksnotice("ckkskey", self, "Asked to go into ready, but there's no trust; going into waitfortrust");
1102 nextState = SecCKKSZoneKeyStateWaitForTrust;
1103 } else if (self.trustedPeersSetChanged) {
1104 ckksnotice("ckkskey", self, "Received a nudge that the trusted peers set might have changed! Reprocessing.");
1105 nextState = SecCKKSZoneKeyStateProcess;
1106 self.trustedPeersSetChanged = false;
1107 }
1108
1109 // TODO: kick off a key roll if one has been requested
1110
1111 if(!self.keyStateMachineOperation && !nextState) {
1112 // We think we're ready. Double check.
1113 keyset = [CKKSCurrentKeySet loadForZone:self.zoneID];
1114 CKKSZoneKeyState* checkedstate = [self _onqueueEnsureKeyHierarchyHealth:keyset error:&hierarchyError];
1115 if(![checkedstate isEqualToString:SecCKKSZoneKeyStateReady] || hierarchyError) {
1116 // Things is bad. Kick off a heal to fix things up.
1117 ckksnotice("ckkskey", self, "Thought we were ready, but the key hierarchy is %@: %@", checkedstate, hierarchyError);
1118 nextState = checkedstate;
1119 if([nextState isEqualToString:SecCKKSZoneKeyStateError]) {
1120 nextError = hierarchyError;
1121 }
1122 }
1123 }
1124
1125 } else if([state isEqualToString: SecCKKSZoneKeyStateInitialized]) {
1126 // We're initialized and CloudKit is ready. If we're trusted, see what needs done. Otherwise, wait.
1127
1128 // Note: we might be still 'untrusted' at this point. The state machine is responsible for not entering 'ready' until
1129 // we are trusted.
1130 // This is acceptable only if the key state machine does not make new TLKs without being trusted!
1131
1132 // Set this state, for test use
1133 self.keyHierarchyState = SecCKKSZoneKeyStateInitialized;
1134
1135 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.zoneName];
1136 [self _onqueuePerformKeyStateInitialized:ckse];
1137
1138 // We need to either:
1139 // Wait for the fixup operation to occur
1140 // Go into 'ready'
1141 // Or start a key state fetch
1142 if(self.lastFixupOperation && ![self.lastFixupOperation isFinished]) {
1143 nextState = SecCKKSZoneKeyStateWaitForFixupOperation;
1144 } else {
1145 // Check if we have an existing key hierarchy in keyset
1146 keyset = [CKKSCurrentKeySet loadForZone:self.zoneID];
1147 if(keyset.error && !([keyset.error.domain isEqual: @"securityd"] && keyset.error.code == errSecItemNotFound)) {
1148 ckkserror("ckkskey", self, "Error examining existing key hierarchy: %@", error);
1149 }
1150
1151 if(keyset.tlk && keyset.classA && keyset.classC && !keyset.error) {
1152 // This is likely a restart of securityd, and we think we're ready. Double check.
1153
1154 CKKSZoneKeyState* checkedstate = [self _onqueueEnsureKeyHierarchyHealth:keyset error:&hierarchyError];
1155 if([checkedstate isEqualToString:SecCKKSZoneKeyStateReady] && !hierarchyError) {
1156 ckksnotice("ckkskey", self, "Already have existing key hierarchy for %@; using it.", self.zoneID.zoneName);
1157 } else {
1158 ckksnotice("ckkskey", self, "Initial scan shows key hierarchy is %@: %@", checkedstate, hierarchyError);
1159 }
1160 nextState = checkedstate;
1161
1162 } else {
1163 // We have no local key hierarchy. One might exist in CloudKit, or it might not.
1164 ckksnotice("ckkskey", self, "No existing key hierarchy for %@. Check if there's one in CloudKit...", self.zoneID.zoneName);
1165 nextState = SecCKKSZoneKeyStateFetch;
1166 }
1167 }
1168
1169 } else if([state isEqualToString:SecCKKSZoneKeyStateFetch]) {
1170 ckksnotice("ckkskey", self, "Starting a key hierarchy fetch");
1171 [self _onqueueKeyHierarchyFetch];
1172
1173 } else if([state isEqualToString: SecCKKSZoneKeyStateNeedFullRefetch]) {
1174 ckksnotice("ckkskey", self, "Starting a key hierarchy full refetch");
1175 [self _onqueueKeyHierarchyFetchForReasons:[NSSet setWithObjects:CKKSFetchBecauseKeyHierarchy, CKKSFetchBecauseResync, nil]];
1176 self.keyStateMachineRefetched = true;
1177 self.keyStateFullRefetchRequested = false;
1178
1179 } else if([state isEqualToString:SecCKKSZoneKeyStateWaitForFixupOperation]) {
1180 // We should enter 'initialized' when the fixup operation completes
1181 ckksnotice("ckkskey", self, "Waiting for the fixup operation: %@", self.lastFixupOperation);
1182
1183 self.keyStateMachineOperation = [NSBlockOperation named:@"key-state-after-fixup" withBlock:^{
1184 STRONGIFY(self);
1185 [self dispatchSyncWithAccountKeys:^bool{
1186 ckksnotice("ckkskey", self, "Fixup operation complete! Restarting key hierarchy machinery");
1187 [self _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateInitialized withError:nil];
1188 return true;
1189 }];
1190 }];
1191 [self.keyStateMachineOperation addNullableDependency:self.lastFixupOperation];
1192
1193 } else if([state isEqualToString: SecCKKSZoneKeyStateFetchComplete]) {
1194 // We've just completed a fetch of everything. Are there any remote keys?
1195 keyset = [CKKSCurrentKeySet loadForZone:self.zoneID];
1196
1197 NSError* localerror = nil;
1198
1199 NSArray<CKKSKey*>* localKeys = [CKKSKey localKeys:self.zoneID error:&localerror];
1200 NSArray<CKKSKey*>* remoteKeys = [CKKSKey remoteKeys:self.zoneID error: &localerror];
1201
1202 if(localKeys == nil || remoteKeys == nil || localerror) {
1203 ckkserror("ckkskey", self, "couldn't fetch keys from local database, entering error state: %@", localerror);
1204 self.keyHierarchyState = SecCKKSZoneKeyStateError;
1205 self.keyHierarchyError = localerror;
1206 [self _onqueueHandleKeyStateNonTransientDependency:nil];
1207 return;
1208 }
1209
1210 if(remoteKeys.count > 0u) {
1211 // Process the keys we received.
1212 self.keyStateMachineOperation = [[CKKSProcessReceivedKeysStateMachineOperation alloc] initWithCKKSKeychainView: self];
1213 } else if( (keyset.currentTLKPointer || keyset.currentClassAPointer || keyset.currentClassCPointer) &&
1214 !(keyset.tlk && keyset.classA && keyset.classC)) {
1215 // Huh. We appear to have current key pointers, but the keys themselves don't exist. That's weird.
1216 // Transfer to the "unhealthy" state to request a fix
1217 ckksnotice("ckkskey", self, "We appear to have current key pointers but no keys to match them: %@ Moving to 'unhealthy'", keyset);
1218 nextState = SecCKKSZoneKeyStateUnhealthy;
1219 } else {
1220 // No remote keys, and the pointers look sane? Do we have an existing key hierarchy?
1221 CKKSZoneKeyState* checkedstate = [self _onqueueEnsureKeyHierarchyHealth:keyset error:&hierarchyError];
1222 if([checkedstate isEqualToString:SecCKKSZoneKeyStateReady] && !hierarchyError) {
1223 ckksnotice("ckkskey", self, "After fetch, everything looks good.");
1224 nextState = checkedstate;
1225
1226 } else if(localKeys.count == 0 && remoteKeys.count == 0) {
1227 ckksnotice("ckkskey", self, "After fetch, we don't have any key hierarchy. Entering a waiting state: %@", hierarchyError ?: @"no error");
1228 nextState = SecCKKSZoneKeyStateWaitForTLKCreation;
1229 } else {
1230 ckksnotice("ckkskey", self, "After fetch, we have a possibly unhealthy key hierarchy. Moving to %@: %@", checkedstate, hierarchyError ?: @"no error");
1231 nextState = checkedstate;
1232 }
1233 }
1234
1235 } else if([state isEqualToString:SecCKKSZoneKeyStateWaitForTLKCreation]) {
1236
1237 if(self.tlkCreationRequested) {
1238 self.tlkCreationRequested = false;
1239 ckksnotice("ckkskey", self, "TLK creation requested; kicking off operation");
1240 self.keyStateMachineOperation = [[CKKSNewTLKOperation alloc] initWithCKKSKeychainView: self ckoperationGroup:self.keyHierarchyOperationGroup];
1241
1242 } else if(self.keyStateProcessRequested) {
1243 ckksnotice("ckkskey", self, "We believe we need to create TLKs but we also received a key nudge; moving to key state Process.");
1244 nextState = SecCKKSZoneKeyStateProcess;
1245
1246 } else {
1247 ckksnotice("ckkskey", self, "We believe we need to create TLKs; waiting for Octagon (via %@)", self.suggestTLKUpload);
1248 [self.suggestTLKUpload trigger];
1249 }
1250
1251
1252 } else if([state isEqualToString:SecCKKSZoneKeyStateWaitForTLKUpload]) {
1253 ckksnotice("ckkskey", self, "We believe we have TLKs that need uploading");
1254
1255
1256 if(self.keyStateProcessRequested) {
1257 keyset = [CKKSCurrentKeySet loadForZone:self.zoneID];
1258 if(keyset.currentTLKPointer.currentKeyUUID) {
1259 ckksnotice("ckkskey", self, "Received a nudge that our TLK records might be here (and there's some current TLK pointer)");
1260 nextState = SecCKKSZoneKeyStateProcess;
1261 } else {
1262 ckksnotice("ckkskey", self, "Received a nudge that our TLK records might be here, but there's no TLK pointer. Staying in WaitForTLKUpload.");
1263 self.keyStateProcessRequested = false;
1264 }
1265 }
1266
1267 if(nextState == nil) {
1268 ckksnotice("ckkskey", self, "Alerting any listener of our proposed keyset: %@", self.lastNewTLKOperation.keyset);
1269 [self _onqueueRunKeysetProviderOperations:self.lastNewTLKOperation.keyset];
1270
1271 ckksnotice("ckkskey", self, "Notifying Octagon again, just in case");
1272 [self.suggestTLKUpload trigger];
1273 }
1274
1275 } else if([state isEqualToString: SecCKKSZoneKeyStateWaitForTLK]) {
1276 // We're in a hold state: waiting for the TLK bytes to arrive.
1277
1278 if(self.keyStateProcessRequested) {
1279 // Someone has requsted a reprocess! Go to the correct state.
1280 ckksnotice("ckkskey", self, "Received a nudge that our TLK might be here! Reprocessing.");
1281 nextState = SecCKKSZoneKeyStateProcess;
1282
1283 } else if(self.trustedPeersSetChanged) {
1284 // Hmm, maybe this trust set change will cause us to recover this TLK (due to a previously-untrusted share becoming trusted). Worth a shot!
1285 ckksnotice("ckkskey", self, "Received a nudge that the trusted peers set might have changed! Reprocessing.");
1286 nextState = SecCKKSZoneKeyStateProcess;
1287 self.trustedPeersSetChanged = false;
1288
1289 } else {
1290 keyset = [CKKSCurrentKeySet loadForZone:self.zoneID];
1291
1292 // Should we nuke this zone?
1293 if(self.trustStatus == CKKSAccountStatusAvailable) {
1294 if([self _onqueueOtherDevicesReportHavingTLKs:keyset]) {
1295 ckksnotice("ckkskey", self, "Other devices report having TLK(%@). Entering a waiting state", keyset.currentTLKPointer);
1296 } else {
1297 ckksnotice("ckkskey", self, "No other devices have TLK(%@). Beginning zone reset...", keyset.currentTLKPointer);
1298 self.keyHierarchyOperationGroup = [CKOperationGroup CKKSGroupWithName:@"tlk-missing"];
1299 nextState = SecCKKSZoneKeyStateResettingZone;
1300 }
1301 } else {
1302 ckksnotice("ckkskey", self, "This device isn't trusted, so don't modify the existing TLK(%@)", keyset.currentTLKPointer);
1303 nextState = SecCKKSZoneKeyStateWaitForTrust;
1304 }
1305 }
1306
1307 } else if([state isEqualToString: SecCKKSZoneKeyStateWaitForUnlock]) {
1308 ckksnotice("ckkskey", self, "Requested to enter waitforunlock");
1309 self.keyStateMachineOperation = [self operationToEnterState:SecCKKSZoneKeyStateInitialized keyStateError:nil named:@"key-state-after-unlock"];
1310 [self.keyStateMachineOperation addNullableDependency: self.lockStateTracker.unlockDependency];
1311
1312 } else if([state isEqualToString: SecCKKSZoneKeyStateReadyPendingUnlock]) {
1313 ckksnotice("ckkskey", self, "Believe we're ready, but rechecking after unlock");
1314 self.keyStateMachineOperation = [self operationToEnterState:SecCKKSZoneKeyStateInitialized keyStateError:nil named:@"key-state-after-unlock"];
1315 [self.keyStateMachineOperation addNullableDependency: self.lockStateTracker.unlockDependency];
1316
1317 } else if([state isEqualToString: SecCKKSZoneKeyStateBadCurrentPointers]) {
1318 // The current key pointers are broken, but we're not sure why.
1319 ckksnotice("ckkskey", self, "Our current key pointers are reported broken. Attempting a fix!");
1320 self.keyStateMachineOperation = [[CKKSHealKeyHierarchyOperation alloc] initWithCKKSKeychainView: self ckoperationGroup:self.keyHierarchyOperationGroup];
1321
1322 } else if([state isEqualToString: SecCKKSZoneKeyStateNewTLKsFailed]) {
1323 ckksnotice("ckkskey", self, "Creating new TLKs didn't work. Attempting to refetch!");
1324 [self _onqueueKeyHierarchyFetch];
1325
1326 } else if([state isEqualToString: SecCKKSZoneKeyStateHealTLKSharesFailed]) {
1327 ckksnotice("ckkskey", self, "Creating new TLK shares didn't work. Attempting to refetch!");
1328 [self _onqueueKeyHierarchyFetch];
1329
1330 } else if([state isEqualToString:SecCKKSZoneKeyStateUnhealthy]) {
1331 if(self.trustStatus != CKKSAccountStatusAvailable) {
1332 ckksnotice("ckkskey", self, "Looks like the key hierarchy is unhealthy, but we're untrusted.");
1333 nextState = SecCKKSZoneKeyStateWaitForTrust;
1334
1335 } else {
1336 ckksnotice("ckkskey", self, "Looks like the key hierarchy is unhealthy. Launching fix.");
1337 self.keyStateMachineOperation = [[CKKSHealKeyHierarchyOperation alloc] initWithCKKSKeychainView:self ckoperationGroup:self.keyHierarchyOperationGroup];
1338 }
1339
1340 } else if([state isEqualToString:SecCKKSZoneKeyStateHealTLKShares]) {
1341 ckksnotice("ckksshare", self, "Key hierarchy is okay, but not shared appropriately. Launching fix.");
1342 self.keyStateMachineOperation = [[CKKSHealTLKSharesOperation alloc] initWithCKKSKeychainView:self
1343 ckoperationGroup:self.keyHierarchyOperationGroup];
1344
1345 } else if([state isEqualToString:SecCKKSZoneKeyStateProcess]) {
1346 ckksnotice("ckksshare", self, "Launching key state process");
1347 self.keyStateMachineOperation = [[CKKSProcessReceivedKeysStateMachineOperation alloc] initWithCKKSKeychainView: self];
1348
1349 // Since we're starting a reprocess, this is answering all previous requests.
1350 self.keyStateProcessRequested = false;
1351
1352 } else {
1353 ckkserror("ckks", self, "asked to advance state machine to unknown state: %@", state);
1354 self.keyHierarchyState = state;
1355
1356 keyset = [CKKSCurrentKeySet loadForZone:self.zoneID];
1357 [self _onqueueHandleKeyStateNonTransientDependency:keyset];
1358 return;
1359 }
1360
1361 // Handle the key state ready dependency
1362 // If we're in ready and not entering a non-ready state, we should activate the ready dependency. Otherwise, we should create it.
1363 if(([state isEqualToString:SecCKKSZoneKeyStateReady] || [state isEqualToString:SecCKKSZoneKeyStateReadyPendingUnlock]) &&
1364 (nextState == nil || [nextState isEqualToString:SecCKKSZoneKeyStateReady] || [nextState isEqualToString:SecCKKSZoneKeyStateReadyPendingUnlock])) {
1365
1366 // Ready enough!
1367 [[CKKSAnalytics logger] setDateProperty:[NSDate date] forKey:CKKSAnalyticsLastKeystateReady inView:self];
1368
1369 if(self.keyStateReadyDependency) {
1370 [self scheduleOperation: self.keyStateReadyDependency];
1371 self.keyStateReadyDependency = nil;
1372 }
1373
1374 // If there are any OQEs waiting to be encrypted, launch an op to fix them
1375 NSError* localerror = nil;
1376 NSInteger outdatedOQEs = [CKKSOutgoingQueueEntry countByState:SecCKKSStateReencrypt zone:self.zoneID error:&localerror];
1377
1378 if(localerror) {
1379 ckkserror("ckkskey", self, "couldn't fetch OQEs from local database, entering error state: %@", localerror);
1380 self.keyHierarchyState = SecCKKSZoneKeyStateError;
1381 self.keyHierarchyError = localerror;
1382 [self _onqueueHandleKeyStateNonTransientDependency:nil];
1383 return;
1384 }
1385
1386 if(outdatedOQEs > 0) {
1387 ckksnotice("ckksreencrypt", self, "Reencrypting outgoing items as the key hierarchy is ready");
1388 CKKSReencryptOutgoingItemsOperation* op = [[CKKSReencryptOutgoingItemsOperation alloc] initWithCKKSKeychainView:self ckoperationGroup:self.keyHierarchyOperationGroup];
1389 [self scheduleOperation:op];
1390 }
1391 } else {
1392 // Not in ready: we need a key state ready dependency
1393 if(self.keyStateReadyDependency == nil || [self.keyStateReadyDependency isFinished]) {
1394 self.keyHierarchyOperationGroup = [CKOperationGroup CKKSGroupWithName:@"key-state-broken"];
1395 self.keyStateReadyDependency = [self createKeyStateReadyDependency:@"Key state has become ready again." ckoperationGroup:self.keyHierarchyOperationGroup];
1396 }
1397 }
1398
1399 NSAssert(!((self.keyStateMachineOperation != nil) &&
1400 (nextState != nil)),
1401 @"Should have a machine operation or a next state, not both");
1402
1403 // Start any operations, or log that we aren't
1404 if(self.keyStateMachineOperation) {
1405 [self scheduleOperation: self.keyStateMachineOperation];
1406 ckksnotice("ckkskey", self, "Now in key state: %@", state);
1407 self.keyHierarchyState = state;
1408
1409 } else if([state isEqualToString:SecCKKSZoneKeyStateError]) {
1410 ckksnotice("ckkskey", self, "Entering key state 'error'");
1411 self.keyHierarchyState = state;
1412
1413 } else if(nextState == nil) {
1414 ckksnotice("ckkskey", self, "Entering key state: %@", state);
1415 self.keyHierarchyState = state;
1416
1417 } else if(![state isEqualToString: nextState]) {
1418 ckksnotice("ckkskey", self, "Staying in state %@, but proceeding to %@ as soon as possible", self.keyHierarchyState, nextState);
1419 self.keyStateMachineOperation = [self operationToEnterState:nextState keyStateError:nextError named:[NSString stringWithFormat:@"next-key-state-%@", nextState]];
1420 [self scheduleOperation: self.keyStateMachineOperation];
1421
1422 } else {
1423 // Nothing to do and not in a waiting state? This is likely a bug, but, hey: pretend to be in ready!
1424 if(!([state isEqualToString:SecCKKSZoneKeyStateReady] || [state isEqualToString:SecCKKSZoneKeyStateReadyPendingUnlock])) {
1425 ckkserror("ckkskey", self, "No action to take in state %@; BUG, but: maybe we're ready?", state);
1426 nextState = SecCKKSZoneKeyStateReady;
1427 self.keyStateMachineOperation = [self operationToEnterState:nextState keyStateError:nil named:@"next-key-state"];
1428 [self scheduleOperation: self.keyStateMachineOperation];
1429 }
1430 }
1431
1432 // If the keystate is non-transient, ensure we've loaded the keyset, and provide it to any waiters
1433 // If it is transient, just call the handler anyway: it needs to set up the dependency
1434 if(!CKKSKeyStateTransient(self.keyHierarchyState) && keyset == nil) {
1435 keyset = [CKKSCurrentKeySet loadForZone:self.zoneID];
1436 }
1437 [self _onqueueHandleKeyStateNonTransientDependency:keyset];
1438 }
1439
1440 - (void)_onqueueHandleKeyStateNonTransientDependency:(CKKSCurrentKeySet* _Nullable)keyset {
1441 dispatch_assert_queue(self.queue);
1442
1443 if(CKKSKeyStateTransient(self.keyHierarchyState)) {
1444 if(self.keyStateNonTransientDependency == nil || [self.keyStateNonTransientDependency isFinished]) {
1445 self.keyStateNonTransientDependency = [self createKeyStateNontransientDependency];
1446 }
1447 } else {
1448 // Nontransient: go for it
1449 if(self.keyStateNonTransientDependency) {
1450 [self scheduleOperation: self.keyStateNonTransientDependency];
1451 self.keyStateNonTransientDependency = nil;
1452 }
1453
1454 if(keyset && keyset.currentTLKPointer.currentKeyUUID) {
1455 [self _onqueueRunKeysetProviderOperations:keyset];
1456 } else {
1457 ckksnotice("ckkskey", self, "State machine is nontransient, but no keyset...");
1458 }
1459 }
1460 }
1461
1462 - (NSOperation*)operationToEnterState:(CKKSZoneKeyState*)state keyStateError:(NSError* _Nullable)keyStateError named:(NSString*)name {
1463 WEAKIFY(self);
1464
1465 return [NSBlockOperation named:name withBlock:^{
1466 STRONGIFY(self);
1467 if(!self) {
1468 return;
1469 }
1470 [self dispatchSyncWithAccountKeys:^bool{
1471 [self _onqueueAdvanceKeyStateMachineToState:state withError:keyStateError];
1472 return true;
1473 }];
1474 }];
1475 }
1476
1477 - (BOOL)otherDevicesReportHavingTLKs:(CKKSCurrentKeySet*)keyset
1478 {
1479 __block BOOL report = false;
1480 [self dispatchSync:^bool{
1481 report = [self _onqueueOtherDevicesReportHavingTLKs:keyset];
1482 return true;
1483 }];
1484 return report ? YES : NO;
1485 }
1486
1487 - (bool)_onqueueOtherDevicesReportHavingTLKs:(CKKSCurrentKeySet*)keyset
1488 {
1489 dispatch_assert_queue(self.queue);
1490
1491 //Has there been any activity indicating that other trusted devices have keys in the past 45 days, or untrusted devices in the past 4?
1492 // (We chose 4 as devices attempt to upload their device state every 3 days. If a device is unceremoniously kicked out of circle, we normally won't immediately reset.)
1493 NSDate* now = [NSDate date];
1494 NSDateComponents* trustedOffset = [[NSDateComponents alloc] init];
1495 [trustedOffset setDay:-45];
1496 NSDate* trustedDeadline = [[NSCalendar currentCalendar] dateByAddingComponents:trustedOffset toDate:now options:0];
1497
1498 NSDateComponents* untrustedOffset = [[NSDateComponents alloc] init];
1499 [untrustedOffset setDay:-4];
1500 NSDate* untrustedDeadline = [[NSCalendar currentCalendar] dateByAddingComponents:untrustedOffset toDate:now options:0];
1501
1502
1503 NSMutableSet<NSString*>* trustedPeerIDs = [NSMutableSet set];
1504 for(CKKSPeerProviderState* trustState in self.currentTrustStates) {
1505 for(id<CKKSPeer> peer in trustState.currentTrustedPeers) {
1506 [trustedPeerIDs addObject:peer.peerID];
1507 }
1508 }
1509
1510 NSError* localerror = nil;
1511
1512 NSArray<CKKSDeviceStateEntry*>* allDeviceStates = [CKKSDeviceStateEntry allInZone:self.zoneID error:&localerror];
1513 if(localerror) {
1514 ckkserror("ckkskey", self, "Error fetching device states: %@", localerror);
1515 localerror = nil;
1516 return true;
1517 }
1518 for(CKKSDeviceStateEntry* device in allDeviceStates) {
1519 // The peerIDs in CDSEs aren't written with the peer prefix. Make sure we match both.
1520 NSString* sosPeerID = device.circlePeerID ? [CKKSSOSPeerPrefix stringByAppendingString:device.circlePeerID] : nil;
1521
1522 if([trustedPeerIDs containsObject:device.circlePeerID] ||
1523 [trustedPeerIDs containsObject:sosPeerID] ||
1524 [trustedPeerIDs containsObject:device.octagonPeerID]) {
1525 // Is this a recent DSE? If it's older than the deadline, skip it
1526 if([device.storedCKRecord.modificationDate compare:trustedDeadline] == NSOrderedAscending) {
1527 ckksnotice("ckkskey", self, "Trusted device state (%@) is too old; ignoring", device);
1528 continue;
1529 }
1530 } else {
1531 // Device is untrusted. How does it fare with the untrustedDeadline?
1532 if([device.storedCKRecord.modificationDate compare:untrustedDeadline] == NSOrderedAscending) {
1533 ckksnotice("ckkskey", self, "Device (%@) is not trusted and from too long ago; ignoring device state (%@)", device.circlePeerID, device);
1534 continue;
1535 } else {
1536 ckksnotice("ckkskey", self, "Device (%@) is not trusted, but very recent. Including in heuristic: %@", device.circlePeerID, device);
1537 }
1538 }
1539
1540 if([device.keyState isEqualToString:SecCKKSZoneKeyStateReady] ||
1541 [device.keyState isEqualToString:SecCKKSZoneKeyStateReadyPendingUnlock]) {
1542 ckksnotice("ckkskey", self, "Other device (%@) has keys; it should send them to us", device);
1543 return true;
1544 }
1545 }
1546
1547 NSArray<CKKSTLKShareRecord*>* tlkShares = [CKKSTLKShareRecord allForUUID:keyset.currentTLKPointer.currentKeyUUID
1548 zoneID:self.zoneID
1549 error:&localerror];
1550 if(localerror) {
1551 ckkserror("ckkskey", self, "Error fetching device states: %@", localerror);
1552 localerror = nil;
1553 return false;
1554 }
1555
1556 for(CKKSTLKShareRecord* tlkShare in tlkShares) {
1557 if([trustedPeerIDs containsObject:tlkShare.senderPeerID] &&
1558 [tlkShare.storedCKRecord.modificationDate compare:trustedDeadline] == NSOrderedDescending) {
1559 ckksnotice("ckkskey", self, "Trusted TLK Share (%@) created recently; other devices have keys and should send them to us", tlkShare);
1560 return true;
1561 }
1562 }
1563
1564 // Okay, how about the untrusted deadline?
1565 for(CKKSTLKShareRecord* tlkShare in tlkShares) {
1566 if([tlkShare.storedCKRecord.modificationDate compare:untrustedDeadline] == NSOrderedDescending) {
1567 ckksnotice("ckkskey", self, "Untrusted TLK Share (%@) created very recently; other devices might have keys and should rejoin the circle (and send them to us)", tlkShare);
1568 return true;
1569 }
1570 }
1571
1572 return false;
1573 }
1574
1575 // For this key, who doesn't yet have a valid CKKSTLKShare for it?
1576 // Note that we really want a record sharing the TLK to ourselves, so this function might return
1577 // a non-empty set even if all peers have the TLK: it wants us to make a record for ourself.
1578 - (NSSet<id<CKKSPeer>>*)_onqueueFindPeers:(CKKSPeerProviderState*)trustState
1579 missingShare:(CKKSKey*)key
1580 afterUploading:(NSSet<CKKSTLKShareRecord*>* _Nullable)newShares
1581 error:(NSError* __autoreleasing*)error
1582 {
1583 dispatch_assert_queue(self.queue);
1584
1585 if(!key) {
1586 ckkserror("ckksshare", self, "Attempting to find missing shares for nil key");
1587 return [NSSet set];
1588 }
1589
1590 if(trustState.currentTrustedPeersError) {
1591 ckkserror("ckksshare", self, "Couldn't find missing shares because trusted peers aren't available: %@", trustState.currentTrustedPeersError);
1592 if(error) {
1593 *error = trustState.currentTrustedPeersError;
1594 }
1595 return [NSSet set];
1596 }
1597 if(trustState.currentSelfPeersError) {
1598 ckkserror("ckksshare", self, "Couldn't find missing shares because self peers aren't available: %@", trustState.currentSelfPeersError);
1599 if(error) {
1600 *error = trustState.currentSelfPeersError;
1601 }
1602 return [NSSet set];
1603 }
1604
1605 NSMutableSet<id<CKKSPeer>>* peersMissingShares = [NSMutableSet set];
1606
1607 // Ensure that the 'self peer' is one of the current trusted peers. Otherwise, any TLKShare we create
1608 // won't be considered trusted the next time through...
1609 if(![trustState.currentTrustedPeerIDs containsObject:trustState.currentSelfPeers.currentSelf.peerID]) {
1610 ckkserror("ckksshare", self, "current self peer (%@) is not in the set of trusted peers: %@",
1611 trustState.currentSelfPeers.currentSelf.peerID,
1612 trustState.currentTrustedPeerIDs);
1613
1614 if(error) {
1615 *error = [NSError errorWithDomain:CKKSErrorDomain
1616 code:CKKSLackingTrust
1617 description:[NSString stringWithFormat:@"current self peer (%@) is not in the set of trusted peers",
1618 trustState.currentSelfPeers.currentSelf.peerID]];
1619 }
1620
1621 return nil;
1622 }
1623
1624 for(id<CKKSRemotePeerProtocol> peer in trustState.currentTrustedPeers) {
1625 if(![peer shouldHaveView:self.zoneName]) {
1626 ckkserror("ckksshare", self, "Peer (%@) is not supposed to have view, skipping", peer);
1627 continue;
1628 }
1629
1630 NSError* peerError = nil;
1631 // Find all the shares for this peer for this key
1632 NSArray<CKKSTLKShareRecord*>* currentPeerShares = [CKKSTLKShareRecord allFor:peer.peerID
1633 keyUUID:key.uuid
1634 zoneID:self.zoneID
1635 error:&peerError];
1636
1637 if(peerError) {
1638 ckkserror("ckksshare", self, "Couldn't load shares for peer %@: %@", peer, peerError);
1639 if(error) {
1640 *error = peerError;
1641 }
1642 return nil;
1643 }
1644
1645 // Include the new shares, too....
1646 NSArray<CKKSTLKShareRecord*>* possiblePeerShares = newShares ? [currentPeerShares arrayByAddingObjectsFromArray:[newShares allObjects]] : currentPeerShares;
1647
1648 // Determine if we think this peer has enough things shared to them
1649 bool alreadyShared = false;
1650 for(CKKSTLKShareRecord* existingPeerShare in possiblePeerShares) {
1651 // Ensure this share is to this peer...
1652 if(![existingPeerShare.share.receiverPeerID isEqualToString:peer.peerID]) {
1653 continue;
1654 }
1655
1656 // If an SOS Peer sent this share, is its signature still valid? Or did the signing key change?
1657 if([existingPeerShare.senderPeerID hasPrefix:CKKSSOSPeerPrefix]) {
1658 NSError* signatureError = nil;
1659 if(![existingPeerShare signatureVerifiesWithPeerSet:trustState.currentTrustedPeers error:&signatureError]) {
1660 ckksnotice("ckksshare", self, "Existing TLKShare's signature doesn't verify with current peer set: %@ %@", signatureError, existingPeerShare);
1661 continue;
1662 }
1663 }
1664
1665 if([existingPeerShare.tlkUUID isEqualToString:key.uuid] && [trustState.currentTrustedPeerIDs containsObject:existingPeerShare.senderPeerID]) {
1666 // Was this shared to us?
1667 if([peer.peerID isEqualToString: trustState.currentSelfPeers.currentSelf.peerID]) {
1668 // We only count this as 'found' if we did the sharing and it's to our current keys
1669 NSData* currentKey = trustState.currentSelfPeers.currentSelf.publicEncryptionKey.keyData;
1670
1671 if([existingPeerShare.senderPeerID isEqualToString:trustState.currentSelfPeers.currentSelf.peerID] &&
1672 [existingPeerShare.share.receiverPublicEncryptionKeySPKI isEqual:currentKey]) {
1673 ckksnotice("ckksshare", self, "Local peer %@ is shared %@ via self: %@", peer, key, existingPeerShare);
1674 alreadyShared = true;
1675 break;
1676 } else {
1677 ckksnotice("ckksshare", self, "Local peer %@ is shared %@ via trusted %@, but that's not good enough", peer, key, existingPeerShare);
1678 }
1679
1680 } else {
1681 // Was this shared to the remote peer's current keys?
1682 NSData* currentKeySPKI = peer.publicEncryptionKey.keyData;
1683
1684 if([existingPeerShare.share.receiverPublicEncryptionKeySPKI isEqual:currentKeySPKI]) {
1685 // Some other peer has a trusted share. Cool!
1686 ckksnotice("ckksshare", self, "Peer %@ is shared %@ via trusted %@", peer, key, existingPeerShare);
1687 alreadyShared = true;
1688 break;
1689 } else {
1690 ckksnotice("ckksshare", self, "Peer %@ has a share for %@, but to old keys: %@", peer, key, existingPeerShare);
1691 }
1692 }
1693 }
1694 }
1695
1696 if(!alreadyShared) {
1697 // Add this peer to our set, if it has an encryption key to receive the share
1698 if(peer.publicEncryptionKey) {
1699 [peersMissingShares addObject:peer];
1700 }
1701 }
1702 }
1703
1704 if(peersMissingShares.count > 0u) {
1705 // Log each and every one of the things
1706 ckksnotice("ckksshare", self, "Missing TLK shares for %lu peers: %@", (unsigned long)peersMissingShares.count, peersMissingShares);
1707 ckksnotice("ckksshare", self, "Self peers are (%@) %@", trustState.currentSelfPeersError ?: @"no error", trustState.currentSelfPeers);
1708 ckksnotice("ckksshare", self, "Trusted peers are (%@) %@", trustState.currentTrustedPeersError ?: @"no error", trustState.currentTrustedPeers);
1709 }
1710
1711 return peersMissingShares;
1712 }
1713
1714 - (BOOL)_onqueueAreNewSharesSufficient:(NSSet<CKKSTLKShareRecord*>*)newShares
1715 currentTLK:(CKKSKey*)key
1716 error:(NSError* __autoreleasing*)error
1717 {
1718 dispatch_assert_queue(self.queue);
1719
1720 for(CKKSPeerProviderState* trustState in self.currentTrustStates) {
1721 NSError* localError = nil;
1722 NSSet<id<CKKSPeer>>* peersMissingShares = [self _onqueueFindPeers:trustState
1723 missingShare:key
1724 afterUploading:newShares
1725 error:&localError];
1726 if(peersMissingShares == nil || localError) {
1727 if(trustState.essential) {
1728 if(error) {
1729 *error = localError;
1730 }
1731 return NO;
1732 } else {
1733 ckksnotice("ckksshare", self, "Failed to find peers for nonessential system: %@", trustState);
1734 // Not a hard failure.
1735 }
1736 }
1737
1738 if(peersMissingShares.count > 0) {
1739 ckksnotice("ckksshare", self, "New share set is missing shares for peers: %@", peersMissingShares);
1740 return NO;
1741 }
1742 }
1743
1744 return YES;
1745 }
1746
1747 - (NSSet<CKKSTLKShareRecord*>*)_onqueueCreateMissingKeyShares:(CKKSKey*)key
1748 error:(NSError* __autoreleasing*)error
1749 {
1750 NSError* localerror = nil;
1751 NSSet<CKKSTLKShareRecord*>* newShares = nil;
1752
1753 // If any one of our trust states succeed, this function doesn't have an error
1754 for(CKKSPeerProviderState* trustState in self.currentTrustStates) {
1755 NSError* stateError = nil;
1756
1757 NSSet<CKKSTLKShareRecord*>* newTrustShares = [self _onqueueCreateMissingKeyShares:key
1758 peers:trustState
1759 error:&stateError];
1760
1761
1762 if(newTrustShares && !stateError) {
1763 newShares = newShares ? [newShares setByAddingObjectsFromSet:newTrustShares] : newTrustShares;
1764 } else {
1765 ckksnotice("ckksshare", self, "Unable to create shares for trust set %@: %@", trustState, stateError);
1766 if(localerror == nil) {
1767 localerror = stateError;
1768 }
1769 }
1770 }
1771
1772 // Only report an error if none of the trust states were able to succeed
1773 if(newShares) {
1774 return newShares;
1775 } else {
1776 if(error && localerror) {
1777 *error = localerror;
1778 }
1779 return nil;
1780 }
1781 }
1782
1783 - (NSSet<CKKSTLKShareRecord*>*)_onqueueCreateMissingKeyShares:(CKKSKey*)key
1784 peers:(CKKSPeerProviderState*)trustState
1785 error:(NSError* __autoreleasing*)error
1786 {
1787 dispatch_assert_queue(self.queue);
1788
1789 if(trustState.currentTrustedPeersError) {
1790 ckkserror("ckksshare", self, "Couldn't create missing shares because trusted peers aren't available: %@", trustState.currentTrustedPeersError);
1791 if(error) {
1792 *error = trustState.currentTrustedPeersError;
1793 }
1794 return nil;
1795 }
1796 if(trustState.currentSelfPeersError) {
1797 ckkserror("ckksshare", self, "Couldn't create missing shares because self peers aren't available: %@", trustState.currentSelfPeersError);
1798 if(error) {
1799 *error = trustState.currentSelfPeersError;
1800 }
1801 return nil;
1802 }
1803
1804 NSSet<id<CKKSPeer>>* remainingPeers = [self _onqueueFindPeers:trustState missingShare:key afterUploading:nil error:error];
1805 NSMutableSet<CKKSTLKShareRecord*>* newShares = [NSMutableSet set];
1806
1807 if(!remainingPeers) {
1808 return nil;
1809 }
1810
1811 NSError* localerror = nil;
1812
1813 if(![key ensureKeyLoaded:error]) {
1814 return nil;
1815 }
1816
1817 for(id<CKKSPeer> peer in remainingPeers) {
1818 if(!peer.publicEncryptionKey) {
1819 ckksnotice("ckksshare", self, "No need to make TLK for %@; they don't have any encryption keys", peer);
1820 continue;
1821 }
1822
1823 // Create a share for this peer.
1824 ckksnotice("ckksshare", self, "Creating share of %@ as %@ for %@", key, trustState.currentSelfPeers.currentSelf, peer);
1825 CKKSTLKShareRecord* newShare = [CKKSTLKShareRecord share:key
1826 as:trustState.currentSelfPeers.currentSelf
1827 to:peer
1828 epoch:-1
1829 poisoned:0
1830 error:&localerror];
1831
1832 if(localerror) {
1833 ckkserror("ckksshare", self, "Couldn't create new share for %@: %@", peer, localerror);
1834 if(error) {
1835 *error = localerror;
1836 }
1837 return nil;
1838 }
1839
1840 [newShares addObject: newShare];
1841 }
1842
1843 return newShares;
1844 }
1845
1846 - (CKKSZoneKeyState*)_onqueueEnsureKeyHierarchyHealth:(CKKSCurrentKeySet*)set error:(NSError* __autoreleasing *)error {
1847 dispatch_assert_queue(self.queue);
1848
1849 if(!set.currentTLKPointer && !set.currentClassAPointer && !set.currentClassCPointer) {
1850 ckkserror("ckkskey", self, "Error examining existing key hierarchy (missing all CKPs, likely no hierarchy exists): %@", set);
1851 return SecCKKSZoneKeyStateWaitForTLKCreation;
1852 }
1853
1854 // Check keyset
1855 if(!set.tlk || !set.classA || !set.classC) {
1856 ckkserror("ckkskey", self, "Error examining existing key hierarchy (missing at least one key): %@", set);
1857 if(error) {
1858 *error = set.error;
1859 }
1860 return SecCKKSZoneKeyStateUnhealthy;
1861 }
1862
1863 NSError* localerror = nil;
1864 bool probablyOkIfUnlocked = false;
1865
1866 // keychain being locked is not a fatal error here
1867 [set.tlk loadKeyMaterialFromKeychain:&localerror];
1868 if(localerror && !([localerror.domain isEqual: @"securityd"] && localerror.code == errSecInteractionNotAllowed)) {
1869 ckkserror("ckkskey", self, "Error loading TLK(%@): %@", set.tlk, localerror);
1870 if(error) {
1871 *error = localerror;
1872 }
1873 return SecCKKSZoneKeyStateUnhealthy;
1874 } else if(localerror) {
1875 ckkserror("ckkskey", self, "Soft error loading TLK(%@), maybe locked: %@", set.tlk, localerror);
1876 probablyOkIfUnlocked = true;
1877 }
1878 localerror = nil;
1879
1880 // keychain being locked is not a fatal error here
1881 [set.classA loadKeyMaterialFromKeychain:&localerror];
1882 if(localerror && !([localerror.domain isEqual: @"securityd"] && localerror.code == errSecInteractionNotAllowed)) {
1883 ckkserror("ckkskey", self, "Error loading classA key(%@): %@", set.classA, localerror);
1884 if(error) {
1885 *error = localerror;
1886 }
1887 return SecCKKSZoneKeyStateUnhealthy;
1888 } else if(localerror) {
1889 ckkserror("ckkskey", self, "Soft error loading classA key(%@), maybe locked: %@", set.classA, localerror);
1890 probablyOkIfUnlocked = true;
1891 }
1892 localerror = nil;
1893
1894 // keychain being locked is a fatal error here, since this is class C
1895 [set.classC loadKeyMaterialFromKeychain:&localerror];
1896 if(localerror) {
1897 ckkserror("ckkskey", self, "Error loading classC(%@): %@", set.classC, localerror);
1898 if(error) {
1899 *error = localerror;
1900 }
1901 return SecCKKSZoneKeyStateUnhealthy;
1902 }
1903
1904 // Check that the classA and classC keys point to the current TLK
1905 if(![set.classA.parentKeyUUID isEqualToString: set.tlk.uuid]) {
1906 localerror = [NSError errorWithDomain:CKKSServerExtensionErrorDomain
1907 code:CKKSServerUnexpectedSyncKeyInChain
1908 userInfo:@{
1909 NSLocalizedDescriptionKey: @"Current class A key does not wrap to current TLK",
1910 }];
1911 ckkserror("ckkskey", self, "Key hierarchy unhealthy: %@", localerror);
1912 if(error) {
1913 *error = localerror;
1914 }
1915 return SecCKKSZoneKeyStateUnhealthy;
1916 }
1917 if(![set.classC.parentKeyUUID isEqualToString: set.tlk.uuid]) {
1918 localerror = [NSError errorWithDomain:CKKSServerExtensionErrorDomain
1919 code:CKKSServerUnexpectedSyncKeyInChain
1920 userInfo:@{
1921 NSLocalizedDescriptionKey: @"Current class C key does not wrap to current TLK",
1922 }];
1923 ckkserror("ckkskey", self, "Key hierarchy unhealthy: %@", localerror);
1924 if(error) {
1925 *error = localerror;
1926 }
1927 return SecCKKSZoneKeyStateUnhealthy;
1928 }
1929
1930 self.activeTLK = [set.tlk uuid];
1931
1932 // Now that we're pretty sure we have the keys, are they shared appropriately?
1933 // We need trust in order to proceed here
1934 if(self.currentTrustStates.count == 0u) {
1935 ckkserror("ckkskey", self, "Can't check TLKShares due to missing trust states");
1936 return SecCKKSZoneKeyStateWaitForTrust;
1937 }
1938
1939 // Check that every trusted peer has at least one TLK share
1940 // If any trust state check works, don't error out
1941 bool anyTrustStateSucceeded = false;
1942 for(CKKSPeerProviderState* trustState in self.currentTrustStates) {
1943 NSSet<id<CKKSPeer>>* missingShares = [self _onqueueFindPeers:trustState missingShare:set.tlk afterUploading:nil error:&localerror];
1944 if(localerror && [self.lockStateTracker isLockedError: localerror]) {
1945 ckkserror("ckkskey", self, "Couldn't find missing TLK shares due to lock state: %@", localerror);
1946 probablyOkIfUnlocked = true;
1947
1948 } else if(([localerror.domain isEqualToString:TrustedPeersHelperErrorDomain] && localerror.code == TrustedPeersHelperErrorNoPreparedIdentity) ||
1949 ([localerror.domain isEqualToString:CKKSErrorDomain] && localerror.code == CKKSLackingTrust) ||
1950 ([localerror.domain isEqualToString:CKKSErrorDomain] && localerror.code == CKKSNoPeersAvailable)) {
1951 ckkserror("ckkskey", self, "Couldn't find missing TLK shares due some trust issue: %@", localerror);
1952
1953 if(trustState.essential) {
1954 ckkserror("ckkskey", self, "Trust state is considered essential; entering waitfortrust: %@", trustState);
1955
1956 // Octagon can reinform us when it thinks we should start again
1957 self.trustStatus = CKKSAccountStatusUnknown;
1958 return SecCKKSZoneKeyStateWaitForTrust;
1959 } else {
1960 ckkserror("ckkskey", self, "Peer provider is considered nonessential; ignoring error: %@", trustState);
1961 continue;
1962 }
1963
1964 } else if(localerror) {
1965 ckkserror("ckkskey", self, "Error finding missing TLK shares: %@", localerror);
1966 continue;
1967 }
1968
1969 if(!missingShares || missingShares.count != 0u) {
1970 localerror = [NSError errorWithDomain:CKKSErrorDomain code:CKKSMissingTLKShare
1971 description:[NSString stringWithFormat:@"Missing shares for %lu peers", (unsigned long)missingShares.count]];
1972 if(error) {
1973 *error = localerror;
1974 }
1975 return SecCKKSZoneKeyStateHealTLKShares;
1976 } else {
1977 ckksnotice("ckksshare", self, "TLK (%@) is shared correctly for trust state %@", set.tlk, trustState.peerProviderID);
1978 }
1979
1980 anyTrustStateSucceeded |= true;
1981 }
1982
1983 if(!anyTrustStateSucceeded) {
1984 if(error) {
1985 *error = localerror;
1986 }
1987
1988 return SecCKKSZoneKeyStateError;
1989 }
1990
1991 // Got to the bottom? Cool! All keys are present and accounted for.
1992 return probablyOkIfUnlocked ? SecCKKSZoneKeyStateReadyPendingUnlock : SecCKKSZoneKeyStateReady;
1993 }
1994
1995 - (void)_onqueueKeyHierarchyFetch {
1996 [self _onqueueKeyHierarchyFetchForReasons:[NSSet setWithArray:@[CKKSFetchBecauseKeyHierarchy]]];
1997 }
1998
1999 - (void)_onqueueKeyHierarchyFetchForReasons:(NSSet<CKKSFetchBecause*>*)reasons
2000 {
2001 dispatch_assert_queue(self.queue);
2002
2003 WEAKIFY(self);
2004 self.keyStateMachineOperation = [NSBlockOperation blockOperationWithBlock: ^{
2005 STRONGIFY(self);
2006 if(!self) {
2007 ckkserror("ckks", self, "received callback for released object");
2008 return;
2009 }
2010 [self.launch addEvent:@"fetch-complete"];
2011
2012 [self dispatchSyncWithAccountKeys: ^bool{
2013 [self _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateFetchComplete withError: nil];
2014 return true;
2015 }];
2016 }];
2017 self.keyStateMachineOperation.name = @"waiting-for-fetch";
2018
2019 NSOperation* fetchOp = [self.zoneChangeFetcher requestSuccessfulFetchForManyReasons:reasons];
2020 [self.keyStateMachineOperation addDependency: fetchOp];
2021
2022 self.keyStateFetchRequested = false;
2023 }
2024
2025 - (void) handleKeychainEventDbConnection: (SecDbConnectionRef) dbconn
2026 source:(SecDbTransactionSource)txionSource
2027 added: (SecDbItemRef) added
2028 deleted: (SecDbItemRef) deleted
2029 rateLimiter: (CKKSRateLimiter*) rateLimiter
2030 {
2031 if(!SecCKKSIsEnabled()) {
2032 ckksnotice("ckks", self, "Skipping handleKeychainEventDbConnection due to disabled CKKS");
2033 return;
2034 }
2035
2036 __block NSError* error = nil;
2037
2038 // Tombstones come in as item modifications or item adds. Handle modifications here.
2039 bool addedTombstone = added && SecDbItemIsTombstone(added);
2040 bool deletedTombstone = deleted && SecDbItemIsTombstone(deleted);
2041
2042 bool addedSync = added && SecDbItemIsSyncable(added);
2043 bool deletedSync = deleted && SecDbItemIsSyncable(deleted);
2044
2045 bool isAdd = ( added && !deleted) || (added && deleted && !addedTombstone && deletedTombstone) || (added && deleted && addedSync && !deletedSync);
2046 bool isDelete = (!added && deleted) || (added && deleted && addedTombstone && !deletedTombstone) || (added && deleted && !addedSync && deletedSync);
2047 bool isModify = ( added && deleted) && (!isAdd) && (!isDelete);
2048
2049 // On an update that changes an item's primary key, SecDb modifies the existing item, then adds a new tombstone to replace the old primary key.
2050 // Therefore, we might receive an added tombstone here with no deleted item to accompany it. This should be considered a deletion.
2051 if(addedTombstone && !deleted) {
2052 isAdd = false;
2053 isDelete = true;
2054 isModify = false;
2055
2056 // Passed to withItem: below
2057 deleted = added;
2058 }
2059
2060 // If neither item is syncable, don't proceed further in the syncing system
2061 bool proceed = addedSync || deletedSync;
2062
2063 if(!proceed) {
2064 ckksnotice("ckks", self, "skipping sync of non-sync item (%d, %d)", addedSync, deletedSync);
2065 return;
2066 }
2067
2068 // It's possible to ask for an item to be deleted without adding a corresponding tombstone.
2069 // This is arguably a bug, as it generates an out-of-sync state, but it is in the API contract.
2070 // CKKS should ignore these, but log very upset messages.
2071 if(isDelete && !addedTombstone) {
2072 ckksnotice("ckks", self, "Client has asked for an item deletion to not sync. Keychain is now out of sync with account");
2073 return;
2074 }
2075
2076 // Only synchronize items which can transfer between devices
2077 NSString* protection = (__bridge NSString*)SecDbItemGetCachedValueWithName(added ? added : deleted, kSecAttrAccessible);
2078 if(! ([protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleWhenUnlocked] ||
2079 [protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAfterFirstUnlock] ||
2080 [protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAlwaysPrivate])) {
2081 ckksnotice("ckks", self, "skipping sync of device-bound(%@) item", protection);
2082 return;
2083 }
2084
2085 if(txionSource == kSecDbSOSTransaction) {
2086 ckksnotice("ckks", self, "Received an incoming %@ from SOS", isAdd ? @"addition" : (isModify ? @"modification" : @"deletion"));
2087 }
2088
2089 // Our caller gave us a database connection. We must get on the local queue to ensure atomicity
2090 // Note that we're at the mercy of the surrounding db transaction, so don't try to rollback here
2091 [self dispatchSyncWithConnection: dbconn block: ^bool {
2092 // Schedule a "view changed" notification
2093 [self.notifyViewChangedScheduler trigger];
2094
2095 if(self.accountStatus == CKKSAccountStatusNoAccount) {
2096 // No account; CKKS shouldn't attempt anything.
2097 self.droppedItems = true;
2098 ckksnotice("ckks", self, "Dropping sync item modification due to CK account state; will scan to find changes later");
2099
2100 // We're positively not logged into CloudKit, and therefore don't expect this item to be synced anytime particularly soon.
2101 NSString* uuid = (__bridge NSString*)SecDbItemGetValue(added ? added : deleted, &v10itemuuid, NULL);
2102
2103 SecBoolNSErrorCallback syncCallback = [[CKKSViewManager manager] claimCallbackForUUID:uuid];
2104 if(syncCallback) {
2105 [CKKSViewManager callSyncCallbackWithErrorNoAccount: syncCallback];
2106 }
2107
2108 return true;
2109 }
2110
2111 CKKSOutgoingQueueEntry* oqe = nil;
2112 if (isAdd) {
2113 oqe = [CKKSOutgoingQueueEntry withItem: added action: SecCKKSActionAdd ckks:self error: &error];
2114 } else if(isDelete) {
2115 oqe = [CKKSOutgoingQueueEntry withItem: deleted action: SecCKKSActionDelete ckks:self error: &error];
2116 } else if(isModify) {
2117 oqe = [CKKSOutgoingQueueEntry withItem: added action: SecCKKSActionModify ckks:self error: &error];
2118 } else {
2119 ckkserror("ckks", self, "processKeychainEventItemAdded given garbage: %@ %@", added, deleted);
2120 return true;
2121 }
2122
2123 CKOperationGroup* operationGroup = [CKOperationGroup CKKSGroupWithName:@"keychain-api-use"];
2124
2125 if(error) {
2126 ckkserror("ckks", self, "Couldn't create outgoing queue entry: %@", error);
2127 self.droppedItems = true;
2128
2129 // If the problem is 'no UUID', launch a scan operation to find and fix it
2130 // We don't want to fix it up here, in the closing moments of a transaction
2131 if([error.domain isEqualToString:CKKSErrorDomain] && error.code == CKKSNoUUIDOnItem) {
2132 ckksnotice("ckks", self, "Launching scan operation to find UUID");
2133 [self scanLocalItems:@"uuid-find-scan" ckoperationGroup:operationGroup after:nil];
2134 }
2135
2136 // If the problem is 'couldn't load key', tell the key hierarchy state machine to fix it
2137 if([error.domain isEqualToString:CKKSErrorDomain] && error.code == errSecItemNotFound) {
2138 [self.pokeKeyStateMachineScheduler trigger];
2139 }
2140
2141 return true;
2142 }
2143
2144 if(rateLimiter) {
2145 NSDate* limit = nil;
2146 NSInteger value = [rateLimiter judge:oqe at:[NSDate date] limitTime:&limit];
2147 if(limit) {
2148 oqe.waitUntil = limit;
2149 SecPLLogRegisteredEvent(@"CKKSSyncing", @{ @"ratelimit" : @(value), @"accessgroup" : oqe.accessgroup});
2150 }
2151 }
2152
2153 [oqe saveToDatabaseWithConnection: dbconn error: &error];
2154 if(error) {
2155 ckkserror("ckks", self, "Couldn't save outgoing queue entry to database: %@", error);
2156 return true;
2157 } else {
2158 ckksnotice("ckks", self, "Saved %@ to outgoing queue", oqe);
2159 }
2160
2161 // This update supercedes all other local modifications to this item (_except_ those in-flight).
2162 // Delete all items in reencrypt or error.
2163 CKKSOutgoingQueueEntry* reencryptOQE = [CKKSOutgoingQueueEntry tryFromDatabase:oqe.uuid state:SecCKKSStateReencrypt zoneID:self.zoneID error:&error];
2164 if(error) {
2165 ckkserror("ckks", self, "Couldn't load reencrypt OQE sibling for %@: %@", oqe, error);
2166 }
2167 if(reencryptOQE) {
2168 [reencryptOQE deleteFromDatabase:&error];
2169 if(error) {
2170 ckkserror("ckks", self, "Couldn't delete reencrypt OQE sibling(%@) for %@: %@", reencryptOQE, oqe, error);
2171 }
2172 error = nil;
2173 }
2174
2175 CKKSOutgoingQueueEntry* errorOQE = [CKKSOutgoingQueueEntry tryFromDatabase:oqe.uuid state:SecCKKSStateError zoneID:self.zoneID error:&error];
2176 if(error) {
2177 ckkserror("ckks", self, "Couldn't load error OQE sibling for %@: %@", oqe, error);
2178 }
2179 if(errorOQE) {
2180 [errorOQE deleteFromDatabase:&error];
2181 if(error) {
2182 ckkserror("ckks", self, "Couldn't delete error OQE sibling(%@) for %@: %@", reencryptOQE, oqe, error);
2183 }
2184 }
2185
2186 [self processOutgoingQueue:operationGroup];
2187
2188 return true;
2189 }];
2190 }
2191
2192 -(void)setCurrentItemForAccessGroup:(NSData* _Nonnull)newItemPersistentRef
2193 hash:(NSData*)newItemSHA1
2194 accessGroup:(NSString*)accessGroup
2195 identifier:(NSString*)identifier
2196 replacing:(NSData* _Nullable)oldCurrentItemPersistentRef
2197 hash:(NSData*)oldItemSHA1
2198 complete:(void (^) (NSError* operror)) complete
2199 {
2200 if(accessGroup == nil || identifier == nil) {
2201 NSError* error = [NSError errorWithDomain:CKKSErrorDomain
2202 code:errSecParam
2203 description:@"No access group or identifier given"];
2204 ckkserror("ckkscurrent", self, "Cancelling request: %@", error);
2205 complete(error);
2206 return;
2207 }
2208
2209 // Not being in a CloudKit account is an automatic failure.
2210 // But, wait a good long while for the CloudKit account state to be known (in the case of daemon startup)
2211 [self.accountStateKnown wait:(SecCKKSTestsEnabled() ? 1*NSEC_PER_SEC : 30*NSEC_PER_SEC)];
2212
2213 if(self.accountStatus != CKKSAccountStatusAvailable) {
2214 NSError* error = [NSError errorWithDomain:CKKSErrorDomain
2215 code:CKKSNotLoggedIn
2216 description:@"User is not signed into iCloud."];
2217 ckksnotice("ckkscurrent", self, "Rejecting current item pointer set since we don't have an iCloud account.");
2218 complete(error);
2219 return;
2220 }
2221
2222 ckksnotice("ckkscurrent", self, "Starting change current pointer operation for %@-%@", accessGroup, identifier);
2223 CKKSUpdateCurrentItemPointerOperation* ucipo = [[CKKSUpdateCurrentItemPointerOperation alloc] initWithCKKSKeychainView:self
2224 newItem:newItemPersistentRef
2225 hash:newItemSHA1
2226 accessGroup:accessGroup
2227 identifier:identifier
2228 replacing:oldCurrentItemPersistentRef
2229 hash:oldItemSHA1
2230 ckoperationGroup:[CKOperationGroup CKKSGroupWithName:@"currentitem-api"]];
2231
2232 WEAKIFY(self);
2233 CKKSResultOperation* returnCallback = [CKKSResultOperation operationWithBlock:^{
2234 STRONGIFY(self);
2235
2236 if(ucipo.error) {
2237 ckkserror("ckkscurrent", self, "Failed setting a current item pointer for %@ with %@", ucipo.currentPointerIdentifier, ucipo.error);
2238 } else {
2239 ckksnotice("ckkscurrent", self, "Finished setting a current item pointer for %@", ucipo.currentPointerIdentifier);
2240 }
2241 complete(ucipo.error);
2242 }];
2243 returnCallback.name = @"setCurrentItem-return-callback";
2244 [returnCallback addDependency: ucipo];
2245 [self scheduleOperation: returnCallback];
2246
2247 // Now, schedule ucipo. It modifies the CloudKit zone, so it should insert itself into the list of OutgoingQueueOperations.
2248 // Then, we won't have simultaneous zone-modifying operations.
2249 [ucipo linearDependencies:self.outgoingQueueOperations];
2250
2251 // If this operation hasn't started within 60 seconds, cancel it and return a "timed out" error.
2252 [ucipo timeout:60*NSEC_PER_SEC];
2253
2254 [self scheduleOperation:ucipo];
2255 return;
2256 }
2257
2258 -(void)getCurrentItemForAccessGroup:(NSString*)accessGroup
2259 identifier:(NSString*)identifier
2260 fetchCloudValue:(bool)fetchCloudValue
2261 complete:(void (^) (NSString* uuid, NSError* operror)) complete
2262 {
2263 if(accessGroup == nil || identifier == nil) {
2264 ckksnotice("ckkscurrent", self, "Rejecting current item pointer get since no access group(%@) or identifier(%@) given", accessGroup, identifier);
2265 complete(NULL, [NSError errorWithDomain:CKKSErrorDomain
2266 code:errSecParam
2267 description:@"No access group or identifier given"]);
2268 return;
2269 }
2270
2271 // Not being in a CloudKit account is an automatic failure.
2272 // But, wait a good long while for the CloudKit account state to be known (in the case of daemon startup)
2273 [self.accountStateKnown wait:(SecCKKSTestsEnabled() ? 1*NSEC_PER_SEC : 30*NSEC_PER_SEC)];
2274
2275 if(self.accountStatus != CKKSAccountStatusAvailable) {
2276 ckksnotice("ckkscurrent", self, "Rejecting current item pointer get since we don't have an iCloud account.");
2277 complete(NULL, [NSError errorWithDomain:CKKSErrorDomain
2278 code:CKKSNotLoggedIn
2279 description:@"User is not signed into iCloud."]);
2280 return;
2281 }
2282
2283 CKKSResultOperation* fetchAndProcess = nil;
2284 if(fetchCloudValue) {
2285 fetchAndProcess = [self fetchAndProcessCKChanges:CKKSFetchBecauseCurrentItemFetchRequest];
2286 }
2287
2288 WEAKIFY(self);
2289 CKKSResultOperation* getCurrentItem = [CKKSResultOperation named:@"get-current-item-pointer" withBlock:^{
2290 if(fetchAndProcess.error) {
2291 ckksnotice("ckkscurrent", self, "Rejecting current item pointer get since fetch failed: %@", fetchAndProcess.error);
2292 complete(NULL, fetchAndProcess.error);
2293 return;
2294 }
2295
2296 STRONGIFY(self);
2297
2298 [self dispatchSync: ^bool {
2299 NSError* error = nil;
2300 NSString* currentIdentifier = [NSString stringWithFormat:@"%@-%@", accessGroup, identifier];
2301
2302 CKKSCurrentItemPointer* cip = [CKKSCurrentItemPointer fromDatabase:currentIdentifier
2303 state:SecCKKSProcessedStateLocal
2304 zoneID:self.zoneID
2305 error:&error];
2306 if(!cip || error) {
2307 ckkserror("ckkscurrent", self, "No current item pointer for %@", currentIdentifier);
2308 complete(nil, error);
2309 return false;
2310 }
2311
2312 if(!cip.currentItemUUID) {
2313 ckkserror("ckkscurrent", self, "Current item pointer is empty %@", cip);
2314 complete(nil, [NSError errorWithDomain:CKKSErrorDomain
2315 code:errSecInternalError
2316 description:@"Current item pointer is empty"]);
2317 return false;
2318 }
2319
2320 ckksinfo("ckkscurrent", self, "Retrieved current item pointer: %@", cip);
2321 complete(cip.currentItemUUID, NULL);
2322 return true;
2323 }];
2324 }];
2325
2326 [getCurrentItem addNullableDependency:fetchAndProcess];
2327 [self scheduleOperation: getCurrentItem];
2328 }
2329
2330 - (CKKSKey*) keyForItem: (SecDbItemRef) item error: (NSError * __autoreleasing *) error {
2331 CKKSKeyClass* class = nil;
2332
2333 NSString* protection = (__bridge NSString*)SecDbItemGetCachedValueWithName(item, kSecAttrAccessible);
2334 if([protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleWhenUnlocked]) {
2335 class = SecCKKSKeyClassA;
2336 } else if([protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAlwaysPrivate] ||
2337 [protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAfterFirstUnlock]) {
2338 class = SecCKKSKeyClassC;
2339 } else {
2340 NSError* localError = [NSError errorWithDomain:CKKSErrorDomain
2341 code:CKKSInvalidKeyClass
2342 description:[NSString stringWithFormat:@"can't pick key class for protection %@", protection]];
2343 ckkserror("ckks", self, "can't pick key class: %@ %@", localError, item);
2344 if(error) {
2345 *error = localError;
2346 }
2347
2348 return nil;
2349 }
2350
2351 NSError* currentKeyError = nil;
2352 CKKSKey* key = [CKKSKey currentKeyForClass: class zoneID:self.zoneID error:&currentKeyError];
2353 if(!key || currentKeyError) {
2354 ckkserror("ckks", self, "Couldn't find current key for %@: %@", class, currentKeyError);
2355
2356 if(error) {
2357 *error = currentKeyError;
2358 }
2359 return nil;
2360 }
2361
2362 // and make sure it's unwrapped.
2363 NSError* loadedError = nil;
2364 if(![key ensureKeyLoaded:&loadedError]) {
2365 ckkserror("ckks", self, "Couldn't load key(%@): %@", key, loadedError);
2366 if(error) {
2367 *error = loadedError;
2368 }
2369 return nil;
2370 }
2371
2372 return key;
2373 }
2374
2375 - (CKKSResultOperation<CKKSKeySetProviderOperationProtocol>*)findKeySet
2376 {
2377 __block CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp = nil;
2378
2379 [self dispatchSyncWithAccountKeys:^bool {
2380 CKKSCurrentKeySet* keyset = [CKKSCurrentKeySet loadForZone:self.zoneID];
2381 if(keyset.currentTLKPointer.currentKeyUUID && keyset.tlk.uuid) {
2382 ckksnotice("ckks", self, "Already have keyset %@", keyset);
2383
2384 keysetOp = [[CKKSProvideKeySetOperation alloc] initWithZoneName:self.zoneName keySet:keyset];
2385 [self scheduleOperationWithoutDependencies:keysetOp];
2386 return true;
2387 } else if([self.keyHierarchyState isEqualToString:SecCKKSZoneKeyStateWaitForTLKUpload]) {
2388 CKKSCurrentKeySet* proposedKeySet = self.lastNewTLKOperation.keyset;
2389 ckksnotice("ckks", self, "Already have proposed keyset %@", proposedKeySet);
2390
2391 keysetOp = [[CKKSProvideKeySetOperation alloc] initWithZoneName:self.zoneName keySet:proposedKeySet];
2392 [self scheduleOperationWithoutDependencies:keysetOp];
2393 return true;
2394 } else {
2395 // No existing keyset (including keys) exists.
2396 // The state machine will know what to do!
2397 self.tlkCreationRequested = true;
2398
2399 ckksnotice("ckks", self, "Received a keyset request; forwarding to state machine");
2400
2401 keysetOp = (CKKSProvideKeySetOperation*) [self findFirstPendingOperation:self.keysetProviderOperations];
2402 if(!keysetOp) {
2403 keysetOp = [[CKKSProvideKeySetOperation alloc] initWithZoneName:self.zoneName];
2404 [self.keysetProviderOperations addObject:keysetOp];
2405
2406 // This is an abuse of operations: they should generally run when added to a queue, not wait, but this allows recipients to set timeouts
2407 [self scheduleOperationWithoutDependencies:keysetOp];
2408 }
2409
2410 [self _onqueueAdvanceKeyStateMachineToState:nil withError:nil];
2411 }
2412
2413 return true;
2414 }];
2415
2416 return keysetOp;
2417 }
2418
2419 - (void)_onqueueRunKeysetProviderOperations:(CKKSCurrentKeySet*)keyset
2420 {
2421 ckksnotice("ckkskey", self, "Providing keyset (%@) to listeners", keyset);
2422
2423 // We have some keyset; they can ask again if they want a new one
2424 self.tlkCreationRequested = false;
2425
2426 for(CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* op in self.keysetProviderOperations) {
2427 if([op isPending]) {
2428 [op provideKeySet:keyset];
2429 }
2430 }
2431 }
2432
2433 - (void)receiveTLKUploadRecords:(NSArray<CKRecord*>*)records
2434 {
2435 // First, filter for records matching this zone
2436 NSMutableArray<CKRecord*>* zoneRecords = [NSMutableArray array];
2437 for(CKRecord* record in records) {
2438 if([record.recordID.zoneID isEqual:self.zoneID]) {
2439 [zoneRecords addObject:record];
2440 }
2441 }
2442
2443 ckksnotice("ckkskey", self, "Received a set of %lu TLK upload records", (unsigned long)zoneRecords.count);
2444
2445 if(!zoneRecords || zoneRecords.count == 0) {
2446 return;
2447 }
2448
2449 [self dispatchSyncWithAccountKeys:^bool {
2450
2451 for(CKRecord* record in zoneRecords) {
2452 [self _onqueueCKRecordChanged:record resync:false];
2453 }
2454
2455 return true;
2456 }];
2457 }
2458
2459 // Use the following method to find the first pending operation in a weak collection
2460 - (NSOperation*)findFirstPendingOperation: (NSHashTable*) table {
2461 return [self findFirstPendingOperation:table ofClass:nil];
2462 }
2463
2464 // Use the following method to find the first pending operation in a weak collection
2465 - (NSOperation*)findFirstPendingOperation: (NSHashTable*) table ofClass:(Class)class {
2466 @synchronized(table) {
2467 for(NSOperation* op in table) {
2468 if(op != nil && [op isPending] && (class == nil || [op isKindOfClass: class])) {
2469 return op;
2470 }
2471 }
2472 return nil;
2473 }
2474 }
2475
2476 // Use the following method to count the pending operations in a weak collection
2477 - (int64_t)countPendingOperations: (NSHashTable*) table {
2478 @synchronized(table) {
2479 int count = 0;
2480 for(NSOperation* op in table) {
2481 if(op != nil && !([op isExecuting] || [op isFinished])) {
2482 count++;
2483 }
2484 }
2485 return count;
2486 }
2487 }
2488
2489 - (CKKSOutgoingQueueOperation*)processOutgoingQueue:(CKOperationGroup*)ckoperationGroup {
2490 return [self processOutgoingQueueAfter:nil ckoperationGroup:ckoperationGroup];
2491 }
2492
2493 - (CKKSOutgoingQueueOperation*)processOutgoingQueueAfter:(CKKSResultOperation*)after ckoperationGroup:(CKOperationGroup*)ckoperationGroup {
2494 return [self processOutgoingQueueAfter:after requiredDelay:DISPATCH_TIME_FOREVER ckoperationGroup:ckoperationGroup];
2495 }
2496
2497 - (CKKSOutgoingQueueOperation*)processOutgoingQueueAfter:(CKKSResultOperation*)after
2498 requiredDelay:(uint64_t)requiredDelay
2499 ckoperationGroup:(CKOperationGroup*)ckoperationGroup
2500 {
2501 CKKSOutgoingQueueOperation* outgoingop =
2502 (CKKSOutgoingQueueOperation*) [self findFirstPendingOperation:self.outgoingQueueOperations
2503 ofClass:[CKKSOutgoingQueueOperation class]];
2504 if(outgoingop) {
2505 if(after) {
2506 [outgoingop addDependency: after];
2507 }
2508 if([outgoingop isPending]) {
2509 if(!outgoingop.ckoperationGroup && ckoperationGroup) {
2510 outgoingop.ckoperationGroup = ckoperationGroup;
2511 } else if(ckoperationGroup) {
2512 ckkserror("ckks", self, "Throwing away CKOperationGroup(%@) in favor of (%@)", ckoperationGroup.name, outgoingop.ckoperationGroup.name);
2513 }
2514
2515 // Will log any pending dependencies as well
2516 ckksnotice("ckksoutgoing", self, "Returning existing %@", outgoingop);
2517
2518 // Shouldn't be necessary, but can't hurt
2519 [self.outgoingQueueOperationScheduler triggerAt:requiredDelay];
2520 return outgoingop;
2521 }
2522 }
2523
2524 CKKSOutgoingQueueOperation* op = [[CKKSOutgoingQueueOperation alloc] initWithCKKSKeychainView:self ckoperationGroup:ckoperationGroup];
2525 op.name = @"outgoing-queue-operation";
2526 [op addNullableDependency:after];
2527 [op addNullableDependency:self.outgoingQueueOperationScheduler.operationDependency];
2528
2529 [self.outgoingQueueOperationScheduler triggerAt:requiredDelay];
2530
2531 [op linearDependencies:self.outgoingQueueOperations];
2532
2533 [self scheduleOperation: op];
2534 ckksnotice("ckksoutgoing", self, "Scheduled %@", op);
2535 return op;
2536 }
2537
2538 - (void)processIncomingQueueAfterNextUnlock {
2539 // Thread races aren't so important here; we might end up with two or three copies of this operation, but that's okay.
2540 if(![self.processIncomingQueueAfterNextUnlockOperation isPending]) {
2541 WEAKIFY(self);
2542
2543 CKKSResultOperation* restartIncomingQueueOperation = [CKKSResultOperation operationWithBlock:^{
2544 STRONGIFY(self);
2545 // This IQO shouldn't error if the keybag has locked again. It will simply try again later.
2546 [self processIncomingQueue:false];
2547 }];
2548
2549 restartIncomingQueueOperation.name = @"reprocess-incoming-queue-after-unlock";
2550 self.processIncomingQueueAfterNextUnlockOperation = restartIncomingQueueOperation;
2551
2552 [restartIncomingQueueOperation addNullableDependency:self.lockStateTracker.unlockDependency];
2553 [self scheduleOperation: restartIncomingQueueOperation];
2554 }
2555 }
2556
2557 - (CKKSResultOperation*)resultsOfNextProcessIncomingQueueOperation {
2558 if(self.resultsOfNextIncomingQueueOperationOperation && [self.resultsOfNextIncomingQueueOperationOperation isPending]) {
2559 return self.resultsOfNextIncomingQueueOperationOperation;
2560 }
2561
2562 // Else, make a new one.
2563 self.resultsOfNextIncomingQueueOperationOperation = [CKKSResultOperation named:[NSString stringWithFormat:@"wait-for-next-incoming-queue-operation-%@", self.zoneName] withBlock:^{}];
2564 return self.resultsOfNextIncomingQueueOperationOperation;
2565 }
2566
2567 - (CKKSIncomingQueueOperation*)processIncomingQueue:(bool)failOnClassA {
2568 return [self processIncomingQueue:failOnClassA after: nil];
2569 }
2570
2571 - (CKKSIncomingQueueOperation*) processIncomingQueue:(bool)failOnClassA after: (CKKSResultOperation*) after {
2572 CKKSIncomingQueueOperation* incomingop = (CKKSIncomingQueueOperation*) [self findFirstPendingOperation:self.incomingQueueOperations];
2573 if(incomingop) {
2574 ckksinfo("ckks", self, "Skipping processIncomingQueue due to at least one pending instance");
2575 if(after) {
2576 [incomingop addNullableDependency: after];
2577 }
2578
2579 // check (again) for race condition; if the op has started we need to add another (for the dependency)
2580 if([incomingop isPending]) {
2581 incomingop.errorOnClassAFailure |= failOnClassA;
2582 return incomingop;
2583 }
2584 }
2585
2586 CKKSIncomingQueueOperation* op = [[CKKSIncomingQueueOperation alloc] initWithCKKSKeychainView:self errorOnClassAFailure:failOnClassA];
2587 op.name = @"incoming-queue-operation";
2588 if(after != nil) {
2589 [op addSuccessDependency: after];
2590 }
2591
2592 if(self.resultsOfNextIncomingQueueOperationOperation) {
2593 [self.resultsOfNextIncomingQueueOperationOperation addSuccessDependency:op];
2594 [self scheduleOperation:self.resultsOfNextIncomingQueueOperationOperation];
2595 }
2596
2597 [self scheduleOperation: op];
2598 return op;
2599 }
2600
2601 - (CKKSScanLocalItemsOperation*)scanLocalItems:(NSString*)operationName {
2602 return [self scanLocalItems:operationName ckoperationGroup:nil after:nil];
2603 }
2604
2605 - (CKKSScanLocalItemsOperation*)scanLocalItems:(NSString*)operationName
2606 ckoperationGroup:(CKOperationGroup*)operationGroup
2607 after:(NSOperation*)after
2608 {
2609 CKKSScanLocalItemsOperation* scanOperation = (CKKSScanLocalItemsOperation*)[self findFirstPendingOperation:self.scanLocalItemsOperations];
2610
2611 if(scanOperation) {
2612 [scanOperation addNullableDependency:after];
2613
2614 // check (again) for race condition; if the op has started we need to add another (for the dependency)
2615 if([scanOperation isPending]) {
2616 scanOperation.ckoperationGroup = operationGroup;
2617
2618 scanOperation.name = [NSString stringWithFormat:@"%@::%@", scanOperation.name, operationName];
2619 return scanOperation;
2620 }
2621 }
2622
2623 scanOperation = [[CKKSScanLocalItemsOperation alloc] initWithCKKSKeychainView:self ckoperationGroup:operationGroup];
2624 scanOperation.name = operationName;
2625
2626 [scanOperation addNullableDependency:self.lastFixupOperation];
2627 [scanOperation addNullableDependency:self.lockStateTracker.unlockDependency];
2628 [scanOperation addNullableDependency:self.keyStateReadyDependency];
2629 [scanOperation addNullableDependency:after];
2630
2631 [scanOperation linearDependencies:self.scanLocalItemsOperations];
2632
2633 [self scheduleOperation:scanOperation];
2634 return scanOperation;
2635 }
2636
2637 - (CKKSUpdateDeviceStateOperation*)updateDeviceState:(bool)rateLimit
2638 waitForKeyHierarchyInitialization:(uint64_t)timeout
2639 ckoperationGroup:(CKOperationGroup*)ckoperationGroup {
2640
2641 WEAKIFY(self);
2642
2643 // If securityd just started, the key state might be in some transient early state. Wait a bit.
2644 CKKSResultOperation* waitForKeyReady = [CKKSResultOperation named:@"device-state-wait" withBlock:^{
2645 STRONGIFY(self);
2646 ckksnotice("ckksdevice", self, "Finished waiting for key hierarchy transient state, currently %@", self.keyHierarchyState);
2647 }];
2648
2649 [waitForKeyReady addNullableDependency:self.keyStateNonTransientDependency];
2650 [waitForKeyReady timeout:timeout];
2651 [self.waitingQueue addOperation:waitForKeyReady];
2652
2653 CKKSUpdateDeviceStateOperation* op = [[CKKSUpdateDeviceStateOperation alloc] initWithCKKSKeychainView:self rateLimit:rateLimit ckoperationGroup:ckoperationGroup];
2654 op.name = @"device-state-operation";
2655
2656 [op addDependency: waitForKeyReady];
2657
2658 // op modifies the CloudKit zone, so it should insert itself into the list of OutgoingQueueOperations.
2659 // Then, we won't have simultaneous zone-modifying operations and confuse ourselves.
2660 // However, since we might have pending OQOs, it should try to insert itself at the beginning of the linearized list
2661 [op linearDependenciesWithSelfFirst:self.outgoingQueueOperations];
2662
2663 // CKKSUpdateDeviceStateOperations are special: they should fire even if we don't believe we're in an iCloud account.
2664 // They also shouldn't block or be blocked by any other operation; our wait operation above will handle that
2665 [self scheduleOperationWithoutDependencies:op];
2666 return op;
2667 }
2668
2669 // There are some errors which won't be reported but will be reflected in the CDSE; any error coming out of here is fatal
2670 - (CKKSDeviceStateEntry*)_onqueueCurrentDeviceStateEntry: (NSError* __autoreleasing*)error {
2671 NSError* localerror = nil;
2672
2673 CKKSAccountStateTracker* accountTracker = self.accountTracker;
2674 CKKSAccountStatus hsa2Status = accountTracker.hsa2iCloudAccountStatus;
2675
2676 // We must have an HSA2 iCloud account and a CloudKit account to even create one of these
2677 if(hsa2Status != CKKSAccountStatusAvailable ||
2678 accountTracker.currentCKAccountInfo.accountStatus != CKAccountStatusAvailable) {
2679 ckkserror("ckksdevice", self, "No iCloud account active: %@ hsa2 account:%@",
2680 accountTracker.currentCKAccountInfo,
2681 CKKSAccountStatusToString(hsa2Status));
2682 localerror = [NSError errorWithDomain:@"securityd"
2683 code:errSecInternalError
2684 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"No active HSA2 iCloud account: %@", accountTracker.currentCKAccountInfo]}];
2685 if(error) {
2686 *error = localerror;
2687 }
2688 return nil;
2689 }
2690
2691 NSString* ckdeviceID = accountTracker.ckdeviceID;
2692 if(ckdeviceID == nil) {
2693 ckkserror("ckksdevice", self, "No CK device ID available; cannot make device state entry");
2694 localerror = [NSError errorWithDomain:CKKSErrorDomain
2695 code:CKKSNotLoggedIn
2696 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"No CK device ID: %@", accountTracker.currentCKAccountInfo]}];
2697 if(error) {
2698 *error = localerror;
2699 }
2700 return nil;
2701 }
2702
2703 CKKSDeviceStateEntry* oldcdse = [CKKSDeviceStateEntry tryFromDatabase:ckdeviceID zoneID:self.zoneID error:&localerror];
2704 if(localerror) {
2705 ckkserror("ckksdevice", self, "Couldn't read old CKKSDeviceStateEntry from database: %@", localerror);
2706 if(error) {
2707 *error = localerror;
2708 }
2709 return nil;
2710 }
2711
2712 // Find out what we think the current keys are
2713 CKKSCurrentKeyPointer* currentTLKPointer = [CKKSCurrentKeyPointer tryFromDatabase: SecCKKSKeyClassTLK zoneID:self.zoneID error:&localerror];
2714 CKKSCurrentKeyPointer* currentClassAPointer = [CKKSCurrentKeyPointer tryFromDatabase: SecCKKSKeyClassA zoneID:self.zoneID error:&localerror];
2715 CKKSCurrentKeyPointer* currentClassCPointer = [CKKSCurrentKeyPointer tryFromDatabase: SecCKKSKeyClassC zoneID:self.zoneID error:&localerror];
2716 if(localerror) {
2717 // Things is broken, but the whole point of this record is to share the brokenness. Continue.
2718 ckkserror("ckksdevice", self, "Couldn't read current key pointers from database: %@; proceeding", localerror);
2719 localerror = nil;
2720 }
2721
2722 CKKSKey* suggestedTLK = currentTLKPointer.currentKeyUUID ? [CKKSKey tryFromDatabase:currentTLKPointer.currentKeyUUID zoneID:self.zoneID error:&localerror] : nil;
2723 CKKSKey* suggestedClassAKey = currentClassAPointer.currentKeyUUID ? [CKKSKey tryFromDatabase:currentClassAPointer.currentKeyUUID zoneID:self.zoneID error:&localerror] : nil;
2724 CKKSKey* suggestedClassCKey = currentClassCPointer.currentKeyUUID ? [CKKSKey tryFromDatabase:currentClassCPointer.currentKeyUUID zoneID:self.zoneID error:&localerror] : nil;
2725
2726 if(localerror) {
2727 // Things is broken, but the whole point of this record is to share the brokenness. Continue.
2728 ckkserror("ckksdevice", self, "Couldn't read keys from database: %@; proceeding", localerror);
2729 localerror = nil;
2730 }
2731
2732 // Check if we posess the keys in the keychain
2733 [suggestedTLK ensureKeyLoaded:&localerror];
2734 if(localerror && [self.lockStateTracker isLockedError:localerror]) {
2735 ckkserror("ckksdevice", self, "Device is locked; couldn't read TLK from keychain. Assuming it is present and continuing; error was %@", localerror);
2736 localerror = nil;
2737 } else if(localerror) {
2738 ckkserror("ckksdevice", self, "Couldn't read TLK from keychain. We do not have a current TLK. Error was %@", localerror);
2739 suggestedTLK = nil;
2740 }
2741
2742 [suggestedClassAKey ensureKeyLoaded:&localerror];
2743 if(localerror && [self.lockStateTracker isLockedError:localerror]) {
2744 ckkserror("ckksdevice", self, "Device is locked; couldn't read ClassA key from keychain. Assuming it is present and continuing; error was %@", localerror);
2745 localerror = nil;
2746 } else if(localerror) {
2747 ckkserror("ckksdevice", self, "Couldn't read ClassA key from keychain. We do not have a current ClassA key. Error was %@", localerror);
2748 suggestedClassAKey = nil;
2749 }
2750
2751 [suggestedClassCKey ensureKeyLoaded:&localerror];
2752 // class C keys are stored class C, so uh, don't check lock state.
2753 if(localerror) {
2754 ckkserror("ckksdevice", self, "Couldn't read ClassC key from keychain. We do not have a current ClassC key. Error was %@", localerror);
2755 suggestedClassCKey = nil;
2756 }
2757
2758 // We'd like to have the circle peer ID. Give the account state tracker a fighting chance, but not having it is not an error
2759 // But, if the platform doesn't have SOS, don't bother
2760 if(OctagonPlatformSupportsSOS() && [accountTracker.accountCirclePeerIDInitialized wait:500*NSEC_PER_MSEC] != 0 && !accountTracker.accountCirclePeerID) {
2761 ckkserror("ckksdevice", self, "No SOS peer ID available");
2762 }
2763
2764 // We'd also like the Octagon status
2765 if([accountTracker.octagonInformationInitialized wait:500*NSEC_PER_MSEC] != 0 && !accountTracker.octagonPeerID) {
2766 ckkserror("ckksdevice", self, "No octagon peer ID available");
2767 }
2768
2769 // Reset the last unlock time to 'day' granularity in UTC
2770 NSCalendar* calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierISO8601];
2771 calendar.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
2772 NSDate* lastUnlockDay = self.lockStateTracker.lastUnlockTime;
2773 lastUnlockDay = lastUnlockDay ? [calendar startOfDayForDate:lastUnlockDay] : nil;
2774
2775 // We only really want the oldcdse for its encodedCKRecord, so make a new cdse here
2776 CKKSDeviceStateEntry* newcdse = [[CKKSDeviceStateEntry alloc] initForDevice:ckdeviceID
2777 osVersion:SecCKKSHostOSVersion()
2778 lastUnlockTime:lastUnlockDay
2779 octagonPeerID:accountTracker.octagonPeerID
2780 octagonStatus:accountTracker.octagonStatus
2781 circlePeerID:accountTracker.accountCirclePeerID
2782 circleStatus:accountTracker.currentCircleStatus.status
2783 keyState:self.keyHierarchyState
2784 currentTLKUUID:suggestedTLK.uuid
2785 currentClassAUUID:suggestedClassAKey.uuid
2786 currentClassCUUID:suggestedClassCKey.uuid
2787 zoneID:self.zoneID
2788 encodedCKRecord:oldcdse.encodedCKRecord];
2789 return newcdse;
2790 }
2791
2792 - (CKKSSynchronizeOperation*) resyncWithCloud {
2793 CKKSSynchronizeOperation* op = [[CKKSSynchronizeOperation alloc] initWithCKKSKeychainView: self];
2794 [self scheduleOperation: op];
2795 return op;
2796 }
2797
2798 - (CKKSLocalSynchronizeOperation*)resyncLocal {
2799 CKKSLocalSynchronizeOperation* op = [[CKKSLocalSynchronizeOperation alloc] initWithCKKSKeychainView:self];
2800 [self scheduleOperation: op];
2801 return op;
2802 }
2803
2804 - (CKKSResultOperation*)fetchAndProcessCKChanges:(CKKSFetchBecause*)because {
2805 return [self fetchAndProcessCKChanges:because after:nil];
2806 }
2807
2808 - (CKKSResultOperation*)fetchAndProcessCKChanges:(CKKSFetchBecause*)because after:(CKKSResultOperation*)after {
2809 if(!SecCKKSIsEnabled()) {
2810 ckksinfo("ckks", self, "Skipping fetchAndProcessCKChanges due to disabled CKKS");
2811 return nil;
2812 }
2813
2814 if(after) {
2815 [self.zoneChangeFetcher holdFetchesUntil:after];
2816 }
2817
2818 // We fetched some changes; try to process them!
2819 return [self processIncomingQueue:false after:[self.zoneChangeFetcher requestSuccessfulFetch:because]];
2820 }
2821
2822 - (CKKSResultOperation*)fetchAndProcessCKChangesDueToAPNS:(CKRecordZoneNotification*)notification {
2823 if(!SecCKKSIsEnabled()) {
2824 ckksinfo("ckks", self, "Skipping fetchAndProcessCKChanges due to disabled CKKS");
2825 return nil;
2826 }
2827
2828 CKKSResultOperation *fetchOp = [self.zoneChangeFetcher requestFetchDueToAPNS:notification];
2829 if (fetchOp == nil) {
2830 ckksnotice("ckks", self, "Skipping push induced processCKChanges due to zones are not ready");
2831 return nil;
2832 }
2833
2834 // We fetched some changes; try to process them!
2835 return [self processIncomingQueue:false after:fetchOp];
2836 }
2837
2838 // Lets the view know about a failed CloudKit write. If the error is "already have one of these records", it will
2839 // store the new records and kick off the new processing
2840 //
2841 // Note that you need to tell this function the records you wanted to save, so it can determine what needs deletion
2842 - (bool)_onqueueCKWriteFailed:(NSError*)ckerror attemptedRecordsChanged:(NSDictionary<CKRecordID*, CKRecord*>*)savedRecords {
2843 dispatch_assert_queue(self.queue);
2844
2845 NSDictionary<CKRecordID*,NSError*>* partialErrors = ckerror.userInfo[CKPartialErrorsByItemIDKey];
2846 if([ckerror.domain isEqual:CKErrorDomain] && ckerror.code == CKErrorPartialFailure && partialErrors) {
2847 // Check if this error was "you're out of date"
2848 bool recordChanged = true;
2849
2850 for(NSError* error in partialErrors.allValues) {
2851 if((![error.domain isEqual:CKErrorDomain]) || (error.code != CKErrorBatchRequestFailed && error.code != CKErrorServerRecordChanged && error.code != CKErrorUnknownItem)) {
2852 // There's an error in there that isn't CKErrorServerRecordChanged, CKErrorBatchRequestFailed, or CKErrorUnknownItem. Don't handle nicely...
2853 recordChanged = false;
2854 }
2855 }
2856
2857 if(recordChanged) {
2858 ckksnotice("ckks", self, "Received a ServerRecordChanged error, attempting to update new records and delete unknown ones");
2859
2860 bool updatedRecord = false;
2861
2862 for(CKRecordID* recordID in partialErrors.allKeys) {
2863 NSError* error = partialErrors[recordID];
2864 if([error.domain isEqual:CKErrorDomain] && error.code == CKErrorServerRecordChanged) {
2865 CKRecord* newRecord = error.userInfo[CKRecordChangedErrorServerRecordKey];
2866 ckksnotice("ckks", self, "On error: updating our idea of: %@", newRecord);
2867
2868 updatedRecord |= [self _onqueueCKRecordChanged:newRecord resync:true];
2869 } else if([error.domain isEqual:CKErrorDomain] && error.code == CKErrorUnknownItem) {
2870 CKRecord* record = savedRecords[recordID];
2871 ckksnotice("ckks", self, "On error: handling an unexpected delete of: %@ %@", recordID, record);
2872
2873 updatedRecord |= [self _onqueueCKRecordDeleted:recordID recordType:record.recordType resync:true];
2874 }
2875 }
2876
2877 if(updatedRecord) {
2878 [self processIncomingQueue:false];
2879 return true;
2880 }
2881 }
2882
2883 // Check if this error was the CKKS server extension rejecting the write
2884 for(CKRecordID* recordID in partialErrors.allKeys) {
2885 NSError* error = partialErrors[recordID];
2886
2887 NSError* underlyingError = error.userInfo[NSUnderlyingErrorKey];
2888 NSError* thirdLevelError = underlyingError.userInfo[NSUnderlyingErrorKey];
2889 ckksnotice("ckks", self, "Examining 'write failed' error: %@ %@ %@", error, underlyingError, thirdLevelError);
2890
2891 if([error.domain isEqualToString:CKErrorDomain] && error.code == CKErrorServerRejectedRequest &&
2892 underlyingError && [underlyingError.domain isEqualToString:CKInternalErrorDomain] && underlyingError.code == CKErrorInternalPluginError &&
2893 thirdLevelError && [thirdLevelError.domain isEqualToString:@"CloudkitKeychainService"]) {
2894
2895 if(thirdLevelError.code == CKKSServerUnexpectedSyncKeyInChain) {
2896 // The server thinks the classA/C synckeys don't wrap directly the to top TLK, but we don't (otherwise, we would have fixed it).
2897 // Issue a key hierarchy fetch and see what's what.
2898 ckkserror("ckks", self, "CKKS Server extension has told us about %@ for record %@; requesting refetch and reprocess of key hierarchy", thirdLevelError, recordID);
2899 [self _onqueueKeyStateMachineRequestFetch];
2900
2901 } else if(thirdLevelError.code == CKKSServerMissingRecord) {
2902 // The server is concerned that there's a missing record somewhere.
2903 // Issue a key hierarchy fetch and see what's happening
2904 ckkserror("ckks", self, "CKKS Server extension has told us about %@ for record %@; requesting refetch and reprocess of key hierarchy", thirdLevelError, recordID);
2905 [self _onqueueKeyStateMachineRequestFetch];
2906
2907 } else {
2908 ckkserror("ckks", self, "CKKS Server extension has told us about %@ for record %@, but we don't currently handle this error", thirdLevelError, recordID);
2909 }
2910 }
2911 }
2912 }
2913
2914 return false;
2915 }
2916
2917 - (bool)_onqueueCKRecordDeleted:(CKRecordID*)recordID recordType:(NSString*)recordType resync:(bool)resync {
2918 dispatch_assert_queue(self.queue);
2919
2920 // TODO: resync doesn't really mean much here; what does it mean for a record to be 'deleted' if you're fetching from scratch?
2921
2922 if([recordType isEqual: SecCKRecordItemType]) {
2923 ckksinfo("ckks", self, "CloudKit notification: deleted record(%@): %@", recordType, recordID);
2924 NSError* error = nil;
2925 NSError* iqeerror = nil;
2926 CKKSMirrorEntry* ckme = [CKKSMirrorEntry fromDatabase: [recordID recordName] zoneID:self.zoneID error: &error];
2927
2928 // Deletes always succeed, not matter the generation count
2929 if(ckme) {
2930 [ckme deleteFromDatabase:&error];
2931
2932 CKKSIncomingQueueEntry* iqe = [[CKKSIncomingQueueEntry alloc] initWithCKKSItem:ckme.item action:SecCKKSActionDelete state:SecCKKSStateNew];
2933 [iqe saveToDatabase:&iqeerror];
2934 if(iqeerror) {
2935 ckkserror("ckks", self, "Couldn't save incoming queue entry: %@", iqeerror);
2936 }
2937 }
2938 ckksinfo("ckks", self, "CKKSMirrorEntry was deleted: %@ %@ error: %@", recordID, ckme, error);
2939 // TODO: actually pass error back up
2940 return (error == nil);
2941
2942 } else if([recordType isEqual: SecCKRecordCurrentItemType]) {
2943 ckksinfo("ckks", self, "CloudKit notification: deleted current item pointer(%@): %@", recordType, recordID);
2944 NSError* error = nil;
2945
2946 [[CKKSCurrentItemPointer tryFromDatabase:[recordID recordName] state:SecCKKSProcessedStateRemote zoneID:self.zoneID error:&error] deleteFromDatabase:&error];
2947 [[CKKSCurrentItemPointer fromDatabase:[recordID recordName] state:SecCKKSProcessedStateLocal zoneID:self.zoneID error:&error] deleteFromDatabase:&error];
2948
2949 ckksinfo("ckks", self, "CKKSCurrentItemPointer was deleted: %@ error: %@", recordID, error);
2950 return (error == nil);
2951
2952 } else if([recordType isEqual: SecCKRecordIntermediateKeyType]) {
2953 // TODO: handle in some interesting way
2954 return true;
2955 } else if([recordType isEqual: SecCKRecordTLKShareType]) {
2956 NSError* error = nil;
2957 ckksinfo("ckks", self, "CloudKit notification: deleted tlk share record(%@): %@", recordType, recordID);
2958 CKKSTLKShareRecord* share = [CKKSTLKShareRecord tryFromDatabaseFromCKRecordID:recordID error:&error];
2959 [share deleteFromDatabase:&error];
2960
2961 if(error) {
2962 ckkserror("ckks", self, "CK notification: Couldn't delete deleted TLKShare: %@ %@", recordID, error);
2963 }
2964 return (error == nil);
2965
2966 } else if([recordType isEqual: SecCKRecordDeviceStateType]) {
2967 NSError* error = nil;
2968 ckksinfo("ckks", self, "CloudKit notification: deleted device state record(%@): %@", recordType, recordID);
2969
2970 CKKSDeviceStateEntry* cdse = [CKKSDeviceStateEntry tryFromDatabaseFromCKRecordID:recordID error:&error];
2971 [cdse deleteFromDatabase: &error];
2972 ckksinfo("ckks", self, "CKKSCurrentItemPointer(%@) was deleted: %@ error: %@", cdse, recordID, error);
2973
2974 return (error == nil);
2975
2976 } else if ([recordType isEqualToString:SecCKRecordManifestType]) {
2977 ckksinfo("ckks", self, "CloudKit notification: deleted manifest record (%@): %@", recordType, recordID);
2978
2979 NSError* error = nil;
2980 CKKSManifest* manifest = [CKKSManifest manifestForRecordName:recordID.recordName error:&error];
2981 if (manifest) {
2982 [manifest deleteFromDatabase:&error];
2983 }
2984
2985 ckksinfo("ckks", self, "CKKSManifest was deleted: %@ %@ error: %@", recordID, manifest, error);
2986 // TODO: actually pass error back up
2987 return error == nil;
2988 }
2989
2990 else {
2991 ckkserror("ckksfetch", self, "unknown record type: %@ %@", recordType, recordID);
2992 return false;
2993 }
2994 }
2995
2996 - (bool)_onqueueCKRecordChanged:(CKRecord*)record resync:(bool)resync {
2997 dispatch_assert_queue(self.queue);
2998
2999 @autoreleasepool {
3000 ckksnotice("ckksfetch", self, "Processing record modification(%@): %@", record.recordType, record);
3001
3002 if([[record recordType] isEqual: SecCKRecordItemType]) {
3003 [self _onqueueCKRecordItemChanged:record resync:resync];
3004 return true;
3005 } else if([[record recordType] isEqual: SecCKRecordCurrentItemType]) {
3006 [self _onqueueCKRecordCurrentItemPointerChanged:record resync:resync];
3007 return true;
3008 } else if([[record recordType] isEqual: SecCKRecordIntermediateKeyType]) {
3009 [self _onqueueCKRecordKeyChanged:record resync:resync];
3010 return true;
3011 } else if ([[record recordType] isEqual: SecCKRecordTLKShareType]) {
3012 [self _onqueueCKRecordTLKShareChanged:record resync:resync];
3013 return true;
3014 } else if([[record recordType] isEqualToString: SecCKRecordCurrentKeyType]) {
3015 [self _onqueueCKRecordCurrentKeyPointerChanged:record resync:resync];
3016 return true;
3017 } else if ([[record recordType] isEqualToString:SecCKRecordManifestType]) {
3018 [self _onqueueCKRecordManifestChanged:record resync:resync];
3019 return true;
3020 } else if ([[record recordType] isEqualToString:SecCKRecordManifestLeafType]) {
3021 [self _onqueueCKRecordManifestLeafChanged:record resync:resync];
3022 return true;
3023 } else if ([[record recordType] isEqualToString:SecCKRecordDeviceStateType]) {
3024 [self _onqueueCKRecordDeviceStateChanged:record resync:resync];
3025 return true;
3026 } else {
3027 ckkserror("ckksfetch", self, "unknown record type: %@ %@", [record recordType], record);
3028 return false;
3029 }
3030 }
3031 }
3032
3033 - (void)_onqueueCKRecordItemChanged:(CKRecord*)record resync:(bool)resync {
3034 dispatch_assert_queue(self.queue);
3035
3036 NSError* error = nil;
3037 // Find if we knew about this record in the past
3038 bool update = false;
3039 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase: [[record recordID] recordName] zoneID:self.zoneID error:&error];
3040
3041 if(error) {
3042 ckkserror("ckks", self, "error loading a CKKSMirrorEntry from database: %@", error);
3043 // TODO: quit?
3044 }
3045
3046 if(resync) {
3047 if(!ckme) {
3048 ckkserror("ckksresync", self, "BUG: No local item matching resynced CloudKit record: %@", record);
3049 } else if(![ckme matchesCKRecord:record]) {
3050 ckkserror("ckksresync", self, "BUG: Local item doesn't match resynced CloudKit record: %@ %@", ckme, record);
3051 } else {
3052 ckksnotice("ckksresync", self, "Already know about this item record, updating anyway: %@", record.recordID);
3053 }
3054 }
3055
3056 if(ckme && ckme.item && ckme.item.generationCount > [record[SecCKRecordGenerationCountKey] unsignedLongLongValue]) {
3057 ckkserror("ckks", self, "received a record from CloudKit with a bad generation count: %@ (%ld > %@)", ckme.uuid,
3058 (long) ckme.item.generationCount,
3059 record[SecCKRecordGenerationCountKey]);
3060 // Abort processing this record.
3061 return;
3062 }
3063
3064 // If we found an old version in the database; this might be an update
3065 if(ckme) {
3066 if([ckme matchesCKRecord:record] && !resync) {
3067 // This is almost certainly a record we uploaded; CKFetchChanges sends them back as new records
3068 ckksnotice("ckks", self, "CloudKit has told us of record we already know about; skipping update");
3069 return;
3070 }
3071
3072 update = true;
3073 // Set the CKKSMirrorEntry's fields to be whatever this record holds
3074 [ckme setFromCKRecord: record];
3075 } else {
3076 // Have to make a new CKKSMirrorEntry
3077 ckme = [[CKKSMirrorEntry alloc] initWithCKRecord: record];
3078 }
3079
3080 [ckme saveToDatabase: &error];
3081
3082 if(error) {
3083 ckkserror("ckks", self, "couldn't save new CKRecord to database: %@ %@", record, error);
3084 } else {
3085 ckksdebug("ckks", self, "CKKSMirrorEntry was created: %@", ckme);
3086 }
3087
3088 NSError* iqeerror = nil;
3089 CKKSIncomingQueueEntry* iqe = [[CKKSIncomingQueueEntry alloc] initWithCKKSItem:ckme.item
3090 action:(update ? SecCKKSActionModify : SecCKKSActionAdd)
3091 state:SecCKKSStateNew];
3092 [iqe saveToDatabase:&iqeerror];
3093 if(iqeerror) {
3094 ckkserror("ckks", self, "Couldn't save modified incoming queue entry: %@", iqeerror);
3095 } else {
3096 ckksdebug("ckks", self, "CKKSIncomingQueueEntry was created: %@", iqe);
3097 }
3098
3099 // A remote change has occured for this record. Delete any pending local changes; they will be overwritten.
3100 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry tryFromDatabase:ckme.uuid state: SecCKKSStateNew zoneID:self.zoneID error: &error];
3101 if(error) {
3102 ckkserror("ckks", self, "Couldn't load OutgoingQueueEntry: %@", error);
3103 }
3104 if(oqe) {
3105 [self _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateDeleted error:&error];
3106 }
3107
3108 // Reencryptions are pending changes too
3109 oqe = [CKKSOutgoingQueueEntry tryFromDatabase:ckme.uuid state: SecCKKSStateReencrypt zoneID:self.zoneID error: &error];
3110 if(error) {
3111 ckkserror("ckks", self, "Couldn't load reencrypted OutgoingQueueEntry: %@", error);
3112 }
3113 if(oqe) {
3114 [oqe deleteFromDatabase:&error];
3115 if(error) {
3116 ckkserror("ckks", self, "Couldn't delete reencrypted oqe(%@): %@", oqe, error);
3117 }
3118 }
3119 }
3120
3121 - (void)_onqueueCKRecordKeyChanged:(CKRecord*)record resync:(bool)resync {
3122 dispatch_assert_queue(self.queue);
3123
3124 NSError* error = nil;
3125
3126 if(resync) {
3127 NSError* resyncerror = nil;
3128
3129 CKKSKey* key = [CKKSKey tryFromDatabaseAnyState:record.recordID.recordName zoneID:self.zoneID error:&resyncerror];
3130 if(resyncerror) {
3131 ckkserror("ckksresync", self, "error loading key: %@", resyncerror);
3132 }
3133 if(!key) {
3134 ckkserror("ckksresync", self, "BUG: No sync key matching resynced CloudKit record: %@", record);
3135 } else if(![key matchesCKRecord:record]) {
3136 ckkserror("ckksresync", self, "BUG: Local sync key doesn't match resynced CloudKit record(s): %@ %@", key, record);
3137 } else {
3138 ckksnotice("ckksresync", self, "Already know about this sync key, skipping update: %@", record);
3139 return;
3140 }
3141 }
3142
3143 CKKSKey* remotekey = [[CKKSKey alloc] initWithCKRecord: record];
3144
3145 // Do we already know about this key?
3146 CKKSKey* possibleLocalKey = [CKKSKey tryFromDatabase:remotekey.uuid zoneID:self.zoneID error:&error];
3147 if(error) {
3148 ckkserror("ckkskey", self, "Error findibg exsiting local key for %@: %@", remotekey, error);
3149 // Go on, assuming there isn't a local key
3150 } else if(possibleLocalKey && [possibleLocalKey matchesCKRecord:record]) {
3151 // Okay, nothing new here. Update the CKRecord and move on.
3152 // Note: If the new record doesn't match the local copy, we have to go through the whole dance below
3153 possibleLocalKey.storedCKRecord = record;
3154 [possibleLocalKey saveToDatabase:&error];
3155
3156 if(error) {
3157 ckkserror("ckkskey", self, "Couldn't update existing key: %@: %@", possibleLocalKey, error);
3158 }
3159 return;
3160 }
3161
3162 // Drop into the synckeys table as a 'remote' key, then ask for a rekey operation.
3163 remotekey.state = SecCKKSProcessedStateRemote;
3164 remotekey.currentkey = false;
3165
3166 [remotekey saveToDatabase:&error];
3167 if(error) {
3168 ckkserror("ckkskey", self, "Couldn't save key record to database: %@: %@", remotekey, error);
3169 ckksinfo("ckkskey", self, "CKRecord was %@", record);
3170 }
3171
3172 // We've saved a new key in the database; trigger a rekey operation.
3173 [self _onqueueKeyStateMachineRequestProcess];
3174 }
3175
3176 - (void)_onqueueCKRecordTLKShareChanged:(CKRecord*)record resync:(bool)resync {
3177 dispatch_assert_queue(self.queue);
3178
3179 NSError* error = nil;
3180 if(resync) {
3181 // TODO fill in
3182 }
3183
3184 // CKKSTLKShares get saved with no modification
3185 CKKSTLKShareRecord* share = [[CKKSTLKShareRecord alloc] initWithCKRecord:record];
3186 [share saveToDatabase:&error];
3187 if(error) {
3188 ckkserror("ckksshare", self, "Couldn't save new TLK share to database: %@ %@", share, error);
3189 }
3190
3191 [self _onqueueKeyStateMachineRequestProcess];
3192 }
3193
3194 - (void)_onqueueCKRecordCurrentKeyPointerChanged:(CKRecord*)record resync:(bool)resync {
3195 dispatch_assert_queue(self.queue);
3196
3197 // Pull out the old CKP, if it exists
3198 NSError* ckperror = nil;
3199 CKKSCurrentKeyPointer* oldckp = [CKKSCurrentKeyPointer tryFromDatabase:((CKKSKeyClass*) record.recordID.recordName) zoneID:self.zoneID error:&ckperror];
3200 if(ckperror) {
3201 ckkserror("ckkskey", self, "error loading ckp: %@", ckperror);
3202 }
3203
3204 if(resync) {
3205 if(!oldckp) {
3206 ckkserror("ckksresync", self, "BUG: No current key pointer matching resynced CloudKit record: %@", record);
3207 } else if(![oldckp matchesCKRecord:record]) {
3208 ckkserror("ckksresync", self, "BUG: Local current key pointer doesn't match resynced CloudKit record: %@ %@", oldckp, record);
3209 } else {
3210 ckksnotice("ckksresync", self, "Current key pointer has 'changed', but it matches our local copy: %@", record);
3211 }
3212 }
3213
3214 NSError* error = nil;
3215 CKKSCurrentKeyPointer* currentkey = [[CKKSCurrentKeyPointer alloc] initWithCKRecord: record];
3216
3217 [currentkey saveToDatabase: &error];
3218 if(error) {
3219 ckkserror("ckkskey", self, "Couldn't save current key pointer to database: %@: %@", currentkey, error);
3220 ckksinfo("ckkskey", self, "CKRecord was %@", record);
3221 }
3222
3223 if([oldckp matchesCKRecord:record]) {
3224 ckksnotice("ckkskey", self, "Current key pointer modification doesn't change anything interesting; skipping reprocess: %@", record);
3225 } else {
3226 // We've saved a new key in the database; trigger a rekey operation.
3227 [self _onqueueKeyStateMachineRequestProcess];
3228 }
3229 }
3230
3231 - (void)_onqueueCKRecordCurrentItemPointerChanged:(CKRecord*)record resync:(bool)resync {
3232 dispatch_assert_queue(self.queue);
3233
3234 if(resync) {
3235 NSError* ciperror = nil;
3236 CKKSCurrentItemPointer* localcip = [CKKSCurrentItemPointer tryFromDatabase:record.recordID.recordName state:SecCKKSProcessedStateLocal zoneID:self.zoneID error:&ciperror];
3237 CKKSCurrentItemPointer* remotecip = [CKKSCurrentItemPointer tryFromDatabase:record.recordID.recordName state:SecCKKSProcessedStateRemote zoneID:self.zoneID error:&ciperror];
3238 if(ciperror) {
3239 ckkserror("ckksresync", self, "error loading cip: %@", ciperror);
3240 }
3241 if(!(localcip || remotecip)) {
3242 ckkserror("ckksresync", self, "BUG: No current item pointer matching resynced CloudKit record: %@", record);
3243 } else if(! ([localcip matchesCKRecord:record] || [remotecip matchesCKRecord:record]) ) {
3244 ckkserror("ckksresync", self, "BUG: Local current item pointer doesn't match resynced CloudKit record(s): %@ %@ %@", localcip, remotecip, record);
3245 } else {
3246 ckksnotice("ckksresync", self, "Already know about this current item pointer, skipping update: %@", record);
3247 return;
3248 }
3249 }
3250
3251 NSError* error = nil;
3252 CKKSCurrentItemPointer* cip = [[CKKSCurrentItemPointer alloc] initWithCKRecord: record];
3253 cip.state = SecCKKSProcessedStateRemote;
3254
3255 [cip saveToDatabase: &error];
3256 if(error) {
3257 ckkserror("currentitem", self, "Couldn't save current item pointer to database: %@: %@ %@", cip, error, record);
3258 }
3259 }
3260
3261 - (void)_onqueueCKRecordManifestChanged:(CKRecord*)record resync:(bool)resync
3262 {
3263 NSError* error = nil;
3264 CKKSPendingManifest* manifest = [[CKKSPendingManifest alloc] initWithCKRecord:record];
3265 [manifest saveToDatabase:&error];
3266 if (error) {
3267 ckkserror("CKKS", self, "Failed to save fetched manifest record to database: %@: %@", manifest, error);
3268 ckksinfo("CKKS", self, "manifest CKRecord was %@", record);
3269 }
3270 }
3271
3272 - (void)_onqueueCKRecordManifestLeafChanged:(CKRecord*)record resync:(bool)resync
3273 {
3274 NSError* error = nil;
3275 CKKSManifestLeafRecord* manifestLeaf = [[CKKSManifestPendingLeafRecord alloc] initWithCKRecord:record];
3276 [manifestLeaf saveToDatabase:&error];
3277 if (error) {
3278 ckkserror("CKKS", self, "Failed to save fetched manifest leaf record to database: %@: %@", manifestLeaf, error);
3279 ckksinfo("CKKS", self, "manifest leaf CKRecord was %@", record);
3280 }
3281 }
3282
3283 - (void)_onqueueCKRecordDeviceStateChanged:(CKRecord*)record resync:(bool)resync {
3284 if(resync) {
3285 NSError* dserror = nil;
3286 CKKSDeviceStateEntry* cdse = [CKKSDeviceStateEntry tryFromDatabase:record.recordID.recordName zoneID:self.zoneID error:&dserror];
3287 if(dserror) {
3288 ckkserror("ckksresync", self, "error loading cdse: %@", dserror);
3289 }
3290 if(!cdse) {
3291 ckkserror("ckksresync", self, "BUG: No current device state entry matching resynced CloudKit record: %@", record);
3292 } else if(![cdse matchesCKRecord:record]) {
3293 ckkserror("ckksresync", self, "BUG: Local current device state entry doesn't match resynced CloudKit record(s): %@ %@", cdse, record);
3294 } else {
3295 ckksnotice("ckksresync", self, "Already know about this current item pointer, skipping update: %@", record);
3296 return;
3297 }
3298 }
3299
3300 NSError* error = nil;
3301 CKKSDeviceStateEntry* cdse = [[CKKSDeviceStateEntry alloc] initWithCKRecord:record];
3302 [cdse saveToDatabase:&error];
3303 if (error) {
3304 ckkserror("ckksdevice", self, "Failed to save device record to database: %@: %@ %@", cdse, error, record);
3305 }
3306 }
3307
3308 - (bool)_onqueueResetAllInflightOQE:(NSError**)error {
3309 NSError* localError = nil;
3310
3311 while(true) {
3312 NSArray<CKKSOutgoingQueueEntry*> * inflightQueueEntries = [CKKSOutgoingQueueEntry fetch:SecCKKSOutgoingQueueItemsAtOnce
3313 state:SecCKKSStateInFlight
3314 zoneID:self.zoneID
3315 error:&localError];
3316
3317 if(localError != nil) {
3318 ckkserror("ckks", self, "Error finding inflight outgoing queue records: %@", localError);
3319 if(error) {
3320 *error = localError;
3321 }
3322 return false;
3323 }
3324
3325 if([inflightQueueEntries count] == 0u) {
3326 break;
3327 }
3328
3329 for(CKKSOutgoingQueueEntry* oqe in inflightQueueEntries) {
3330 [self _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateNew error:&localError];
3331
3332 if(localError) {
3333 ckkserror("ckks", self, "Error fixing up inflight OQE(%@): %@", oqe, localError);
3334 if(error) {
3335 *error = localError;
3336 }
3337 return false;
3338 }
3339 }
3340 }
3341
3342 return true;
3343 }
3344
3345 - (bool)_onqueueChangeOutgoingQueueEntry: (CKKSOutgoingQueueEntry*) oqe toState: (NSString*) state error: (NSError* __autoreleasing*) error {
3346 dispatch_assert_queue(self.queue);
3347
3348 NSError* localerror = nil;
3349
3350 if([state isEqualToString: SecCKKSStateDeleted]) {
3351 // Hurray, this must be a success
3352 SecBoolNSErrorCallback syncCallback = [[CKKSViewManager manager] claimCallbackForUUID:oqe.uuid];
3353 if(syncCallback) {
3354 syncCallback(true, nil);
3355 }
3356
3357 [oqe deleteFromDatabase: &localerror];
3358 if(localerror) {
3359 ckkserror("ckks", self, "Couldn't delete %@: %@", oqe, localerror);
3360 }
3361
3362 } else if([oqe.state isEqualToString:SecCKKSStateInFlight] && [state isEqualToString:SecCKKSStateNew]) {
3363 // An in-flight OQE is moving to new? See if it's been superceded
3364 CKKSOutgoingQueueEntry* newOQE = [CKKSOutgoingQueueEntry tryFromDatabase:oqe.uuid state:SecCKKSStateNew zoneID:self.zoneID error:&localerror];
3365 if(localerror) {
3366 ckkserror("ckksoutgoing", self, "Couldn't fetch an overwriting OQE, assuming one doesn't exist: %@", localerror);
3367 newOQE = nil;
3368 }
3369
3370 if(newOQE) {
3371 ckksnotice("ckksoutgoing", self, "New modification has come in behind inflight %@; dropping failed change", oqe);
3372 // recurse for that lovely code reuse
3373 [self _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateDeleted error:&localerror];
3374 if(localerror) {
3375 ckkserror("ckksoutgoing", self, "Couldn't delete in-flight OQE: %@", localerror);
3376 if(error) {
3377 *error = localerror;
3378 }
3379 }
3380 } else {
3381 oqe.state = state;
3382 [oqe saveToDatabase: &localerror];
3383 if(localerror) {
3384 ckkserror("ckks", self, "Couldn't save %@ as %@: %@", oqe, state, localerror);
3385 }
3386 }
3387
3388 } else {
3389 oqe.state = state;
3390 [oqe saveToDatabase: &localerror];
3391 if(localerror) {
3392 ckkserror("ckks", self, "Couldn't save %@ as %@: %@", oqe, state, localerror);
3393 }
3394 }
3395
3396 if(error && localerror) {
3397 *error = localerror;
3398 }
3399 return localerror == nil;
3400 }
3401
3402 - (bool)_onqueueErrorOutgoingQueueEntry: (CKKSOutgoingQueueEntry*) oqe itemError: (NSError*) itemError error: (NSError* __autoreleasing*) error {
3403 dispatch_assert_queue(self.queue);
3404
3405 SecBoolNSErrorCallback callback = [[CKKSViewManager manager] claimCallbackForUUID:oqe.uuid];
3406 if(callback) {
3407 callback(false, itemError);
3408 }
3409 NSError* localerror = nil;
3410
3411 // Now, delete the OQE: it's never coming back
3412 [oqe deleteFromDatabase:&localerror];
3413 if(localerror) {
3414 ckkserror("ckks", self, "Couldn't delete %@ (due to error %@): %@", oqe, itemError, localerror);
3415 }
3416
3417 if(error && localerror) {
3418 *error = localerror;
3419 }
3420 return localerror == nil;
3421 }
3422
3423 - (bool)_onqueueUpdateLatestManifestWithError:(NSError**)error
3424 {
3425 dispatch_assert_queue(self.queue);
3426 CKKSManifest* manifest = [CKKSManifest latestTrustedManifestForZone:self.zoneName error:error];
3427 if (manifest) {
3428 self.latestManifest = manifest;
3429 return true;
3430 }
3431 else {
3432 return false;
3433 }
3434 }
3435
3436 - (bool)_onqueueWithAccountKeysCheckTLK:(CKKSKey*)proposedTLK error:(NSError* __autoreleasing *)error {
3437 dispatch_assert_queue(self.queue);
3438 // First, if we have a local identity, check for any TLK shares
3439 NSError* localerror = nil;
3440
3441 if(![proposedTLK wrapsSelf]) {
3442 localerror = [NSError errorWithDomain:CKKSErrorDomain code:CKKSKeyNotSelfWrapped description:[NSString stringWithFormat:@"Potential TLK %@ doesn't wrap itself: %@", proposedTLK, proposedTLK.parentKeyUUID] underlying:NULL];
3443 ckkserror("ckksshare", self, "%@", localerror);
3444 if (error) {
3445 *error = localerror;
3446 }
3447 } else {
3448 bool tlkShares = [self _onqueueWithAccountKeysCheckTLKFromShares:proposedTLK error:&localerror];
3449 // We only want to error out if a positive error occurred. "No shares" is okay.
3450 if(!tlkShares || localerror) {
3451 bool noTrustedTLKShares = [localerror.domain isEqualToString:CKKSErrorDomain] && localerror.code == CKKSNoTrustedTLKShares;
3452 bool noSelfPeer = [localerror.domain isEqualToString:CKKSErrorDomain] && localerror.code == CKKSNoEncryptionKey;
3453 bool noTrust = [localerror.domain isEqualToString:CKKSErrorDomain] && localerror.code == CKKSLackingTrust;
3454
3455 // If this error was something worse than 'couldn't unwrap for reasons including there not being data', report it
3456 if(!(noTrustedTLKShares || noSelfPeer || noTrust)) {
3457 if(error) {
3458 *error = localerror;
3459 }
3460 ckkserror("ckksshare", self, "Errored unwrapping TLK with TLKShares: %@", localerror);
3461 return false;
3462 } else {
3463 ckkserror("ckksshare", self, "Non-fatal error unwrapping TLK with TLKShares: %@", localerror);
3464 }
3465 }
3466 }
3467
3468 if([proposedTLK loadKeyMaterialFromKeychain:error]) {
3469 // Hurray!
3470 return true;
3471 } else {
3472 return false;
3473 }
3474 }
3475
3476 // This version only examines if this TLK is recoverable from TLK shares
3477 - (bool)_onqueueWithAccountKeysCheckTLKFromShares:(CKKSKey*)proposedTLK error:(NSError* __autoreleasing *)error {
3478 // But being recoverable from any trust set is okay
3479 NSError* localerror = nil;
3480
3481 if(self.currentTrustStates.count == 0u) {
3482 if(error) {
3483 *error = [NSError errorWithDomain:CKKSErrorDomain
3484 code:CKKSLackingTrust
3485 description:@"No current trust states; can't check TLK"];
3486 }
3487 return false;
3488 }
3489
3490 for(CKKSPeerProviderState* trustState in self.currentTrustStates) {
3491 ckkserror("ckksshare", self, "Checking TLK from trust state %@", trustState);
3492 bool recovered = [self _onqueueWithAccountKeysWithPeers:trustState
3493 checkTLK:proposedTLK
3494 error:&localerror];
3495
3496 if(recovered) {
3497 ckkserror("ckksshare", self, "Recovered the TLK");
3498 return true;
3499 }
3500
3501 ckkserror("ckksshare", self, "Unable to recover TLK from trust set: %@", localerror);
3502 }
3503
3504 // Only report the last error
3505 if(error && localerror) {
3506 *error = localerror;
3507 }
3508 return false;
3509 }
3510
3511 - (bool)_onqueueWithAccountKeysWithPeers:(CKKSPeerProviderState*)trustState
3512 checkTLK:(CKKSKey*)proposedTLK
3513 error:(NSError* __autoreleasing *)error
3514 {
3515 NSError* localerror = NULL;
3516 if(!trustState.currentSelfPeers.currentSelf || trustState.currentSelfPeersError) {
3517 ckkserror("ckksshare", self, "Don't have self peers for %@: %@", trustState.peerProviderID, trustState.currentSelfPeersError);
3518 if(error) {
3519 if([self.lockStateTracker isLockedError:trustState.currentSelfPeersError]) {
3520 // Locked error should propagate
3521 *error = trustState.currentSelfPeersError;
3522 } else {
3523 *error = [NSError errorWithDomain:CKKSErrorDomain
3524 code:CKKSNoEncryptionKey
3525 description:@"No current self peer"
3526 underlying:trustState.currentSelfPeersError];
3527 }
3528 }
3529 return false;
3530 }
3531
3532 if(!trustState.currentTrustedPeers || trustState.currentTrustedPeersError) {
3533 ckkserror("ckksshare", self, "Don't have trusted peers: %@", trustState.currentTrustedPeersError);
3534 if(error) {
3535 *error = [NSError errorWithDomain:CKKSErrorDomain
3536 code:CKKSNoPeersAvailable
3537 description:@"No trusted peers"
3538 underlying:trustState.currentTrustedPeersError];
3539 }
3540 return false;
3541 }
3542
3543 NSError* lastShareError = nil;
3544
3545 for(id<CKKSSelfPeer> selfPeer in trustState.currentSelfPeers.allSelves) {
3546 NSArray<CKKSTLKShareRecord*>* possibleShares = [CKKSTLKShareRecord allFor:selfPeer.peerID
3547 keyUUID:proposedTLK.uuid
3548 zoneID:self.zoneID
3549 error:&localerror];
3550 if(localerror) {
3551 ckkserror("ckksshare", self, "Error fetching CKKSTLKShares for %@: %@", selfPeer, localerror);
3552 }
3553
3554 if(possibleShares.count == 0) {
3555 ckksnotice("ckksshare", self, "No CKKSTLKShares to %@ for %@", selfPeer, proposedTLK);
3556 continue;
3557 }
3558
3559 for(CKKSTLKShareRecord* possibleShare in possibleShares) {
3560 NSError* possibleShareError = nil;
3561 ckksnotice("ckksshare", self, "Checking possible TLK share %@ as %@", possibleShare, selfPeer);
3562
3563 CKKSKey* possibleKey = [possibleShare recoverTLK:selfPeer
3564 trustedPeers:trustState.currentTrustedPeers
3565 error:&possibleShareError];
3566
3567 if(possibleShareError) {
3568 ckkserror("ckksshare", self, "Unable to unwrap TLKShare(%@) as %@: %@",
3569 possibleShare, selfPeer, possibleShareError);
3570 ckkserror("ckksshare", self, "Current trust set: %@", trustState.currentTrustedPeers);
3571 lastShareError = possibleShareError;
3572 continue;
3573 }
3574
3575 bool result = [proposedTLK trySelfWrappedKeyCandidate:possibleKey.aessivkey error:&possibleShareError];
3576 if(possibleShareError) {
3577 ckkserror("ckksshare", self, "Unwrapped TLKShare(%@) does not unwrap proposed TLK(%@) as %@: %@",
3578 possibleShare, proposedTLK, trustState.currentSelfPeers.currentSelf, possibleShareError);
3579 lastShareError = possibleShareError;
3580 continue;
3581 }
3582
3583 if(result) {
3584 ckksnotice("ckksshare", self, "TLKShare(%@) unlocked TLK(%@) as %@",
3585 possibleShare, proposedTLK, selfPeer);
3586
3587 // The proposed TLK is trusted key material. Persist it as a "trusted" key.
3588 [proposedTLK saveKeyMaterialToKeychain:true error:&possibleShareError];
3589 if(possibleShareError) {
3590 ckkserror("ckksshare", self, "Couldn't store the new TLK(%@) to the keychain: %@", proposedTLK, possibleShareError);
3591 if(error) {
3592 *error = possibleShareError;
3593 }
3594 return false;
3595 }
3596
3597 return true;
3598 }
3599 }
3600 }
3601
3602 if(error) {
3603 *error = [NSError errorWithDomain:CKKSErrorDomain
3604 code:CKKSNoTrustedTLKShares
3605 description:[NSString stringWithFormat:@"No trusted TLKShares for %@", proposedTLK]
3606 underlying:lastShareError];
3607 }
3608 return false;
3609 }
3610
3611 - (bool)dispatchSyncWithConnection:(SecDbConnectionRef _Nonnull)dbconn block:(bool (^)(void))block {
3612 CFErrorRef cferror = NULL;
3613
3614 // Take the DB transaction, then get on the local queue.
3615 // In the case of exclusive DB transactions, we don't really _need_ the local queue, but, it's here for future use.
3616 bool ret = kc_transaction_type(dbconn, kSecDbExclusiveRemoteCKKSTransactionType, &cferror, ^bool{
3617 __block bool ok = false;
3618
3619 dispatch_sync(self.queue, ^{
3620 ok = block();
3621 });
3622
3623 return ok;
3624 });
3625
3626 if(cferror) {
3627 ckkserror("ckks", self, "error doing database transaction, major problems ahead: %@", cferror);
3628 }
3629 return ret;
3630 }
3631
3632 - (void)dispatchSync: (bool (^)(void)) block {
3633 // important enough to block this thread. Must get a connection first, though!
3634
3635 // Please don't jetsam us...
3636 os_transaction_t transaction = os_transaction_create([[NSString stringWithFormat:@"com.apple.securityd.ckks.%@", self.zoneName] UTF8String]);
3637
3638 CFErrorRef cferror = NULL;
3639 kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
3640 return [self dispatchSyncWithConnection:dbt block:block];
3641 });
3642 if(cferror) {
3643 ckkserror("ckks", self, "error getting database connection, major problems ahead: %@", cferror);
3644 }
3645
3646 (void)transaction;
3647 }
3648
3649 - (void)dispatchSyncWithAccountKeys:(bool (^)(void))block
3650 {
3651 [self dispatchSyncWithPeerProviders:self.currentPeerProviders override:false block:block];
3652 }
3653
3654 - (void)dispatchSyncWithPeerProviders:(NSArray<id<CKKSPeerProvider>>*)peerProviders
3655 override:(bool)overridePeerProviders
3656 block:(bool (^)(void))block
3657 {
3658 NSArray<id<CKKSPeerProvider>>* actualPeerProviders = overridePeerProviders ? peerProviders : self.currentPeerProviders;
3659 NSMutableArray<CKKSPeerProviderState*>* trustStates = [NSMutableArray array];
3660
3661 for(id<CKKSPeerProvider> provider in actualPeerProviders) {
3662 ckksnotice("ckks", self, "Fetching account keys for provider %@", provider);
3663 [trustStates addObject:provider.currentState];
3664 }
3665
3666 [self dispatchSync:^bool{
3667 if(overridePeerProviders) {
3668 self.currentPeerProviders = peerProviders;
3669 }
3670 self.currentTrustStates = trustStates;
3671
3672 bool result = block();
3673
3674 // Forget the peers; they might have class A key material
3675 NSMutableArray<CKKSPeerProviderState*>* noTrustStates = [NSMutableArray array];
3676 for(id<CKKSPeerProvider> provider in peerProviders) {
3677 (void)provider;
3678 [noTrustStates addObject:[CKKSPeerProviderState noPeersState:provider]];
3679 }
3680 self.currentTrustStates = noTrustStates;
3681
3682 return result;
3683 }];
3684 }
3685
3686 #pragma mark - CKKSZoneUpdateReceiver
3687
3688 - (void)notifyZoneChange: (CKRecordZoneNotification*) notification {
3689 ckksnotice("ckks", self, "received a zone change notification for %@ %@", self, notification);
3690
3691 [self fetchAndProcessCKChangesDueToAPNS:notification];
3692 }
3693
3694 - (void)superHandleCKLogin {
3695 [super handleCKLogin];
3696 }
3697
3698 - (void)handleCKLogin {
3699 ckksnotice("ckks", self, "received a notification of CK login");
3700 if(!SecCKKSIsEnabled()) {
3701 ckksnotice("ckks", self, "Skipping CloudKit initialization due to disabled CKKS");
3702 return;
3703 }
3704
3705 WEAKIFY(self);
3706 CKKSResultOperation* login = [CKKSResultOperation named:@"ckks-login" withBlock:^{
3707 STRONGIFY(self);
3708
3709 [self dispatchSyncWithAccountKeys:^bool{
3710 [self superHandleCKLogin];
3711
3712 // Reset key hierarchy state machine to initializing
3713 [self _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateInitializing withError:nil];
3714 return true;
3715 }];
3716
3717 // Change our condition variables to reflect that we think we're logged in
3718 self.loggedOut = [[CKKSCondition alloc] initToChain:self.loggedOut];
3719 [self.loggedIn fulfill];
3720 [self.accountStateKnown fulfill];
3721 }];
3722
3723 [self scheduleAccountStatusOperation:login];
3724 }
3725
3726 - (void)superHandleCKLogout {
3727 [super handleCKLogout];
3728 }
3729
3730 - (void)handleCKLogout {
3731 WEAKIFY(self);
3732 CKKSResultOperation* logout = [CKKSResultOperation named:@"ckks-logout" withBlock: ^{
3733 STRONGIFY(self);
3734 if(!self) {
3735 return;
3736 }
3737 [self dispatchSync:^bool {
3738 ckksnotice("ckks", self, "received a notification of CK logout");
3739 [self superHandleCKLogout];
3740
3741 NSError* error = nil;
3742 [self _onqueueResetLocalData: &error];
3743 if(error) {
3744 ckkserror("ckks", self, "error while resetting local data: %@", error);
3745 }
3746
3747 [self _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateLoggedOut withError:nil];
3748
3749 self.loggedIn = [[CKKSCondition alloc] initToChain: self.loggedIn];
3750 [self.loggedOut fulfill];
3751 [self.accountStateKnown fulfill];
3752
3753 return true;
3754 }];
3755 }];
3756
3757 [self scheduleAccountStatusOperation: logout];
3758 }
3759
3760 #pragma mark - Trust operations
3761
3762 - (void)beginTrustedOperation:(NSArray<id<CKKSPeerProvider>>*)peerProviders
3763 suggestTLKUpload:(CKKSNearFutureScheduler*)suggestTLKUpload
3764 {
3765 for(id<CKKSPeerProvider> peerProvider in peerProviders) {
3766 [peerProvider registerForPeerChangeUpdates:self];
3767 }
3768
3769 [self.launch addEvent:@"beginTrusted"];
3770
3771 [self dispatchSyncWithPeerProviders:peerProviders override:true block:^bool {
3772 ckksnotice("ckkstrust", self, "Beginning trusted operation");
3773 CKKSAccountStatus oldTrustStatus = self.trustStatus;
3774
3775 self.suggestTLKUpload = suggestTLKUpload;
3776
3777 self.trustStatus = CKKSAccountStatusAvailable;
3778 if(self.trustDependency) {
3779 [self scheduleOperation: self.trustDependency];
3780 self.trustDependency = nil;
3781 }
3782 [self _onqueueAdvanceKeyStateMachineToState:nil withError:nil];
3783
3784 if(oldTrustStatus == CKKSAccountStatusNoAccount) {
3785 ckksnotice("ckkstrust", self, "Moving from an untrusted status; we need to process incoming queue and scan for any new items");
3786
3787 // Next, try to process them (replacing local entries)
3788 CKKSIncomingQueueOperation* initialProcess = [self processIncomingQueue:true after:nil];
3789 initialProcess.name = @"initial-process-incoming-queue";
3790
3791 // If all that succeeds, iterate through all keychain items and find the ones which need to be uploaded
3792 self.initialScanOperation = [self scanLocalItems:@"newly-trusted-scan"
3793 ckoperationGroup:nil
3794 after:initialProcess];
3795 }
3796
3797 return true;
3798 }];
3799 }
3800
3801 - (void)endTrustedOperation
3802 {
3803 [self.launch addEvent:@"endTrusted"];
3804
3805 [self dispatchSyncWithPeerProviders:nil override:true block:^bool {
3806 ckksnotice("ckkstrust", self, "Ending trusted operation");
3807
3808 self.suggestTLKUpload = nil;
3809
3810 self.trustStatus = CKKSAccountStatusNoAccount;
3811 if(!self.trustDependency) {
3812 self.trustDependency = [CKKSResultOperation named:@"wait-for-trust" withBlock:^{}];
3813 }
3814 [self _onqueueAdvanceKeyStateMachineToState:nil withError:nil];
3815 return true;
3816 }];
3817 }
3818
3819 #pragma mark - CKKSChangeFetcherClient
3820
3821 - (BOOL)zoneIsReadyForFetching
3822 {
3823 __block BOOL ready = NO;
3824
3825 [self dispatchSync: ^bool {
3826 ready = (bool)[self _onQueueZoneIsReadyForFetching];
3827 return ready;
3828 }];
3829
3830 return ready;
3831 }
3832
3833 - (BOOL)_onQueueZoneIsReadyForFetching
3834 {
3835 if(self.accountStatus != CKKSAccountStatusAvailable) {
3836 ckksnotice("ckksfetch", self, "Not participating in fetch: not logged in");
3837 return NO;
3838 }
3839
3840 if(!self.zoneCreated) {
3841 ckksnotice("ckksfetch", self, "Not participating in fetch: zone not created yet");
3842 return NO;
3843 }
3844 return YES;
3845 }
3846
3847 - (CKKSCloudKitFetchRequest*)participateInFetch
3848 {
3849 __block CKKSCloudKitFetchRequest* request = [[CKKSCloudKitFetchRequest alloc] init];
3850
3851 [self dispatchSync: ^bool {
3852 if (![self _onQueueZoneIsReadyForFetching]) {
3853 ckksnotice("ckksfetch", self, "skipping fetch since zones are not ready");
3854 return false;
3855 }
3856
3857 request.participateInFetch = true;
3858 [self.launch addEvent:@"fetch"];
3859
3860 if([self.keyHierarchyState isEqualToString:SecCKKSZoneKeyStateNeedFullRefetch]) {
3861 // We want to return a nil change tag (to force a resync)
3862 ckksnotice("ckksfetch", self, "Beginning refetch");
3863 request.changeToken = nil;
3864 request.resync = true;
3865 } else {
3866 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.zoneName];
3867 if(!ckse) {
3868 ckkserror("ckksfetch", self, "couldn't fetch zone change token for %@", self.zoneName);
3869 return false;
3870 }
3871 request.changeToken = ckse.changeToken;
3872 }
3873 return true;
3874 }];
3875
3876 if (request.changeToken == nil) {
3877 self.launch.firstLaunch = true;
3878 }
3879
3880 return request;
3881 }
3882
3883 - (void)changesFetched:(NSArray<CKRecord*>*)changedRecords
3884 deletedRecordIDs:(NSArray<CKKSCloudKitDeletion*>*)deletedRecords
3885 newChangeToken:(CKServerChangeToken*)newChangeToken
3886 moreComing:(BOOL)moreComing
3887 resync:(BOOL)resync
3888 {
3889 [self.launch addEvent:@"changes-fetched"];
3890
3891 if(changedRecords.count == 0 && deletedRecords.count == 0 && !moreComing && !resync) {
3892 // Early-exit, so we don't pick up the account keys or kick off an IncomingQueue operation for no changes
3893 [self dispatchSync:^bool {
3894 ckkserror("ckksfetch", self, "No record changes in this fetch");
3895
3896 NSError* error = nil;
3897 CKKSZoneStateEntry* state = [CKKSZoneStateEntry state:self.zoneName];
3898 state.lastFetchTime = [NSDate date]; // The last fetch happened right now!
3899 state.changeToken = newChangeToken;
3900 state.moreRecordsInCloudKit = moreComing;
3901 [state saveToDatabase:&error];
3902 if(error) {
3903 ckkserror("ckksfetch", self, "Couldn't save new server change token: %@", error);
3904 }
3905 return true;
3906 }];
3907 return;
3908 }
3909
3910 [self dispatchSyncWithAccountKeys:^bool{
3911 for (CKRecord* record in changedRecords) {
3912 [self _onqueueCKRecordChanged:record resync:resync];
3913 }
3914
3915 for (CKKSCloudKitDeletion* deletion in deletedRecords) {
3916 [self _onqueueCKRecordDeleted:deletion.recordID recordType:deletion.recordType resync:resync];
3917 }
3918
3919 NSError* error = nil;
3920 if(resync) {
3921 // If we're performing a resync, we need to keep track of everything that's actively in
3922 // CloudKit during the fetch, (so that we can find anything that's on-disk and not in CloudKit).
3923 // Please note that if, during a resync, the fetch errors, we won't be notified. If a record is in
3924 // the first refetch but not the second, it'll be added to our set, and the second resync will not
3925 // delete the record (which is a consistency violation, but only with actively changing records).
3926 // A third resync should correctly delete that record.
3927
3928 if(self.resyncRecordsSeen == nil) {
3929 self.resyncRecordsSeen = [NSMutableSet set];
3930 }
3931 for(CKRecord* r in changedRecords) {
3932 [self.resyncRecordsSeen addObject:r.recordID.recordName];
3933 }
3934
3935 // Is there More Coming? If not, self.resyncRecordsSeen contains everything in CloudKit. Inspect for anything extra!
3936 if(moreComing) {
3937 ckksnotice("ckksresync", self, "In a resync, but there's More Coming. Waiting to scan for extra items.");
3938
3939 } else {
3940 // Scan through all CKMirrorEntries and determine if any exist that CloudKit didn't tell us about
3941 ckksnotice("ckksresync", self, "Comparing local UUIDs against the CloudKit list");
3942 NSMutableArray<NSString*>* uuids = [[CKKSMirrorEntry allUUIDs:self.zoneID error:&error] mutableCopy];
3943
3944 for(NSString* uuid in uuids) {
3945 if([self.resyncRecordsSeen containsObject:uuid]) {
3946 ckksnotice("ckksresync", self, "UUID %@ is still in CloudKit; carry on.", uuid);
3947 } else {
3948 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase:uuid zoneID:self.zoneID error:&error];
3949 if(error != nil) {
3950 ckkserror("ckksresync", self, "Couldn't read an item from the database, but it used to be there: %@ %@", uuid, error);
3951 continue;
3952 }
3953 if(!ckme) {
3954 ckkserror("ckksresync", self, "Couldn't read ckme(%@) from database; continuing", uuid);
3955 continue;
3956 }
3957
3958 ckkserror("ckksresync", self, "BUG: Local item %@ not found in CloudKit, deleting", uuid);
3959 [self _onqueueCKRecordDeleted:ckme.item.storedCKRecord.recordID recordType:ckme.item.storedCKRecord.recordType resync:resync];
3960 }
3961 }
3962
3963 // Now that we've inspected resyncRecordsSeen, reset it for the next time through
3964 self.resyncRecordsSeen = nil;
3965 }
3966 }
3967
3968 CKKSZoneStateEntry* state = [CKKSZoneStateEntry state:self.zoneName];
3969 state.lastFetchTime = [NSDate date]; // The last fetch happened right now!
3970 state.changeToken = newChangeToken;
3971 state.moreRecordsInCloudKit = moreComing;
3972 [state saveToDatabase:&error];
3973 if(error) {
3974 ckkserror("ckksfetch", self, "Couldn't save new server change token: %@", error);
3975 }
3976
3977 if(!moreComing) {
3978 // Might as well kick off a IQO!
3979 [self processIncomingQueue:false];
3980 ckksnotice("ckksfetch", self, "Beginning incoming processing for %@", self.zoneID);
3981 }
3982
3983 ckksnotice("ckksfetch", self, "Finished processing changes for %@", self.zoneID);
3984
3985 return true;
3986 }];
3987 }
3988
3989 - (bool)ckErrorOrPartialError:(NSError *)error isError:(CKErrorCode)errorCode
3990 {
3991 if((error.code == errorCode) && [error.domain isEqualToString:CKErrorDomain]) {
3992 return true;
3993 } else if((error.code == CKErrorPartialFailure) && [error.domain isEqualToString:CKErrorDomain]) {
3994 NSDictionary* partialErrors = error.userInfo[CKPartialErrorsByItemIDKey];
3995
3996 NSError* partialError = partialErrors[self.zoneID];
3997 if ((partialError.code == errorCode) && [partialError.domain isEqualToString:CKErrorDomain]) {
3998 return true;
3999 }
4000 }
4001 return false;
4002 }
4003
4004 - (bool)shouldRetryAfterFetchError:(NSError*)error {
4005
4006 bool isChangeTokenExpiredError = [self ckErrorOrPartialError:error isError:CKErrorChangeTokenExpired];
4007 if(isChangeTokenExpiredError) {
4008 ckkserror("ckks", self, "Received notice that our change token is out of date (for %@). Resetting local data...", self.zoneID);
4009
4010 // This is a bit scary: we might confuse some poor key hierarchy state machine operation. But, if a key state machine
4011 // operation is waiting for a successful fetch, we need to do this reset
4012 [self dispatchSyncWithAccountKeys:^bool{
4013 NSError* error = nil;
4014 [self _onqueueResetLocalData:&error];
4015
4016 // We need to rescan the local keychain once we return to a good state
4017 self.droppedItems = true;
4018
4019 if(error) {
4020 ckksnotice("ckksreset", self, "CloudKit-inspired local reset of %@ ended with error: %@", self.zoneID, error);
4021 } else {
4022 ckksnotice("ckksreset", self, "CloudKit-inspired local reset of %@ ended successfully", self.zoneID);
4023 }
4024
4025 // If we're in the middle of a fetch for the key state, then the retried fetch (which should succeed) will be sufficient to progress
4026 // Otherwise, we need to poke the key hierarchy state machine: all of its data is gone
4027 if(![self.keyHierarchyState isEqualToString:SecCKKSZoneKeyStateFetch]) {
4028 [self _onqueueKeyStateMachineRequestFetch];
4029 }
4030
4031 return true;
4032 }];
4033
4034 return true;
4035 }
4036
4037 bool isDeletedZoneError = [self ckErrorOrPartialError:error isError:CKErrorZoneNotFound];
4038 if(isDeletedZoneError) {
4039 ckkserror("ckks", self, "Received notice that our zone(%@) does not exist. Resetting local data.", self.zoneID);
4040
4041 /*
4042 * If someone delete our zone, lets just start over from the begining
4043 */
4044 [self dispatchSync: ^bool{
4045 NSError* resetError = nil;
4046
4047 [self _onqueueResetLocalData: &resetError];
4048 if(resetError) {
4049 ckksnotice("ckksreset", self, "CloudKit-inspired local reset of %@ ended with error: %@", self.zoneID, resetError);
4050 } else {
4051 ckksnotice("ckksreset", self, "CloudKit-inspired local reset of %@ ended successfully", self.zoneID);
4052 }
4053
4054 [self _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateInitializing withError:nil];
4055 return true;
4056 }];
4057
4058 return false;
4059 }
4060
4061 if([error.domain isEqualToString:CKErrorDomain] && (error.code == CKErrorBadContainer)) {
4062 ckkserror("ckks", self, "Received notice that our container does not exist. Nothing to do.");
4063 return false;
4064 }
4065
4066 return true;
4067 }
4068
4069 #pragma mark CKKSPeerUpdateListener
4070
4071 - (void)selfPeerChanged:(id<CKKSPeerProvider>)provider
4072 {
4073 // Currently, we have no idea what to do with this. Kick off a key reprocess?
4074 ckkserror("ckks", self, "Received update that our self identity has changed");
4075 [self keyStateMachineRequestProcess];
4076 }
4077
4078 - (void)trustedPeerSetChanged:(id<CKKSPeerProvider>)provider
4079 {
4080 // We might need to share the TLK to some new people, or we might now trust the TLKs we have.
4081 // The key state machine should handle that, so poke it.
4082 ckkserror("ckks", self, "Received update that the trust set has changed");
4083
4084 self.trustedPeersSetChanged = true;
4085 [self.pokeKeyStateMachineScheduler trigger];
4086 }
4087
4088 #pragma mark - Test Support
4089
4090 - (bool) outgoingQueueEmpty: (NSError * __autoreleasing *) error {
4091 __block bool ret = false;
4092 [self dispatchSync: ^bool{
4093 NSArray* queueEntries = [CKKSOutgoingQueueEntry all: error];
4094 ret = queueEntries && ([queueEntries count] == 0);
4095 return true;
4096 }];
4097
4098 return ret;
4099 }
4100
4101 - (CKKSResultOperation*)waitForFetchAndIncomingQueueProcessing {
4102 CKKSResultOperation* op = [self fetchAndProcessCKChanges:CKKSFetchBecauseTesting];
4103 [op waitUntilFinished];
4104 return op;
4105 }
4106
4107 - (void)waitForKeyHierarchyReadiness {
4108 if(self.keyStateReadyDependency) {
4109 [self.keyStateReadyDependency waitUntilFinished];
4110 }
4111 }
4112
4113 - (void)cancelPendingOperations {
4114 @synchronized(self.outgoingQueueOperations) {
4115 for(NSOperation* op in self.outgoingQueueOperations) {
4116 [op cancel];
4117 }
4118 [self.outgoingQueueOperations removeAllObjects];
4119 }
4120
4121 @synchronized(self.incomingQueueOperations) {
4122 for(NSOperation* op in self.incomingQueueOperations) {
4123 [op cancel];
4124 }
4125 [self.incomingQueueOperations removeAllObjects];
4126 }
4127
4128 @synchronized(self.scanLocalItemsOperations) {
4129 for(NSOperation* op in self.scanLocalItemsOperations) {
4130 [op cancel];
4131 }
4132 [self.scanLocalItemsOperations removeAllObjects];
4133 }
4134
4135 [super cancelAllOperations];
4136 }
4137
4138 - (void)cancelAllOperations {
4139 [self.zoneSetupOperation cancel];
4140 [self.keyStateMachineOperation cancel];
4141 [self.keyStateReadyDependency cancel];
4142 [self.keyStateNonTransientDependency cancel];
4143 [self.zoneChangeFetcher cancel];
4144 [self.notifyViewChangedScheduler cancel];
4145 [self.pokeKeyStateMachineScheduler cancel];
4146
4147 [self cancelPendingOperations];
4148
4149 [self dispatchSync:^bool{
4150 [self _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateCancelled withError: nil];
4151 return true;
4152 }];
4153 }
4154
4155 - (void)halt {
4156 [super halt];
4157
4158 // Don't send any more notifications, either
4159 _notifierClass = nil;
4160 }
4161
4162 - (NSDictionary*)status {
4163 #define stringify(obj) CKKSNilToNSNull([obj description])
4164 #define boolstr(obj) (!!(obj) ? @"yes" : @"no")
4165 __block NSMutableDictionary* ret = nil;
4166 __block NSError* error = nil;
4167 CKKSManifest* manifest = nil;
4168
4169 ret = [[self fastStatus] mutableCopy];
4170
4171 manifest = [CKKSManifest latestTrustedManifestForZone:self.zoneName error:&error];
4172 [self dispatchSync: ^bool {
4173
4174 CKKSCurrentKeySet* keyset = [CKKSCurrentKeySet loadForZone:self.zoneID];
4175 if(keyset.error) {
4176 error = keyset.error;
4177 }
4178
4179 NSString* manifestGeneration = manifest ? [NSString stringWithFormat:@"%lu", (unsigned long)manifest.generationCount] : nil;
4180
4181 if(error) {
4182 ckkserror("ckks", self, "error during status: %@", error);
4183 }
4184 // We actually don't care about this error, especially if it's "no current key pointers"...
4185 error = nil;
4186
4187 // Map deviceStates to strings to avoid NSXPC issues. Obj-c, why is this so hard?
4188 NSArray* deviceStates = [CKKSDeviceStateEntry allInZone:self.zoneID error:&error];
4189 NSMutableArray<NSString*>* mutDeviceStates = [[NSMutableArray alloc] init];
4190 [deviceStates enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
4191 [mutDeviceStates addObject: [obj description]];
4192 }];
4193
4194 NSArray* tlkShares = [CKKSTLKShareRecord allForUUID:keyset.currentTLKPointer.currentKeyUUID zoneID:self.zoneID error:&error];
4195 NSMutableArray<NSString*>* mutTLKShares = [[NSMutableArray alloc] init];
4196 [tlkShares enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
4197 [mutTLKShares addObject: [obj description]];
4198 }];
4199
4200 [ret addEntriesFromDictionary:@{
4201 @"statusError": stringify(error),
4202 @"oqe": CKKSNilToNSNull([CKKSOutgoingQueueEntry countsByStateInZone:self.zoneID error:&error]),
4203 @"iqe": CKKSNilToNSNull([CKKSIncomingQueueEntry countsByStateInZone:self.zoneID error:&error]),
4204 @"ckmirror": CKKSNilToNSNull([CKKSMirrorEntry countsByParentKey:self.zoneID error:&error]),
4205 @"devicestates": CKKSNilToNSNull(mutDeviceStates),
4206 @"tlkshares": CKKSNilToNSNull(mutTLKShares),
4207 @"keys": CKKSNilToNSNull([CKKSKey countsByClass:self.zoneID error:&error]),
4208 @"currentTLK": CKKSNilToNSNull(keyset.tlk.uuid),
4209 @"currentClassA": CKKSNilToNSNull(keyset.classA.uuid),
4210 @"currentClassC": CKKSNilToNSNull(keyset.classC.uuid),
4211 @"currentTLKPtr": CKKSNilToNSNull(keyset.currentTLKPointer.currentKeyUUID),
4212 @"currentClassAPtr": CKKSNilToNSNull(keyset.currentClassAPointer.currentKeyUUID),
4213 @"currentClassCPtr": CKKSNilToNSNull(keyset.currentClassCPointer.currentKeyUUID),
4214 @"currentManifestGen": CKKSNilToNSNull(manifestGeneration),
4215 }];
4216 return false;
4217 }];
4218 return ret;
4219 }
4220
4221 - (NSDictionary*)fastStatus {
4222
4223 __block NSDictionary* ret = nil;
4224
4225 [self dispatchSync: ^bool {
4226
4227 ret = @{
4228 @"view": CKKSNilToNSNull(self.zoneName),
4229 @"ckaccountstatus": self.accountStatus == CKAccountStatusCouldNotDetermine ? @"could not determine" :
4230 self.accountStatus == CKAccountStatusAvailable ? @"logged in" :
4231 self.accountStatus == CKAccountStatusRestricted ? @"restricted" :
4232 self.accountStatus == CKAccountStatusNoAccount ? @"logged out" : @"unknown",
4233 @"accounttracker": stringify(self.accountTracker),
4234 @"fetcher": stringify(self.zoneChangeFetcher),
4235 @"zoneCreated": boolstr(self.zoneCreated),
4236 @"zoneCreatedError": stringify(self.zoneCreatedError),
4237 @"zoneSubscribed": boolstr(self.zoneSubscribed),
4238 @"zoneSubscribedError": stringify(self.zoneSubscribedError),
4239 @"keystate": CKKSNilToNSNull(self.keyHierarchyState),
4240 @"keyStateError": stringify(self.keyHierarchyError),
4241 @"statusError": [NSNull null],
4242 @"launchSequence": CKKSNilToNSNull([self.launch eventsByTime]),
4243
4244 @"zoneSetupOperation": stringify(self.zoneSetupOperation),
4245 @"keyStateOperation": stringify(self.keyStateMachineOperation),
4246 @"lastIncomingQueueOperation": stringify(self.lastIncomingQueueOperation),
4247 @"lastNewTLKOperation": stringify(self.lastNewTLKOperation),
4248 @"lastOutgoingQueueOperation": stringify(self.lastOutgoingQueueOperation),
4249 @"lastProcessReceivedKeysOperation": stringify(self.lastProcessReceivedKeysOperation),
4250 @"lastReencryptOutgoingItemsOperation":stringify(self.lastReencryptOutgoingItemsOperation),
4251 @"lastScanLocalItemsOperation": stringify(self.lastScanLocalItemsOperation),
4252 };
4253 return false;
4254 }];
4255
4256 return ret;
4257 }
4258
4259 #endif /* OCTAGON */
4260 @end