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