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