2 * Copyright (c) 2016 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
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
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.
21 * @APPLE_LICENSE_HEADER_END@
24 #import "CKKSKeychainView.h"
29 #import "CloudKitDependencies.h"
30 #import <CloudKit/CloudKit.h>
31 #import <CloudKit/CloudKit_Private.h>
35 #import "CKKSAPSReceiver.h"
36 #import "CKKSIncomingQueueEntry.h"
37 #import "CKKSOutgoingQueueEntry.h"
38 #import "CKKSCurrentKeyPointer.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"
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 "keychain/ckks/CKKSDeviceStateEntry.h"
57 #import "keychain/ckks/CKKSNearFutureScheduler.h"
58 #import "keychain/ckks/CKKSCurrentItemPointer.h"
59 #import "keychain/ckks/CKKSUpdateCurrentItemPointerOperation.h"
60 #import "keychain/ckks/CKKSUpdateDeviceStateOperation.h"
61 #import "keychain/ckks/CKKSLockStateTracker.h"
62 #import "keychain/ckks/CKKSNotifier.h"
63 #import "keychain/ckks/CloudKitCategories.h"
65 #include <utilities/SecCFWrappers.h>
66 #include <utilities/SecDb.h>
67 #include <securityd/SecDbItem.h>
68 #include <securityd/SecItemDb.h>
69 #include <securityd/SecItemSchema.h>
70 #include <securityd/SecItemServer.h>
71 #include <utilities/debugging.h>
72 #include <Security/SecItemPriv.h>
73 #include <Security/SecureObjectSync/SOSAccountTransaction.h>
74 #include <utilities/SecADWrapper.h>
75 #include <utilities/SecPLWrappers.h>
78 @interface CKKSKeychainView()
79 @property bool setupSuccessful;
80 @property bool keyStateFetchRequested;
81 @property bool keyStateFullRefetchRequested;
82 @property bool keyStateProcessRequested;
83 @property (atomic) NSString *activeTLK;
85 @property (readonly) Class<CKKSNotifier> notifierClass;
87 @property CKKSNearFutureScheduler* initializeScheduler;
89 @property CKKSResultOperation* processIncomingQueueAfterNextUnlockOperation;
91 @property NSMutableDictionary<NSString*, SecBoolNSErrorCallback>* pendingSyncCallbacks;
95 @implementation CKKSKeychainView
98 - (instancetype)initWithContainer: (CKContainer*) container
99 zoneName: (NSString*) zoneName
100 accountTracker:(CKKSCKAccountStateTracker*) accountTracker
101 lockStateTracker:(CKKSLockStateTracker*) lockStateTracker
102 savedTLKNotifier:(CKKSNearFutureScheduler*) savedTLKNotifier
103 fetchRecordZoneChangesOperationClass: (Class<CKKSFetchRecordZoneChangesOperation>) fetchRecordZoneChangesOperationClass
104 modifySubscriptionsOperationClass: (Class<CKKSModifySubscriptionsOperation>) modifySubscriptionsOperationClass
105 modifyRecordZonesOperationClass: (Class<CKKSModifyRecordZonesOperation>) modifyRecordZonesOperationClass
106 apsConnectionClass: (Class<CKKSAPSConnection>) apsConnectionClass
107 notifierClass: (Class<CKKSNotifier>) notifierClass
110 if(self = [super initWithContainer:container
112 accountTracker:accountTracker
113 fetchRecordZoneChangesOperationClass:fetchRecordZoneChangesOperationClass
114 modifySubscriptionsOperationClass:modifySubscriptionsOperationClass
115 modifyRecordZonesOperationClass:modifyRecordZonesOperationClass
116 apsConnectionClass:apsConnectionClass]) {
117 __weak __typeof(self) weakSelf = self;
119 _incomingQueueOperations = [NSHashTable weakObjectsHashTable];
120 _outgoingQueueOperations = [NSHashTable weakObjectsHashTable];
121 _zoneChangeFetcher = [[CKKSZoneChangeFetcher alloc] initWithCKKSKeychainView: self];
123 _notifierClass = notifierClass;
124 _notifyViewChangedScheduler = [[CKKSNearFutureScheduler alloc] initWithName:[NSString stringWithFormat: @"%@-notify-scheduler", self.zoneName]
125 initialDelay:250*NSEC_PER_MSEC
126 continuingDelay:1*NSEC_PER_SEC
127 keepProcessAlive:true
129 __strong __typeof(self) strongSelf = weakSelf;
130 ckksnotice("ckks", strongSelf, "");
131 [strongSelf.notifierClass post:[NSString stringWithFormat:@"com.apple.security.view-change.%@", strongSelf.zoneName]];
133 // Ugly, but: the Manatee and Engram views need to send a fake 'PCS' view change.
134 // TODO: make this data-driven somehow
135 if([strongSelf.zoneName isEqualToString:@"Manatee"] || [strongSelf.zoneName isEqualToString:@"Engram"]) {
136 [strongSelf.notifierClass post:@"com.apple.security.view-change.PCS"];
140 _pendingSyncCallbacks = [[NSMutableDictionary alloc] init];
142 _lockStateTracker = lockStateTracker;
143 _savedTLKNotifier = savedTLKNotifier;
145 _setupSuccessful = false;
147 _keyHierarchyConditions = [[NSMutableDictionary alloc] init];
148 [CKKSZoneKeyStateMap() enumerateKeysAndObjectsUsingBlock:^(CKKSZoneKeyState * _Nonnull key, NSNumber * _Nonnull obj, BOOL * _Nonnull stop) {
149 [self.keyHierarchyConditions setObject: [[CKKSCondition alloc] init] forKey:key];
152 self.keyHierarchyState = SecCKKSZoneKeyStateInitializing;
153 _keyHierarchyError = nil;
154 _keyHierarchyOperationGroup = nil;
155 _keyStateMachineOperation = nil;
156 _keyStateFetchRequested = false;
157 _keyStateProcessRequested = false;
159 _keyStateReadyDependency = [CKKSResultOperation operationWithBlock:^{
160 ckksnotice("ckkskey", weakSelf, "Key state has become ready for the first time.");
162 self.keyStateReadyDependency.name = [NSString stringWithFormat: @"%@-key-state-ready", self.zoneName];
164 dispatch_time_t initializeDelay = SecCKKSTestsEnabled() ? NSEC_PER_MSEC * 500 : NSEC_PER_SEC * 30;
165 _initializeScheduler = [[CKKSNearFutureScheduler alloc] initWithName:[NSString stringWithFormat: @"%@-zone-initializer", self.zoneName]
167 continuingDelay:initializeDelay
168 keepProcessAlive:false
170 __strong __typeof(self) strongSelf = weakSelf;
171 ckksnotice("ckks", strongSelf, "initialize-scheduler restarting setup");
172 [strongSelf maybeRestartSetup];
179 - (NSString*)description {
180 return [NSString stringWithFormat:@"<%@: %@>", NSStringFromClass([self class]), self.zoneName];
183 - (NSString*)debugDescription {
184 return [NSString stringWithFormat:@"<%@: %@ %p>", NSStringFromClass([self class]), self.zoneName, self];
187 - (CKKSZoneKeyState*)keyHierarchyState {
188 return _keyHierarchyState;
191 - (void)setKeyHierarchyState:(CKKSZoneKeyState *)keyHierarchyState {
192 if((keyHierarchyState == nil && _keyHierarchyState == nil) || ([keyHierarchyState isEqualToString:_keyHierarchyState])) {
193 // No change, do nothing.
195 // Fixup the condition variables
196 if(_keyHierarchyState) {
197 self.keyHierarchyConditions[_keyHierarchyState] = [[CKKSCondition alloc] init];
199 if(keyHierarchyState) {
200 [self.keyHierarchyConditions[keyHierarchyState] fulfill];
204 _keyHierarchyState = keyHierarchyState;
207 - (NSString *)lastActiveTLKUUID
209 return self.activeTLK;
212 - (void) initializeZone {
213 // Unfortunate, but makes retriggering easy.
214 [self.initializeScheduler trigger];
217 - (void)maybeRestartSetup {
218 [self dispatchSync: ^bool{
219 if(self.setupStarted && !self.setupComplete) {
220 ckksdebug("ckks", self, "setup has restarted. Ignoring timer fire");
224 if(self.setupSuccessful) {
225 ckksdebug("ckks", self, "setup has completed successfully. Ignoring timer fire");
230 [self _onqueueInitializeZone];
237 self.setupSuccessful = false;
239 // Key hierarchy state machine resets, too
240 self.keyHierarchyState = SecCKKSZoneKeyStateInitializing;
241 _keyHierarchyError = nil;
244 - (void)_onqueueInitializeZone {
245 if(!SecCKKSIsEnabled()) {
246 ckksnotice("ckks", self, "Skipping CloudKit initialization due to disabled CKKS");
250 dispatch_assert_queue(self.queue);
252 __weak __typeof(self) weakSelf = self;
254 NSBlockOperation* afterZoneSetup = [NSBlockOperation blockOperationWithBlock: ^{
255 __strong __typeof(weakSelf) strongSelf = weakSelf;
257 ckkserror("ckks", strongSelf, "received callback for released object");
261 __block bool quit = false;
263 [strongSelf dispatchSync: ^bool {
264 ckksnotice("ckks", strongSelf, "Zone setup progress: %@ %d %@ %d %@",
265 [CKKSCKAccountStateTracker stringFromAccountStatus:strongSelf.accountStatus],
266 strongSelf.zoneCreated, strongSelf.zoneCreatedError, strongSelf.zoneSubscribed, strongSelf.zoneSubscribedError);
268 NSError* error = nil;
269 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state: strongSelf.zoneName];
270 ckse.ckzonecreated = strongSelf.zoneCreated;
271 ckse.ckzonesubscribed = strongSelf.zoneSubscribed;
273 // Although, if the zone subscribed error says there's no zone, mark down that there's no zone
274 if(strongSelf.zoneSubscribedError &&
275 [strongSelf.zoneSubscribedError.domain isEqualToString:CKErrorDomain] && strongSelf.zoneSubscribedError.code == CKErrorPartialFailure) {
276 NSError* subscriptionError = strongSelf.zoneSubscribedError.userInfo[CKPartialErrorsByItemIDKey][strongSelf.zoneID];
277 if(subscriptionError && [subscriptionError.domain isEqualToString:CKErrorDomain] && subscriptionError.code == CKErrorZoneNotFound) {
279 ckkserror("ckks", strongSelf, "zone subscription error appears to say the zone doesn't exist, fixing status: %@", strongSelf.zoneSubscribedError);
280 ckse.ckzonecreated = false;
284 [ckse saveToDatabase: &error];
286 ckkserror("ckks", strongSelf, "couldn't save zone creation status for %@: %@", strongSelf.zoneName, error);
289 if(!strongSelf.zoneCreated || !strongSelf.zoneSubscribed || strongSelf.accountStatus != CKAccountStatusAvailable) {
290 // Something has gone very wrong. Error out and maybe retry.
293 // Note that CKKSZone has probably called [handleLogout]; which means we have a key hierarchy reset queued up. Error here anyway.
294 NSError* realReason = strongSelf.zoneCreatedError ? strongSelf.zoneCreatedError : strongSelf.zoneSubscribedError;
295 [strongSelf _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateError withError: realReason];
297 // We're supposed to be up, but something has gone wrong. Blindly retry until it works.
298 if(strongSelf.accountStatus == CKKSAccountStatusAvailable) {
299 [strongSelf.initializeScheduler trigger];
300 ckksnotice("ckks", strongSelf, "We're logged in, but setup didn't work. Scheduling retry for %@", strongSelf.initializeScheduler.nextFireTime);
304 strongSelf.setupSuccessful = true;
311 ckkserror("ckks", strongSelf, "Quitting setup.");
315 // We can't enter the account queue until an account exists. Before this point, we don't know if one does.
316 [strongSelf dispatchSyncWithAccountQueue: ^bool{
317 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state: strongSelf.zoneName];
319 // Check if we believe we've synced this zone before.
320 if(ckse.changeToken == nil) {
321 strongSelf.keyHierarchyOperationGroup = [CKOperationGroup CKKSGroupWithName:@"initial-setup"];
323 ckksnotice("ckks", strongSelf, "No existing change token; going to try to match local items with CloudKit ones.");
325 // Onboard this keychain: there's likely items in it that we haven't synced yet.
326 // But, there might be items in The Cloud that correspond to these items, with UUIDs that we don't know yet.
327 // First, fetch all remote items.
328 CKKSResultOperation* fetch = [strongSelf.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseInitialStart];
329 fetch.name = @"initial-fetch";
331 // Next, try to process them (replacing local entries)
332 CKKSIncomingQueueOperation* initialProcess = [strongSelf processIncomingQueue: true after: fetch ];
333 initialProcess.name = @"initial-process-incoming-queue";
335 // If all that succeeds, iterate through all keychain items and find the ones which need to be uploaded
336 strongSelf.initialScanOperation = [[CKKSScanLocalItemsOperation alloc] initWithCKKSKeychainView:strongSelf ckoperationGroup:strongSelf.keyHierarchyOperationGroup];
337 strongSelf.initialScanOperation.name = @"initial-scan-operation";
338 [strongSelf.initialScanOperation addNullableDependency:strongSelf.lockStateTracker.unlockDependency];
339 [strongSelf.initialScanOperation addDependency: initialProcess];
340 [strongSelf scheduleOperation: strongSelf.initialScanOperation];
343 // Likely a restart of securityd!
345 strongSelf.keyHierarchyOperationGroup = [CKOperationGroup CKKSGroupWithName:@"restart-setup"];
347 if ([CKKSManifest shouldSyncManifests]) {
348 strongSelf.egoManifest = [CKKSEgoManifest tryCurrentEgoManifestForZone:strongSelf.zoneName];
351 // If it's been more than 24 hours since the last fetch, fetch and process everything.
352 // Otherwise, just kick off the local queue processing.
354 NSDate* now = [NSDate date];
355 NSDateComponents* offset = [[NSDateComponents alloc] init];
356 [offset setHour:-24];
357 NSDate* deadline = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:now options:0];
359 NSOperation* initialProcess = nil;
360 if(ckse.lastFetchTime == nil || [ckse.lastFetchTime compare: deadline] == NSOrderedAscending) {
361 initialProcess = [strongSelf fetchAndProcessCKChanges:CKKSFetchBecauseSecuritydRestart];
363 initialProcess = [strongSelf processIncomingQueue:false];
366 if(!strongSelf.egoManifest) {
367 ckksnotice("ckksmanifest", strongSelf, "No ego manifest on restart; rescanning");
368 strongSelf.initialScanOperation = [[CKKSScanLocalItemsOperation alloc] initWithCKKSKeychainView:strongSelf ckoperationGroup:strongSelf.keyHierarchyOperationGroup];
369 strongSelf.initialScanOperation.name = @"initial-scan-operation";
370 [strongSelf.initialScanOperation addNullableDependency:strongSelf.lockStateTracker.unlockDependency];
371 [strongSelf.initialScanOperation addDependency: initialProcess];
372 [strongSelf scheduleOperation: strongSelf.initialScanOperation];
375 [strongSelf processOutgoingQueue:strongSelf.keyHierarchyOperationGroup];
378 // Tell the key state machine to fire off.
379 [strongSelf _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateInitialized withError: nil];
383 afterZoneSetup.name = @"view-setup";
385 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state: self.zoneName];
386 NSOperation* zoneSetupOperation = [self createSetupOperation: ckse.ckzonecreated zoneSubscribed: ckse.ckzonesubscribed];
388 self.viewSetupOperation = [[CKKSGroupOperation alloc] init];
389 self.viewSetupOperation.name = @"view-setup-group";
390 if(!zoneSetupOperation.isFinished) {
391 [self.viewSetupOperation runBeforeGroupFinished: zoneSetupOperation];
394 [afterZoneSetup addDependency: zoneSetupOperation];
395 [self.viewSetupOperation runBeforeGroupFinished: afterZoneSetup];
397 [self scheduleAccountStatusOperation: self.viewSetupOperation];
400 - (bool)_onqueueResetLocalData: (NSError * __autoreleasing *) error {
401 dispatch_assert_queue(self.queue);
403 NSError* localerror = nil;
404 bool setError = false; // Ugly, but this is the only way to return the first error given
406 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state: self.zoneName];
407 ckse.ckzonecreated = false;
408 ckse.ckzonesubscribed = false; // I'm actually not sure about this: can you be subscribed to a non-existent zone?
409 ckse.changeToken = NULL;
410 [ckse saveToDatabase: &localerror];
412 ckkserror("ckks", self, "couldn't reset zone status for %@: %@", self.zoneName, localerror);
413 if(error && !setError) {
414 *error = localerror; setError = true;
418 [CKKSMirrorEntry deleteAll:self.zoneID error: &localerror];
420 ckkserror("ckks", self, "couldn't delete all CKKSMirrorEntry: %@", localerror);
421 if(error && !setError) {
422 *error = localerror; setError = true;
426 [CKKSOutgoingQueueEntry deleteAll:self.zoneID error: &localerror];
428 ckkserror("ckks", self, "couldn't delete all CKKSOutgoingQueueEntry: %@", localerror);
429 if(error && !setError) {
430 *error = localerror; setError = true;
434 [CKKSIncomingQueueEntry deleteAll:self.zoneID error: &localerror];
436 ckkserror("ckks", self, "couldn't delete all CKKSIncomingQueueEntry: %@", localerror);
437 if(error && !setError) {
438 *error = localerror; setError = true;
442 [CKKSKey deleteAll:self.zoneID error: &localerror];
444 ckkserror("ckks", self, "couldn't delete all CKKSKey: %@", localerror);
445 if(error && !setError) {
446 *error = localerror; setError = true;
450 [CKKSCurrentKeyPointer deleteAll:self.zoneID error: &localerror];
452 ckkserror("ckks", self, "couldn't delete all CKKSCurrentKeyPointer: %@", localerror);
453 if(error && !setError) {
454 *error = localerror; setError = true;
458 [CKKSDeviceStateEntry deleteAll:self.zoneID error:&localerror];
460 ckkserror("ckks", self, "couldn't delete all CKKSDeviceStateEntry: %@", localerror);
461 if(error && !setError) {
462 *error = localerror; setError = true;
466 [self _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateInitializing withError:nil];
468 return (localerror == nil && !setError);
471 - (CKKSResultOperation*)resetLocalData {
472 __weak __typeof(self) weakSelf = self;
474 CKKSGroupOperation* resetFollowUp = [[CKKSGroupOperation alloc] init];
475 resetFollowUp.name = @"local-reset-follow-up-group";
476 __weak __typeof(resetFollowUp) weakResetFollowUp = resetFollowUp;
478 CKKSResultOperation* op = [[CKKSResultOperation alloc] init];
479 op.name = @"local-reset";
481 __weak __typeof(op) weakOp = op;
482 [op addExecutionBlock:^{
483 __strong __typeof(self) strongSelf = weakSelf;
484 __strong __typeof(op) strongOp = weakOp;
485 __strong __typeof(resetFollowUp) strongResetFollowUp = weakResetFollowUp;
486 if(!strongSelf || !strongOp || !strongResetFollowUp) {
490 __block NSError* error = nil;
492 [strongSelf dispatchSync: ^bool{
493 [self _onqueueResetLocalData: &error];
497 [strongSelf resetSetup];
500 ckksnotice("ckksreset", strongSelf, "Local reset finished with error %@", error);
501 strongOp.error = error;
503 if(strongSelf.accountStatus == CKKSAccountStatusAvailable) {
504 // Since we're logged in, we expect a reset to fix up the key hierarchy
505 ckksnotice("ckksreset", strongSelf, "logged in; re-initializing zone");
506 [strongSelf initializeZone];
508 ckksnotice("ckksreset", strongSelf, "waiting for key hierarchy to become ready");
509 CKKSResultOperation* waitOp = [CKKSResultOperation named:@"waiting-for-key-hierarchy" withBlock:^{}];
510 [waitOp timeout: 60*NSEC_PER_SEC];
511 [waitOp addNullableDependency:strongSelf.keyStateReadyDependency];
513 [strongResetFollowUp runBeforeGroupFinished:waitOp];
515 ckksnotice("ckksreset", strongSelf, "logged out; not initializing zone");
520 [resetFollowUp runBeforeGroupFinished:op];
521 [self scheduleOperationWithoutDependencies:resetFollowUp];
522 return resetFollowUp;
525 - (CKKSResultOperation*)resetCloudKitZone {
526 if(!SecCKKSIsEnabled()) {
527 ckksinfo("ckks", self, "Skipping CloudKit reset due to disabled CKKS");
531 CKKSResultOperation* reset = [super beginResetCloudKitZoneOperation];
533 __weak __typeof(self) weakSelf = self;
534 CKKSGroupOperation* resetFollowUp = [[CKKSGroupOperation alloc] init];
535 resetFollowUp.name = @"cloudkit-reset-follow-up-group";
537 __weak __typeof(resetFollowUp) weakResetFollowUp = resetFollowUp;
538 [resetFollowUp runBeforeGroupFinished: [CKKSResultOperation named:@"cloudkit-reset-follow-up" withBlock: ^{
539 __strong __typeof(weakSelf) strongSelf = weakSelf;
541 ckkserror("ckks", strongSelf, "received callback for released object");
544 __strong __typeof(resetFollowUp) strongResetFollowUp = weakResetFollowUp;
547 ckksnotice("ckks", strongSelf, "Successfully deleted zone %@", strongSelf.zoneName);
548 __block NSError* error = nil;
550 [strongSelf dispatchSync: ^bool{
551 [strongSelf _onqueueResetLocalData: &error];
552 strongSelf.setupSuccessful = false;
556 if(strongSelf.accountStatus == CKKSAccountStatusAvailable) {
557 // Since we're logged in, we expect a reset to fix up the key hierarchy
558 ckksnotice("ckksreset", strongSelf, "re-initializing zone");
559 [strongSelf initializeZone];
561 ckksnotice("ckksreset", strongSelf, "waiting for key hierarchy to become ready");
562 CKKSResultOperation* waitOp = [CKKSResultOperation named:@"waiting-for-reset" withBlock:^{}];
563 [waitOp timeout: 60*NSEC_PER_SEC];
564 [waitOp addNullableDependency:strongSelf.keyStateReadyDependency];
566 [strongResetFollowUp runBeforeGroupFinished:waitOp];
568 ckksnotice("ckksreset", strongSelf, "logged out; not initializing zone");
571 // Shouldn't ever happen, since reset is a successDependency
572 ckkserror("ckks", strongSelf, "Couldn't reset zone %@: %@", strongSelf.zoneName, reset.error);
576 [resetFollowUp addSuccessDependency:reset];
577 [self scheduleOperationWithoutDependencies:resetFollowUp];
578 return resetFollowUp;
581 - (void)advanceKeyStateMachine {
582 __weak __typeof(self) weakSelf = self;
584 [self dispatchAsync: ^bool{
585 __strong __typeof(weakSelf) strongSelf = weakSelf;
587 ckkserror("ckks", strongSelf, "received callback for released object");
591 [strongSelf _onqueueAdvanceKeyStateMachineToState: nil withError: nil];
596 - (void)_onqueueKeyStateMachineRequestFetch {
597 dispatch_assert_queue(self.queue);
599 // We're going to set this flag, then nudge the key state machine.
600 // If it was idle, then it should launch a fetch. If there was an active process, this flag will stay high
601 // and the fetch will be launched later.
603 self.keyStateFetchRequested = true;
604 [self _onqueueAdvanceKeyStateMachineToState: nil withError: nil];
607 - (void)_onqueueKeyStateMachineRequestFullRefetch {
608 dispatch_assert_queue(self.queue);
610 self.keyStateFullRefetchRequested = true;
611 [self _onqueueAdvanceKeyStateMachineToState: nil withError: nil];
614 - (void)keyStateMachineRequestProcess {
615 __weak __typeof(self) weakSelf = self;
616 [self dispatchAsync: ^bool{
617 __strong __typeof(weakSelf) strongSelf = weakSelf;
619 ckkserror("ckks", strongSelf, "received callback for released object");
623 [strongSelf _onqueueKeyStateMachineRequestProcess];
628 - (void)_onqueueKeyStateMachineRequestProcess {
629 dispatch_assert_queue(self.queue);
631 // Set the request flag, then nudge the key state machine.
632 // If it was idle, then it should launch a fetch. If there was an active process, this flag will stay high
633 // and the fetch will be launched later.
635 self.keyStateProcessRequested = true;
636 [self _onqueueAdvanceKeyStateMachineToState: nil withError: nil];
639 // The operations suggested by this state machine should call _onqueueAdvanceKeyStateMachineToState once they are complete.
640 // At no other time should keyHierarchyState be modified.
642 // Note that this function cannot rely on doing any database work; it might get rolled back, especially in an error state
643 - (void)_onqueueAdvanceKeyStateMachineToState: (CKKSZoneKeyState*) state withError: (NSError*) error {
644 dispatch_assert_queue(self.queue);
645 __weak __typeof(self) weakSelf = self;
647 // Resetting back to 'initializing' takes all precedence.
648 if([state isEqual: SecCKKSZoneKeyStateInitializing]) {
649 ckksnotice("ckkskey", self, "Resetting the key hierarchy state machine back to 'initializing'");
651 [self.keyStateMachineOperation cancel];
652 self.keyStateMachineOperation = nil;
654 self.keyHierarchyState = SecCKKSZoneKeyStateInitializing;
655 self.keyHierarchyError = nil;
656 self.keyStateFetchRequested = false;
657 self.keyStateProcessRequested = false;
659 self.keyHierarchyOperationGroup = [CKOperationGroup CKKSGroupWithName:@"key-state-reset"];
660 self.keyStateReadyDependency = [CKKSResultOperation operationWithBlock:^{
661 ckksnotice("ckkskey", weakSelf, "Key state has become ready for the first time (after reset).");
663 self.keyStateReadyDependency.name = [NSString stringWithFormat: @"%@-key-state-ready", self.zoneName];
667 // Cancels and error states take precedence
668 if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateError] ||
669 [self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateCancelled] ||
670 self.keyHierarchyError != nil) {
671 // Error state: nowhere to go. Early-exit.
672 ckkserror("ckkskey", self, "Asked to advance state machine from non-exit state %@: %@", self.keyHierarchyState, self.keyHierarchyError);
676 if(error != nil || [state isEqual: SecCKKSZoneKeyStateError]) {
677 // But wait! Is this a "we're locked" error?
678 if([self.lockStateTracker isLockedError:error]) {
679 ckkserror("ckkskey", self, "advised of 'keychain locked' error, ignoring: coming from state (%@): %@", self.keyHierarchyState, error);
680 // After the next unlock, fake that we received the last zone transition
681 CKKSZoneKeyState* lastState = self.keyHierarchyState;
682 self.keyStateMachineOperation = [NSBlockOperation named:@"key-state-after-unlock" withBlock:^{
683 __strong __typeof(self) strongSelf = weakSelf;
687 [strongSelf dispatchSync:^bool{
688 [strongSelf _onqueueAdvanceKeyStateMachineToState:lastState withError:nil];
693 [self.keyStateMachineOperation addNullableDependency:self.lockStateTracker.unlockDependency];
694 [self scheduleOperation:self.keyStateMachineOperation];
697 // Error state: record the error and exit early
698 ckkserror("ckkskey", self, "advised of error: coming from state (%@): %@", self.keyHierarchyState, error);
699 self.keyHierarchyState = SecCKKSZoneKeyStateError;
700 self.keyHierarchyError = error;
705 if([state isEqual: SecCKKSZoneKeyStateCancelled]) {
706 ckkserror("ckkskey", self, "advised of cancel: coming from state (%@): %@", self.keyHierarchyState, error);
707 self.keyHierarchyState = SecCKKSZoneKeyStateCancelled;
708 self.keyHierarchyError = error;
710 // 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.
711 self.keyHierarchyOperationGroup = nil;
712 [self.keyStateReadyDependency cancel];
713 self.keyStateReadyDependency = nil;
717 // Now that the current or new state isn't an error or a cancel, proceed.
719 if(self.keyStateMachineOperation && ![self.keyStateMachineOperation isFinished]) {
721 // 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
722 ckksnotice("ckkskey", self, "Not advancing state machine: waiting for %@", self.keyStateMachineOperation);
728 self.keyStateMachineOperation = nil;
730 ckksnotice("ckkskey", self, "Advancing key hierarchy state machine from %@ to %@", self.keyHierarchyState, state);
731 self.keyHierarchyState = state;
734 // Many of our decisions below will be based on what keys exist. Help them out.
735 CKKSCurrentKeySet* keyset = [[CKKSCurrentKeySet alloc] initForZone:self.zoneID];
736 NSError* localerror = nil;
737 NSArray<CKKSKey*>* localKeys = [CKKSKey localKeys:self.zoneID error:&localerror];
738 NSArray<CKKSKey*>* remoteKeys = [CKKSKey remoteKeys:self.zoneID error: &localerror];
740 // We also are checking for OutgoingQueueEntries in the reencrypt state; this is a sign that our key hierarchy is out of date.
741 NSArray<CKKSOutgoingQueueEntry*>* outdatedOQEs = [CKKSOutgoingQueueEntry allInState: SecCKKSStateReencrypt zoneID:self.zoneID error: &localerror];
743 SecADSetValueForScalarKey((__bridge CFStringRef) SecCKKSAggdViewKeyCount, [localKeys count]);
746 ckkserror("ckkskey", self, "couldn't fetch keys and OQEs from local database, entering error state: %@", localerror);
747 self.keyHierarchyState = SecCKKSZoneKeyStateError;
748 self.keyHierarchyError = localerror;
753 NSArray<CKKSKey*>* allKeys = [CKKSKey allKeys:self.zoneID error:&localerror];
754 ckksdebug("ckkskey", self, "All keys: %@", allKeys);
757 NSError* hierarchyError = nil;
759 if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateInitializing]) {
761 // Wait for CKKSZone to finish initialization.
762 ckkserror("ckkskey", self, "Asked to advance state machine (to %@) while CK zone still initializing.", state);
766 } else if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateReady]) {
767 if(self.keyStateProcessRequested || [remoteKeys count] > 0) {
768 // We've either received some remote keys from the last fetch, or someone has requested a reprocess.
769 ckksnotice("ckkskey", self, "Kicking off a key reprocess based on request:%d and remote key count %lu", self.keyStateProcessRequested, (unsigned long)[remoteKeys count]);
770 [self _onqueueKeyHierarchyProcess];
772 } else if(self.keyStateFullRefetchRequested) {
773 // In ready, but someone has requested a full fetch. Kick it off.
774 ckksnotice("ckkskey", self, "Kicking off a key refetch based on request:%d", self.keyStateFetchRequested);
775 [self _onqueueKeyHierarchyRefetch];
777 } else if(self.keyStateFetchRequested) {
778 // In ready, but someone has requested a fetch. Kick it off.
779 ckksnotice("ckkskey", self, "Kicking off a key refetch based on request:%d", self.keyStateFetchRequested);
780 [self _onqueueKeyHierarchyFetch];
782 // TODO: kick off a key roll if one has been requested
784 if(!self.keyStateMachineOperation) {
785 // We think we're ready. Double check.
786 bool ready = [self _onqueueEnsureKeyHierarchyHealth:&hierarchyError];
787 if(!ready || hierarchyError) {
788 // Things is bad. Kick off a heal to fix things up.
789 ckksnotice("ckkskey", self, "Thought we were ready, but the key hierarchy is unhealthy: %@", hierarchyError);
790 self.keyHierarchyState = SecCKKSZoneKeyStateUnhealthy;
793 // In ready, nothing to do. Notify waiters and quit.
794 self.keyHierarchyOperationGroup = nil;
795 if(self.keyStateReadyDependency) {
796 [self scheduleOperation: self.keyStateReadyDependency];
797 self.keyStateReadyDependency = nil;
800 // If there are any OQEs waiting to be encrypted, launch an op to fix them
801 if([outdatedOQEs count] > 0u) {
802 ckksnotice("ckksreencrypt", self, "Reencrypting outgoing items as the key hierarchy is ready");
803 CKKSReencryptOutgoingItemsOperation* op = [[CKKSReencryptOutgoingItemsOperation alloc] initWithCKKSKeychainView:self ckoperationGroup:self.keyHierarchyOperationGroup];
804 [self scheduleOperation:op];
811 } else if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateInitialized]) {
812 // We're initialized and CloudKit is ready. See what needs done...
814 // Check if we have an existing key hierarchy
815 CKKSKey* tlk = [CKKSKey currentKeyForClass:SecCKKSKeyClassTLK zoneID:self.zoneID error:&error];
816 CKKSKey* classA = [CKKSKey currentKeyForClass:SecCKKSKeyClassA zoneID:self.zoneID error:&error];
817 CKKSKey* classC = [CKKSKey currentKeyForClass:SecCKKSKeyClassC zoneID:self.zoneID error:&error];
819 if(error && !([error.domain isEqual: @"securityd"] && error.code == errSecItemNotFound)) {
820 ckkserror("ckkskey", self, "Error examining existing key hierarchy: %@", error);
823 if(tlk && classA && classC && !error) {
824 // This is likely a restart of securityd, and we think we're ready. Double check.
825 bool ready = [self _onqueueEnsureKeyHierarchyHealth:&hierarchyError];
826 if(ready && !hierarchyError) {
827 ckksnotice("ckkskey", self, "Already have existing key hierarchy for %@; using it.", self.zoneID.zoneName);
828 } else if(hierarchyError && [self.lockStateTracker isLockedError:hierarchyError]) {
829 ckksnotice("ckkskey", self, "Initial scan shows key hierarchy is unavailable since keychain is locked: %@", hierarchyError);
830 self.keyHierarchyState = SecCKKSZoneKeyStateWaitForUnlock;
832 ckksnotice("ckkskey", self, "Initial scan shows key hierarchy is unhealthy: %@", hierarchyError);
833 self.keyHierarchyState = SecCKKSZoneKeyStateUnhealthy;
837 // We have no local key hierarchy. One might exist in CloudKit, or it might not.
838 ckksnotice("ckkskey", self, "No existing key hierarchy for %@. Check if there's one in CloudKit...", self.zoneID.zoneName);
840 [self _onqueueKeyHierarchyFetch];
843 } else if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateFetchComplete]) {
844 // We're initializing this zone, and just completed a fetch of everything. Are there any remote keys?
845 if(remoteKeys.count > 0u) {
846 // Process the keys we received.
847 self.keyStateMachineOperation = [[CKKSProcessReceivedKeysOperation alloc] initWithCKKSKeychainView: self];
848 } else if( (keyset.currentTLKPointer || keyset.currentClassAPointer || keyset.currentClassCPointer) &&
849 !(keyset.tlk && keyset.classA && keyset.classC)) {
850 // Huh. We appear to have current key pointers, but the keys themselves don't exist. That's weird.
851 // Transfer to the "unhealthy" state to request a fix
852 ckksnotice("ckkskey", self, "We appear to have current key pointers but no keys to match them. Moving to 'unhealthy'");
853 self.keyHierarchyState = SecCKKSZoneKeyStateUnhealthy;
855 } else if([remoteKeys count] == 0) {
856 // No keys, no pointers? make some new ones!
857 self.keyStateMachineOperation = [[CKKSNewTLKOperation alloc] initWithCKKSKeychainView: self ckoperationGroup:self.keyHierarchyOperationGroup];
860 } else if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateWaitForTLK]) {
861 // We're in a hold state: waiting for the TLK bytes to arrive.
863 if(self.keyStateProcessRequested) {
864 // Someone has requsted a reprocess! Run a ProcessReceivedKeysOperation.
865 ckksnotice("ckkskey", self, "Received a nudge that our TLK might be here! Starting operation to check.");
866 [self _onqueueKeyHierarchyProcess];
869 } else if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateWaitForUnlock]) {
870 // We're in a hold state: waiting for the keybag to unlock so we can process the keys again.
872 [self _onqueueKeyHierarchyProcess];
873 [self.keyStateMachineOperation addNullableDependency: self.lockStateTracker.unlockDependency];
875 } else if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateBadCurrentPointers]) {
876 // The current key pointers are broken, but we're not sure why.
877 ckksnotice("ckkskey", self, "Our current key pointers are reported broken. Attempting a fix!");
878 self.keyStateMachineOperation = [[CKKSHealKeyHierarchyOperation alloc] initWithCKKSKeychainView: self ckoperationGroup:self.keyHierarchyOperationGroup];
880 } else if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateNewTLKsFailed]) {
881 ckksnotice("ckkskey", self, "Creating new TLKs didn't work. Attempting to refetch!");
882 [self _onqueueKeyHierarchyFetch];
884 } else if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateNeedFullRefetch]) {
885 ckksnotice("ckkskey", self, "Informed of request for full refetch");
886 [self _onqueueKeyHierarchyRefetch];
889 ckkserror("ckks", self, "asked to advance state machine to unknown state: %@", self.keyHierarchyState);
893 if(self.keyStateMachineOperation) {
895 if(self.keyStateReadyDependency == nil || [self.keyStateReadyDependency isFinished]) {
896 ckksnotice("ckkskey", self, "reloading keyStateReadyDependency due to operation %@", self.keyStateMachineOperation);
898 __weak __typeof(self) weakSelf = self;
899 self.keyHierarchyOperationGroup = [CKOperationGroup CKKSGroupWithName:@"key-state-broken"];
900 self.keyStateReadyDependency = [CKKSResultOperation operationWithBlock:^{
901 ckksnotice("ckkskey", weakSelf, "Key state has become ready again.");
903 self.keyStateReadyDependency.name = [NSString stringWithFormat: @"%@-key-state-ready", self.zoneName];
906 [self scheduleOperation: self.keyStateMachineOperation];
907 } else if([self.keyHierarchyState isEqualToString:SecCKKSZoneKeyStateWaitForTLK]) {
908 ckksnotice("ckkskey", self, "Entering %@", self.keyHierarchyState);
910 } else if([self.keyHierarchyState isEqualToString:SecCKKSZoneKeyStateUnhealthy]) {
911 ckksnotice("ckkskey", self, "Looks like the key hierarchy is unhealthy. Launching fix.");
912 self.keyStateMachineOperation = [[CKKSHealKeyHierarchyOperation alloc] initWithCKKSKeychainView:self ckoperationGroup:self.keyHierarchyOperationGroup];
913 [self scheduleOperation: self.keyStateMachineOperation];
916 // Nothing to do and not in a waiting state? Awesome; we must be in the ready state.
917 if(![self.keyHierarchyState isEqual: SecCKKSZoneKeyStateReady]) {
918 ckksnotice("ckkskey", self, "No action to take in state %@; we must be ready.", self.keyHierarchyState);
919 self.keyHierarchyState = SecCKKSZoneKeyStateReady;
921 self.keyHierarchyOperationGroup = nil;
922 if(self.keyStateReadyDependency) {
923 [self scheduleOperation: self.keyStateReadyDependency];
924 self.keyStateReadyDependency = nil;
930 - (bool)_onqueueEnsureKeyHierarchyHealth:(NSError* __autoreleasing *)error {
931 dispatch_assert_queue(self.queue);
933 NSError* localerror = nil;
935 // Check if we have an existing key hierarchy
936 CKKSKey* tlk = [CKKSKey currentKeyForClass:SecCKKSKeyClassTLK zoneID:self.zoneID error:&localerror];
937 CKKSKey* classA = [CKKSKey currentKeyForClass:SecCKKSKeyClassA zoneID:self.zoneID error:&localerror];
938 CKKSKey* classC = [CKKSKey currentKeyForClass:SecCKKSKeyClassC zoneID:self.zoneID error:&localerror];
940 if(localerror || !tlk || !classA || !classC) {
941 ckkserror("ckkskey", self, "Error examining existing key hierarchy: %@", localerror);
942 ckkserror("ckkskey", self, "Keys are: %@ %@ %@", tlk, classA, classC);
949 // keychain being locked is not a fatal error here
950 [tlk loadKeyMaterialFromKeychain:&localerror];
951 if(localerror && !([localerror.domain isEqual: @"securityd"] && localerror.code == errSecInteractionNotAllowed)) {
952 ckksinfo("ckkskey", self, "Error loading TLK(%@): %@", tlk, localerror);
957 } else if(localerror) {
958 ckksinfo("ckkskey", self, "Error loading TLK(%@), maybe locked: %@", tlk, localerror);
962 // keychain being locked is not a fatal error here
963 [classA loadKeyMaterialFromKeychain:&localerror];
964 if(localerror && !([localerror.domain isEqual: @"securityd"] && localerror.code == errSecInteractionNotAllowed)) {
965 ckksinfo("ckkskey", self, "Error loading classA key(%@): %@", classA, localerror);
970 } else if(localerror) {
971 ckksinfo("ckkskey", self, "Error loading classA key(%@), maybe locked: %@", classA, localerror);
975 // keychain being locked is a fatal error here, since this is class C
976 [classA loadKeyMaterialFromKeychain:&localerror];
978 ckksinfo("ckkskey", self, "Error loading classC(%@): %@", classC, localerror);
985 self.activeTLK = [tlk uuid];
987 // Got to the bottom? Cool! All keys are present and accounted for.
991 - (void)_onqueueKeyHierarchyFetch {
992 dispatch_assert_queue(self.queue);
994 __weak __typeof(self) weakSelf = self;
995 self.keyStateMachineOperation = [NSBlockOperation blockOperationWithBlock: ^{
996 __strong __typeof(weakSelf) strongSelf = weakSelf;
998 ckkserror("ckks", strongSelf, "received callback for released object");
1002 [strongSelf dispatchSync: ^bool{
1003 [strongSelf _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateFetchComplete withError: nil];
1007 self.keyStateMachineOperation.name = @"waiting-for-fetch";
1009 NSOperation* fetchOp = [self.zoneChangeFetcher requestSuccessfulFetch: CKKSFetchBecauseKeyHierarchy];
1010 [self.keyStateMachineOperation addDependency: fetchOp];
1012 self.keyStateFetchRequested = false;
1015 - (void)_onqueueKeyHierarchyRefetch {
1016 dispatch_assert_queue(self.queue);
1018 __weak __typeof(self) weakSelf = self;
1019 self.keyStateMachineOperation = [NSBlockOperation blockOperationWithBlock: ^{
1020 __strong __typeof(weakSelf) strongSelf = weakSelf;
1022 ckkserror("ckks", strongSelf, "received callback for released object");
1026 [strongSelf dispatchSync: ^bool{
1027 [strongSelf _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateFetchComplete withError: nil];
1031 self.keyStateMachineOperation.name = @"waiting-for-refetch";
1033 NSOperation* fetchOp = [self.zoneChangeFetcher requestSuccessfulResyncFetch: CKKSFetchBecauseKeyHierarchy];
1034 [self.keyStateMachineOperation addDependency: fetchOp];
1036 self.keyStateMachineRefetched = true;
1037 self.keyStateFullRefetchRequested = false;
1038 self.keyStateFetchRequested = false;
1042 - (void)_onqueueKeyHierarchyProcess {
1043 dispatch_assert_queue(self.queue);
1045 self.keyStateMachineOperation = [[CKKSProcessReceivedKeysOperation alloc] initWithCKKSKeychainView: self];
1047 // Since we're starting a reprocess, this is answering all previous requests.
1048 self.keyStateProcessRequested = false;
1051 - (void) handleKeychainEventDbConnection: (SecDbConnectionRef) dbconn
1052 added: (SecDbItemRef) added
1053 deleted: (SecDbItemRef) deleted
1054 rateLimiter: (CKKSRateLimiter*) rateLimiter
1055 syncCallback: (SecBoolNSErrorCallback) syncCallback {
1056 if(!SecCKKSIsEnabled()) {
1057 ckksinfo("ckks", self, "Skipping handleKeychainEventDbConnection due to disabled CKKS");
1061 __block NSError* error = nil;
1063 if(self.accountStatus != CKKSAccountStatusAvailable && syncCallback) {
1064 // We're not logged into CloudKit, and therefore don't expect this item to be synced anytime particularly soon.
1065 CKKSAccountStatus accountStatus = self.accountStatus;
1066 dispatch_async(self.queue, ^{
1067 syncCallback(false, [NSError errorWithDomain:@"securityd"
1068 code:errSecNotLoggedIn
1069 userInfo:@{NSLocalizedDescriptionKey:
1070 [NSString stringWithFormat: @"No iCloud account available(%d); item is not expected to sync", (int)accountStatus]}]);
1076 // Tombstones come in as item modifications or item adds. Handle modifications here.
1077 bool addedTombstone = added && SecDbItemIsTombstone(added);
1078 bool deletedTombstone = deleted && SecDbItemIsTombstone(deleted);
1080 bool addedSync = added && SecDbItemIsSyncable(added);
1081 bool deletedSync = deleted && SecDbItemIsSyncable(deleted);
1083 bool isAdd = ( added && !deleted) || (added && deleted && !addedTombstone && deletedTombstone) || (added && deleted && addedSync && !deletedSync);
1084 bool isDelete = (!added && deleted) || (added && deleted && addedTombstone && !deletedTombstone) || (added && deleted && !addedSync && deletedSync);
1085 bool isModify = ( added && deleted) && (!isAdd) && (!isDelete);
1087 // 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.
1088 // Therefore, we might receive an added tombstone here with no deleted item to accompany it. This should be considered a deletion.
1089 if(addedTombstone && !deleted) {
1094 // Passed to withItem: below
1098 // If neither item is syncable, don't proceed further in the syncing system
1099 bool proceed = addedSync || deletedSync;
1102 ckksinfo("ckks", self, "skipping sync of non-sync item");
1106 // Only synchronize items which can transfer between devices
1107 NSString* protection = (__bridge NSString*)SecDbItemGetCachedValueWithName(added ? added : deleted, kSecAttrAccessible);
1108 if(! ([protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleWhenUnlocked] ||
1109 [protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAfterFirstUnlock] ||
1110 [protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAlways])) {
1111 ckksinfo("ckks", self, "skipping sync of device-bound item");
1115 // Our caller gave us a database connection. We must get on the local queue to ensure atomicity
1116 // Note that we're at the mercy of the surrounding db transaction, so don't try to rollback here
1117 [self dispatchSyncWithConnection: dbconn block: ^bool {
1118 if(![self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateReady]) {
1119 ckksnotice("ckks", self, "Key state not ready for new items; skipping");
1123 CKKSOutgoingQueueEntry* oqe = nil;
1125 oqe = [CKKSOutgoingQueueEntry withItem: added action: SecCKKSActionAdd ckks:self error: &error];
1126 } else if(isDelete) {
1127 oqe = [CKKSOutgoingQueueEntry withItem: deleted action: SecCKKSActionDelete ckks:self error: &error];
1128 } else if(isModify) {
1129 oqe = [CKKSOutgoingQueueEntry withItem: added action: SecCKKSActionModify ckks:self error: &error];
1131 ckkserror("ckks", self, "processKeychainEventItemAdded given garbage: %@ %@", added, deleted);
1135 CKOperationGroup* operationGroup = [CKOperationGroup CKKSGroupWithName:@"keychain-api-use"];
1138 ckkserror("ckks", self, "Couldn't create outgoing queue entry: %@", error);
1140 // If the problem is 'no UUID', launch a scan operation to find and fix it
1141 // We don't want to fix it up here, in the closing moments of a transaction
1142 if([error.domain isEqualToString:@"securityd"] && error.code == CKKSNoUUIDOnItem) {
1143 ckksnotice("ckks", self, "Launching scan operation");
1144 CKKSScanLocalItemsOperation* scanOperation = [[CKKSScanLocalItemsOperation alloc] initWithCKKSKeychainView: self ckoperationGroup:operationGroup];
1145 [self scheduleOperation: scanOperation];
1148 // If the problem is 'couldn't load key', tell the key hierarchy state machine to fix it
1149 // Then, launch a scan operation to find this item and upload it
1150 if([error.domain isEqualToString:@"securityd"] && error.code == errSecItemNotFound) {
1151 [self _onqueueAdvanceKeyStateMachineToState: nil withError: nil];
1153 ckksnotice("ckks", self, "Launching scan operation to refind %@", added);
1154 CKKSScanLocalItemsOperation* scanOperation = [[CKKSScanLocalItemsOperation alloc] initWithCKKSKeychainView: self ckoperationGroup:operationGroup];
1155 [scanOperation addNullableDependency:self.keyStateReadyDependency];
1156 [self scheduleOperation: scanOperation];
1163 NSDate* limit = nil;
1164 NSInteger value = [rateLimiter judge:oqe at:[NSDate date] limitTime:&limit];
1166 oqe.waitUntil = limit;
1167 SecPLLogRegisteredEvent(@"CKKSSyncing", @{ @"ratelimit" : @(value), @"accessgroup" : oqe.accessgroup});
1171 [oqe saveToDatabaseWithConnection: dbconn error: &error];
1173 ckkserror("ckks", self, "Couldn't save outgoing queue entry to database: %@", error);
1177 // This update supercedes all other local modifications to this item (_except_ those in-flight).
1178 // Delete all items in reencrypt or error.
1179 CKKSOutgoingQueueEntry* reencryptOQE = [CKKSOutgoingQueueEntry tryFromDatabase:oqe.uuid state:SecCKKSStateReencrypt zoneID:self.zoneID error:&error];
1181 ckkserror("ckks", self, "Couldn't load reencrypt OQE sibling for %@: %@", oqe, error);
1184 [reencryptOQE deleteFromDatabase:&error];
1186 ckkserror("ckks", self, "Couldn't delete reencrypt OQE sibling(%@) for %@: %@", reencryptOQE, oqe, error);
1191 CKKSOutgoingQueueEntry* errorOQE = [CKKSOutgoingQueueEntry tryFromDatabase:oqe.uuid state:SecCKKSStateError zoneID:self.zoneID error:&error];
1193 ckkserror("ckks", self, "Couldn't load error OQE sibling for %@: %@", oqe, error);
1196 [errorOQE deleteFromDatabase:&error];
1198 ckkserror("ckks", self, "Couldn't delete error OQE sibling(%@) for %@: %@", reencryptOQE, oqe, error);
1203 self.pendingSyncCallbacks[oqe.uuid] = syncCallback;
1206 // Schedule a "view changed" notification
1207 [self.notifyViewChangedScheduler trigger];
1209 [self processOutgoingQueue:operationGroup];
1215 -(void)setCurrentItemForAccessGroup:(SecDbItemRef)newItem
1216 hash:(NSData*)newItemSHA1
1217 accessGroup:(NSString*)accessGroup
1218 identifier:(NSString*)identifier
1219 replacing:(SecDbItemRef)oldItem
1220 hash:(NSData*)oldItemSHA1
1221 complete:(void (^) (NSError* operror)) complete
1223 if(accessGroup == nil || identifier == nil) {
1224 NSError* error = [NSError errorWithDomain:@"securityd" code:errSecParam userInfo:@{NSLocalizedDescriptionKey: @"No access group or identifier given"}];
1225 ckkserror("ckkscurrent", self, "Cancelling request: %@", error);
1230 __weak __typeof(self) weakSelf = self;
1232 [self dispatchSync:^bool {
1233 NSError* error = nil;
1234 CFErrorRef cferror = NULL;
1236 NSString* newItemUUID = nil;
1237 NSString* oldItemUUID = nil;
1239 // Now that we're on the db queue, ensure that the given hashes for the items match the hashes as they are now.
1240 // That is, the items haven't changed since the caller knew about the item.
1241 NSData* newItemComputedSHA1 = (NSData*) CFBridgingRelease(CFRetainSafe(SecDbItemGetSHA1(newItem, &cferror)));
1242 if(!newItemComputedSHA1 || cferror ||
1243 ![newItemComputedSHA1 isEqual:newItemSHA1]) {
1244 ckksnotice("ckkscurrent", self, "Hash mismatch for new item: %@ vs %@", newItemComputedSHA1, newItemSHA1);
1245 error = [NSError errorWithDomain:@"securityd" code:errSecItemInvalidValue userInfo:@{NSLocalizedDescriptionKey: @"New item has changed; hashes mismatch. Refetch and try again."}];
1247 CFReleaseNull(cferror);
1251 newItemUUID = (NSString*) CFBridgingRelease(CFRetainSafe(SecDbItemGetValue(newItem, &v10itemuuid, &cferror)));
1252 if(!newItemUUID || cferror) {
1253 ckkserror("ckkscurrent", self, "Error fetching UUID for new item: %@", cferror);
1254 complete((__bridge NSError*) cferror);
1255 CFReleaseNull(cferror);
1260 NSData* oldItemComputedSHA1 = (NSData*) CFBridgingRelease(CFRetainSafe(SecDbItemGetSHA1(oldItem, &cferror)));
1261 if(!oldItemComputedSHA1 || cferror ||
1262 ![oldItemComputedSHA1 isEqual:oldItemSHA1]) {
1263 ckksnotice("ckkscurrent", self, "Hash mismatch for old item: %@ vs %@", oldItemComputedSHA1, oldItemSHA1);
1264 error = [NSError errorWithDomain:@"securityd" code:errSecItemInvalidValue userInfo:@{NSLocalizedDescriptionKey: @"Old item has changed; hashes mismatch. Refetch and try again."}];
1266 CFReleaseNull(cferror);
1270 oldItemUUID = (NSString*) CFBridgingRelease(CFRetainSafe(SecDbItemGetValue(oldItem, &v10itemuuid, &cferror)));
1271 if(!oldItemUUID || cferror) {
1272 ckkserror("ckkscurrent", self, "Error fetching UUID for old item: %@", cferror);
1273 complete((__bridge NSError*) cferror);
1274 CFReleaseNull(cferror);
1279 // Not being in a CloudKit account is an automatic failure.
1280 if(self.accountStatus != CKKSAccountStatusAvailable) {
1281 ckksnotice("ckkscurrent", self, "Rejecting current item pointer set since we don't have an iCloud account.");
1282 error = [NSError errorWithDomain:@"securityd" code:errSecNotLoggedIn userInfo:@{NSLocalizedDescriptionKey: @"User is not signed into iCloud."}];
1287 // At this point, we've completed all the checks we need for the SecDbItems. Try to launch this boat!
1288 NSString* currentIdentifier = [NSString stringWithFormat:@"%@-%@", accessGroup, identifier];
1289 ckksnotice("ckkscurrent", self, "Setting current pointer for %@ to %@ (from %@)", currentIdentifier, newItemUUID, oldItemUUID);
1290 CKKSUpdateCurrentItemPointerOperation* ucipo = [[CKKSUpdateCurrentItemPointerOperation alloc] initWithCKKSKeychainView:self
1291 currentPointer:(NSString*)currentIdentifier
1292 oldItemUUID:(NSString*)oldItemUUID
1293 newItemUUID:(NSString*)newItemUUID
1294 ckoperationGroup:[CKOperationGroup CKKSGroupWithName:@"currentitem-api"]];
1295 CKKSResultOperation* returnCallback = [CKKSResultOperation operationWithBlock:^{
1296 __strong __typeof(self) strongSelf = weakSelf;
1299 ckkserror("ckkscurrent", strongSelf, "Failed setting a current item pointer with %@", ucipo.error);
1301 ckksnotice("ckkscurrent", strongSelf, "Finished setting a current item pointer");
1303 complete(ucipo.error);
1305 returnCallback.name = @"setCurrentItem-return-callback";
1306 [returnCallback addDependency: ucipo];
1307 [self scheduleOperation: returnCallback];
1309 // Now, schedule ucipo. It modifies the CloudKit zone, so it should insert itself into the list of OutgoingQueueOperations.
1310 // Then, we won't have simultaneous zone-modifying operations.
1311 [ucipo linearDependencies:self.outgoingQueueOperations];
1313 // If this operation hasn't started within 60 seconds, cancel it and return a "timed out" error.
1314 [ucipo timeout:60*NSEC_PER_SEC];
1316 [self scheduleOperation:ucipo];
1322 -(void)getCurrentItemForAccessGroup:(NSString*)accessGroup
1323 identifier:(NSString*)identifier
1324 fetchCloudValue:(bool)fetchCloudValue
1325 complete:(void (^) (NSString* uuid, NSError* operror)) complete
1327 if(accessGroup == nil || identifier == nil) {
1328 complete(NULL, [NSError errorWithDomain:@"securityd" code:errSecParam userInfo:@{NSLocalizedDescriptionKey: @"No access group or identifier given"}]);
1332 // Not being in a CloudKit account is an automatic failure.
1333 if(self.accountStatus != CKKSAccountStatusAvailable) {
1334 ckksnotice("ckkscurrent", self, "Rejecting current item pointer get since we don't have an iCloud account.");
1335 complete(NULL, [NSError errorWithDomain:@"securityd" code:errSecNotLoggedIn userInfo:@{NSLocalizedDescriptionKey: @"User is not signed into iCloud."}]);
1339 CKKSResultOperation* fetchAndProcess = nil;
1340 if(fetchCloudValue) {
1341 fetchAndProcess = [self fetchAndProcessCKChanges:CKKSFetchBecauseCurrentItemFetchRequest];
1344 __weak __typeof(self) weakSelf = self;
1345 CKKSResultOperation* getCurrentItem = [CKKSResultOperation operationWithBlock:^{
1346 if(fetchAndProcess.error) {
1347 complete(NULL, fetchAndProcess.error);
1351 __strong __typeof(self) strongSelf = weakSelf;
1353 [strongSelf dispatchSync: ^bool {
1354 NSError* error = nil;
1355 NSString* currentIdentifier = [NSString stringWithFormat:@"%@-%@", accessGroup, identifier];
1357 CKKSCurrentItemPointer* cip = [CKKSCurrentItemPointer fromDatabase:currentIdentifier
1358 state:SecCKKSProcessedStateLocal
1359 zoneID:strongSelf.zoneID
1362 ckkserror("ckkscurrent", strongSelf, "No current item pointer for %@", currentIdentifier);
1363 complete(nil, error);
1367 if(!cip.currentItemUUID) {
1368 ckkserror("ckkscurrent", strongSelf, "Current item pointer is empty %@", cip);
1369 complete(nil, [NSError errorWithDomain:@"securityd"
1370 code:errSecInternalError
1371 userInfo:@{NSLocalizedDescriptionKey: @"Current item pointer is empty"}]);
1375 complete(cip.currentItemUUID, NULL);
1380 getCurrentItem.name = @"get-current-item-pointer";
1382 [getCurrentItem addNullableDependency:fetchAndProcess];
1383 [self scheduleOperation: getCurrentItem];
1386 - (CKKSKey*) keyForItem: (SecDbItemRef) item error: (NSError * __autoreleasing *) error {
1387 CKKSKeyClass* class = nil;
1389 NSString* protection = (__bridge NSString*)SecDbItemGetCachedValueWithName(item, kSecAttrAccessible);
1390 if([protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleWhenUnlocked]) {
1391 class = SecCKKSKeyClassA;
1392 } else if([protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAlways] ||
1393 [protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAfterFirstUnlock]) {
1394 class = SecCKKSKeyClassC;
1396 ckkserror("ckks", self, "can't pick key class for protection %@: %@", protection, item);
1398 *error =[NSError errorWithDomain:@"securityd"
1400 userInfo:@{NSLocalizedDescriptionKey:
1401 [NSString stringWithFormat:@"can't pick key class for protection %@: %@", protection, item]}];
1407 CKKSKey* key = [CKKSKey currentKeyForClass: class zoneID:self.zoneID error:error];
1409 // and make sure it's unwrapped.
1410 if(![key ensureKeyLoaded:error]) {
1417 // Use the following method to find the first pending operation in a weak collection
1418 - (NSOperation*)findFirstPendingOperation: (NSHashTable*) table {
1419 return [self findFirstPendingOperation:table ofClass:nil];
1422 // Use the following method to find the first pending operation in a weak collection
1423 - (NSOperation*)findFirstPendingOperation: (NSHashTable*) table ofClass:(Class)class {
1424 @synchronized(table) {
1425 for(NSOperation* op in table) {
1426 if(op != nil && [op isPending] && (class == nil || [op isKindOfClass: class])) {
1434 // Use the following method to count the pending operations in a weak collection
1435 - (int64_t)countPendingOperations: (NSHashTable*) table {
1436 @synchronized(table) {
1438 for(NSOperation* op in table) {
1439 if(op != nil && !([op isExecuting] || [op isFinished])) {
1447 - (CKKSOutgoingQueueOperation*)processOutgoingQueue:(CKOperationGroup*)ckoperationGroup {
1448 return [self processOutgoingQueueAfter:nil ckoperationGroup:ckoperationGroup];
1451 - (CKKSOutgoingQueueOperation*)processOutgoingQueueAfter:(CKKSResultOperation*)after ckoperationGroup:(CKOperationGroup*)ckoperationGroup {
1452 if(!SecCKKSIsEnabled()) {
1453 ckksinfo("ckks", self, "Skipping processOutgoingQueue due to disabled CKKS");
1457 CKKSOutgoingQueueOperation* outgoingop =
1458 (CKKSOutgoingQueueOperation*) [self findFirstPendingOperation:self.outgoingQueueOperations
1459 ofClass:[CKKSOutgoingQueueOperation class]];
1461 ckksinfo("ckks", self, "Skipping processOutgoingQueue due to at least one pending instance");
1463 [outgoingop addDependency: after];
1465 if([outgoingop isPending]) {
1466 if(!outgoingop.ckoperationGroup && ckoperationGroup) {
1467 outgoingop.ckoperationGroup = ckoperationGroup;
1468 } else if(ckoperationGroup) {
1469 ckkserror("ckks", self, "Throwing away CKOperationGroup(%@) in favor of %@", ckoperationGroup, outgoingop.ckoperationGroup);
1476 CKKSOutgoingQueueOperation* op = [[CKKSOutgoingQueueOperation alloc] initWithCKKSKeychainView:self ckoperationGroup:ckoperationGroup];
1477 op.name = @"outgoing-queue-operation";
1478 [op addNullableDependency:after];
1480 [op addNullableDependency: self.initialScanOperation];
1482 [self scheduleOperation: op];
1486 - (void)processIncomingQueueAfterNextUnlock {
1487 // Thread races aren't so important here; we might end up with two or three copies of this operation, but that's okay.
1488 if(![self.processIncomingQueueAfterNextUnlockOperation isPending]) {
1489 __weak __typeof(self) weakSelf = self;
1491 CKKSResultOperation* restartIncomingQueueOperation = [CKKSResultOperation operationWithBlock:^{
1492 __strong __typeof(self) strongSelf = weakSelf;
1493 // This IQO shouldn't error if the keybag has locked again. It will simply try again later.
1494 [strongSelf processIncomingQueue:false];
1497 restartIncomingQueueOperation.name = @"reprocess-incoming-queue-after-unlock";
1498 self.processIncomingQueueAfterNextUnlockOperation = restartIncomingQueueOperation;
1500 [restartIncomingQueueOperation addNullableDependency:self.lockStateTracker.unlockDependency];
1501 [self scheduleOperation: restartIncomingQueueOperation];
1505 - (CKKSIncomingQueueOperation*)processIncomingQueue:(bool)failOnClassA {
1506 return [self processIncomingQueue:failOnClassA after: nil];
1509 - (CKKSIncomingQueueOperation*) processIncomingQueue:(bool)failOnClassA after: (CKKSResultOperation*) after {
1510 if(!SecCKKSIsEnabled()) {
1511 ckksinfo("ckks", self, "Skipping processIncomingQueue due to disabled CKKS");
1515 CKKSIncomingQueueOperation* incomingop = (CKKSIncomingQueueOperation*) [self findFirstPendingOperation:self.incomingQueueOperations];
1517 ckksinfo("ckks", self, "Skipping processIncomingQueue due to at least one pending instance");
1519 [incomingop addDependency: after];
1521 // check (again) for race condition; if the op has started we need to add another (for the dependency)
1522 if([incomingop isPending]) {
1523 incomingop.errorOnClassAFailure |= failOnClassA;
1528 CKKSIncomingQueueOperation* op = [[CKKSIncomingQueueOperation alloc] initWithCKKSKeychainView:self errorOnClassAFailure:failOnClassA];
1529 op.name = @"incoming-queue-operation";
1531 [op addSuccessDependency: after];
1534 [self scheduleOperation: op];
1538 - (CKKSUpdateDeviceStateOperation*)updateDeviceState:(bool)rateLimit ckoperationGroup:(CKOperationGroup*)ckoperationGroup {
1539 if(!SecCKKSIsEnabled()) {
1540 ckksinfo("ckks", self, "Skipping updateDeviceState due to disabled CKKS");
1544 CKKSUpdateDeviceStateOperation* op = [[CKKSUpdateDeviceStateOperation alloc] initWithCKKSKeychainView:self rateLimit:rateLimit ckoperationGroup:ckoperationGroup];
1545 op.name = @"device-state-operation";
1547 // op modifies the CloudKit zone, so it should insert itself into the list of OutgoingQueueOperations.
1548 // Then, we won't have simultaneous zone-modifying operations and confuse ourselves.
1549 [op linearDependencies:self.outgoingQueueOperations];
1551 // CKKSUpdateDeviceStateOperations are special: they should fire even if we don't believe we're in an iCloud account.
1552 [self scheduleAccountStatusOperation:op];
1556 // There are some errors which won't be reported but will be reflected in the CDSE; any error coming out of here is fatal
1557 - (CKKSDeviceStateEntry*)_onqueueCurrentDeviceStateEntry: (NSError* __autoreleasing*)error {
1558 NSError* localerror = nil;
1560 CKKSCKAccountStateTracker* accountTracker = self.accountTracker;
1562 // We must have an iCloud account (with d2de on) to even create one of these
1563 if(accountTracker.currentCKAccountInfo.accountStatus != CKAccountStatusAvailable || accountTracker.currentCKAccountInfo.supportsDeviceToDeviceEncryption != YES) {
1564 ckkserror("ckksdevice", self, "No iCloud account active: %@", accountTracker.currentCKAccountInfo);
1565 localerror = [NSError errorWithDomain:@"securityd"
1566 code:errSecInternalError
1567 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"No active HSA2 iCloud account: %@", accountTracker.currentCKAccountInfo]}];
1569 *error = localerror;
1574 CKKSDeviceStateEntry* oldcdse = [CKKSDeviceStateEntry tryFromDatabase:accountTracker.ckdeviceID zoneID:self.zoneID error:&localerror];
1576 ckkserror("ckksdevice", self, "Couldn't read old CKKSDeviceStateEntry from database: %@", localerror);
1578 *error = localerror;
1583 // Find out what we think the current keys are
1584 CKKSCurrentKeyPointer* currentTLKPointer = [CKKSCurrentKeyPointer tryFromDatabase: SecCKKSKeyClassTLK zoneID:self.zoneID error:&localerror];
1585 CKKSCurrentKeyPointer* currentClassAPointer = [CKKSCurrentKeyPointer tryFromDatabase: SecCKKSKeyClassA zoneID:self.zoneID error:&localerror];
1586 CKKSCurrentKeyPointer* currentClassCPointer = [CKKSCurrentKeyPointer tryFromDatabase: SecCKKSKeyClassC zoneID:self.zoneID error:&localerror];
1588 // Things is broken, but the whole point of this record is to share the brokenness. Continue.
1589 ckkserror("ckksdevice", self, "Couldn't read current key pointers from database: %@; proceeding", localerror);
1593 CKKSKey* suggestedTLK = currentTLKPointer.currentKeyUUID ? [CKKSKey tryFromDatabase:currentTLKPointer.currentKeyUUID zoneID:self.zoneID error:&localerror] : nil;
1594 CKKSKey* suggestedClassAKey = currentClassAPointer.currentKeyUUID ? [CKKSKey tryFromDatabase:currentClassAPointer.currentKeyUUID zoneID:self.zoneID error:&localerror] : nil;
1595 CKKSKey* suggestedClassCKey = currentClassCPointer.currentKeyUUID ? [CKKSKey tryFromDatabase:currentClassCPointer.currentKeyUUID zoneID:self.zoneID error:&localerror] : nil;
1598 // Things is broken, but the whole point of this record is to share the brokenness. Continue.
1599 ckkserror("ckksdevice", self, "Couldn't read keys from database: %@; proceeding", localerror);
1603 // Check if we posess the keys in the keychain
1604 [suggestedTLK ensureKeyLoaded:&localerror];
1605 if(localerror && [self.lockStateTracker isLockedError:localerror]) {
1606 ckkserror("ckksdevice", self, "Device is locked; couldn't read TLK from keychain. Assuming it is present and continuing; error was %@", localerror);
1608 } else if(localerror) {
1609 ckkserror("ckksdevice", self, "Couldn't read TLK from keychain. We do not have a current TLK. Error was %@", localerror);
1613 [suggestedClassAKey ensureKeyLoaded:&localerror];
1614 if(localerror && [self.lockStateTracker isLockedError:localerror]) {
1615 ckkserror("ckksdevice", self, "Device is locked; couldn't read ClassA key from keychain. Assuming it is present and continuing; error was %@", localerror);
1617 } else if(localerror) {
1618 ckkserror("ckksdevice", self, "Couldn't read ClassA key from keychain. We do not have a current ClassA key. Error was %@", localerror);
1619 suggestedClassAKey = nil;
1622 [suggestedClassCKey ensureKeyLoaded:&localerror];
1623 // class C keys are stored class C, so uh, don't check lock state.
1625 ckkserror("ckksdevice", self, "Couldn't read ClassC key from keychain. We do not have a current ClassC key. Error was %@", localerror);
1626 suggestedClassCKey = nil;
1629 // 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
1630 if([accountTracker.accountCirclePeerIDInitialized wait:500*NSEC_PER_MSEC] != 0 && !accountTracker.accountCirclePeerID) {
1631 ckkserror("ckksdevice", self, "No peer ID available");
1634 // We only really want the oldcdse for its encodedCKRecord, so make a new cdse here
1635 CKKSDeviceStateEntry* newcdse = [[CKKSDeviceStateEntry alloc] initForDevice:accountTracker.ckdeviceID
1636 circlePeerID:accountTracker.accountCirclePeerID
1637 circleStatus:accountTracker.currentCircleStatus
1638 keyState:self.keyHierarchyState
1639 currentTLKUUID:suggestedTLK.uuid
1640 currentClassAUUID:suggestedClassAKey.uuid
1641 currentClassCUUID:suggestedClassCKey.uuid
1643 encodedCKRecord:oldcdse.encodedCKRecord];
1647 - (CKKSSynchronizeOperation*) resyncWithCloud {
1648 if(!SecCKKSIsEnabled()) {
1649 ckksinfo("ckks", self, "Skipping resyncWithCloud due to disabled CKKS");
1653 CKKSSynchronizeOperation* op = [[CKKSSynchronizeOperation alloc] initWithCKKSKeychainView: self];
1654 [self scheduleOperation: op];
1658 - (CKKSResultOperation*)fetchAndProcessCKChanges:(CKKSFetchBecause*)because {
1659 if(!SecCKKSIsEnabled()) {
1660 ckksinfo("ckks", self, "Skipping fetchAndProcessCKChanges due to disabled CKKS");
1664 // We fetched some changes; try to process them!
1665 return [self processIncomingQueue:false after:[self.zoneChangeFetcher requestSuccessfulFetch:because]];
1668 // Lets the view know about a failed CloudKit write. If the error is "already have one of these records", it will
1669 // store the new records and kick off the new processing
1671 // Note that you need to tell this function the records you wanted to save, so it can determine what needs deletion
1672 - (bool)_onqueueCKWriteFailed:(NSError*)ckerror attemptedRecordsChanged:(NSDictionary<CKRecordID*, CKRecord*>*)savedRecords {
1673 dispatch_assert_queue(self.queue);
1675 NSDictionary<CKRecordID*,NSError*>* partialErrors = ckerror.userInfo[CKPartialErrorsByItemIDKey];
1676 if([ckerror.domain isEqual:CKErrorDomain] && ckerror.code == CKErrorPartialFailure && partialErrors) {
1677 // Check if this error was "you're out of date"
1678 bool recordChanged = true;
1680 for(NSError* error in partialErrors.allValues) {
1681 if((![error.domain isEqual:CKErrorDomain]) || (error.code != CKErrorBatchRequestFailed && error.code != CKErrorServerRecordChanged && error.code != CKErrorUnknownItem)) {
1682 // There's an error in there that isn't CKErrorServerRecordChanged, CKErrorBatchRequestFailed, or CKErrorUnknownItem. Don't handle nicely...
1683 recordChanged = false;
1688 ckksnotice("ckks", self, "Received a ServerRecordChanged error, attempting to update new records and delete unknown ones");
1690 bool updatedRecord = false;
1692 for(CKRecordID* recordID in partialErrors.allKeys) {
1693 NSError* error = partialErrors[recordID];
1694 if([error.domain isEqual:CKErrorDomain] && error.code == CKErrorServerRecordChanged) {
1695 CKRecord* newRecord = error.userInfo[CKRecordChangedErrorServerRecordKey];
1696 ckksnotice("ckks", self, "On error: updating our idea of: %@", newRecord);
1698 updatedRecord |= [self _onqueueCKRecordChanged:newRecord resync:true];
1699 } else if([error.domain isEqual:CKErrorDomain] && error.code == CKErrorUnknownItem) {
1700 CKRecord* record = savedRecords[recordID];
1701 ckksnotice("ckks", self, "On error: handling an unexpected delete of: %@ %@", recordID, record);
1703 updatedRecord |= [self _onqueueCKRecordDeleted:recordID recordType:record.recordType resync:true];
1708 [self processIncomingQueue:false];
1717 - (bool)_onqueueCKRecordDeleted:(CKRecordID*)recordID recordType:(NSString*)recordType resync:(bool)resync {
1718 dispatch_assert_queue(self.queue);
1720 // TODO: resync doesn't really mean much here; what does it mean for a record to be 'deleted' if you're fetching from scratch?
1722 if([recordType isEqual: SecCKRecordItemType]) {
1723 ckksinfo("ckks", self, "CloudKit notification: deleted record(%@): %@", recordType, recordID);
1724 NSError* error = nil;
1725 NSError* iqeerror = nil;
1726 CKKSMirrorEntry* ckme = [CKKSMirrorEntry fromDatabase: [recordID recordName] zoneID:self.zoneID error: &error];
1728 // Deletes always succeed, not matter the generation count
1730 [ckme deleteFromDatabase:&error];
1732 CKKSIncomingQueueEntry* iqe = [[CKKSIncomingQueueEntry alloc] initWithCKKSItem:ckme.item action:SecCKKSActionDelete state:SecCKKSStateNew];
1733 [iqe saveToDatabase:&iqeerror];
1735 ckkserror("ckks", self, "Couldn't save incoming queue entry: %@", iqeerror);
1738 ckksinfo("ckks", self, "CKKSMirrorEntry was deleted: %@ %@ error: %@", recordID, ckme, error);
1739 // TODO: actually pass error back up
1740 return (error == nil);
1742 } else if([recordType isEqual: SecCKRecordCurrentItemType]) {
1743 ckksinfo("ckks", self, "CloudKit notification: deleted current item pointer(%@): %@", recordType, recordID);
1744 NSError* error = nil;
1746 [[CKKSCurrentItemPointer tryFromDatabase:[recordID recordName] state:SecCKKSProcessedStateRemote zoneID:self.zoneID error:&error] deleteFromDatabase:&error];
1747 [[CKKSCurrentItemPointer fromDatabase:[recordID recordName] state:SecCKKSProcessedStateLocal zoneID:self.zoneID error:&error] deleteFromDatabase:&error];
1749 ckksinfo("ckks", self, "CKKSCurrentItemPointer was deleted: %@ error: %@", recordID, error);
1750 return (error == nil);
1752 } else if([recordType isEqual: SecCKRecordIntermediateKeyType]) {
1753 // TODO: handle in some interesting way
1755 } else if([recordType isEqual: SecCKRecordDeviceStateType]) {
1756 NSError* error = nil;
1757 ckksinfo("ckks", self, "CloudKit notification: deleted device state record(%@): %@", recordType, recordID);
1759 CKKSDeviceStateEntry* cdse = [CKKSDeviceStateEntry tryFromDatabaseFromCKRecordID:recordID error:&error];
1760 [cdse deleteFromDatabase: &error];
1761 ckksinfo("ckks", self, "CKKSCurrentItemPointer(%@) was deleted: %@ error: %@", cdse, recordID, error);
1763 return (error == nil);
1765 } else if ([recordType isEqualToString:SecCKRecordManifestType]) {
1766 ckksinfo("ckks", self, "CloudKit notification: deleted manifest record (%@): %@", recordType, recordID);
1768 NSError* error = nil;
1769 CKKSManifest* manifest = [CKKSManifest manifestForRecordName:recordID.recordName error:&error];
1771 [manifest deleteFromDatabase:&error];
1774 ckksinfo("ckks", self, "CKKSManifest was deleted: %@ %@ error: %@", recordID, manifest, error);
1775 // TODO: actually pass error back up
1776 return error == nil;
1779 ckkserror("ckksfetch", self, "unknown record type: %@ %@", recordType, recordID);
1784 - (bool)_onqueueCKRecordChanged:(CKRecord*)record resync:(bool)resync {
1785 dispatch_assert_queue(self.queue);
1787 ckksinfo("ckksfetch", self, "Processing record modification(%@): %@", record.recordType, record);
1789 if([[record recordType] isEqual: SecCKRecordItemType]) {
1790 [self _onqueueCKRecordItemChanged:record resync:resync];
1792 } else if([[record recordType] isEqual: SecCKRecordCurrentItemType]) {
1793 [self _onqueueCKRecordCurrentItemPointerChanged:record resync:resync];
1795 } else if([[record recordType] isEqual: SecCKRecordIntermediateKeyType]) {
1796 [self _onqueueCKRecordKeyChanged:record resync:resync];
1798 } else if([[record recordType] isEqualToString: SecCKRecordCurrentKeyType]) {
1799 [self _onqueueCKRecordCurrentKeyPointerChanged:record resync:resync];
1801 } else if ([[record recordType] isEqualToString:SecCKRecordManifestType]) {
1802 [self _onqueueCKRecordManifestChanged:record resync:resync];
1804 } else if ([[record recordType] isEqualToString:SecCKRecordManifestLeafType]) {
1805 [self _onqueueCKRecordManifestLeafChanged:record resync:resync];
1807 } else if ([[record recordType] isEqualToString:SecCKRecordDeviceStateType]) {
1808 [self _onqueueCKRecordDeviceStateChanged:record resync:resync];
1811 ckkserror("ckksfetch", self, "unknown record type: %@ %@", [record recordType], record);
1816 - (void)_onqueueCKRecordItemChanged:(CKRecord*)record resync:(bool)resync {
1817 dispatch_assert_queue(self.queue);
1819 NSError* error = nil;
1820 // Find if we knew about this record in the past
1821 bool update = false;
1822 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase: [[record recordID] recordName] zoneID:self.zoneID error:&error];
1825 ckkserror("ckks", self, "error loading a CKKSMirrorEntry from database: %@", error);
1831 ckkserror("ckksresync", self, "BUG: No local item matching resynced CloudKit record: %@", record);
1832 } else if(![ckme matchesCKRecord:record]) {
1833 ckkserror("ckksresync", self, "BUG: Local item doesn't match resynced CloudKit record: %@ %@", ckme, record);
1835 ckksnotice("ckksresync", self, "Already know about this item record, skipping update: %@", record);
1840 if(ckme && ckme.item && ckme.item.generationCount > [record[SecCKRecordGenerationCountKey] unsignedLongLongValue]) {
1841 ckkserror("ckks", self, "received a record from CloudKit with a bad generation count: %@ (%ld > %@)", ckme.uuid,
1842 (long) ckme.item.generationCount,
1843 record[SecCKRecordGenerationCountKey]);
1844 // Abort processing this record.
1848 // If we found an old version in the database; this might be an update
1850 if([ckme matchesCKRecord:record]) {
1851 // This is almost certainly a record we uploaded; CKFetchChanges sends them back as new records
1852 ckksnotice("ckks", self, "CloudKit has told us of record we already know about; skipping update");
1857 // Set the CKKSMirrorEntry's fields to be whatever this record holds
1858 [ckme setFromCKRecord: record];
1860 // Have to make a new CKKSMirrorEntry
1861 ckme = [[CKKSMirrorEntry alloc] initWithCKRecord: record];
1864 [ckme saveToDatabase: &error];
1867 ckkserror("ckks", self, "couldn't save new CKRecord to database: %@ %@", record, error);
1869 ckksdebug("ckks", self, "CKKSMirrorEntry was created: %@", ckme);
1872 NSError* iqeerror = nil;
1873 CKKSIncomingQueueEntry* iqe = [[CKKSIncomingQueueEntry alloc] initWithCKKSItem:ckme.item
1874 action:(update ? SecCKKSActionModify : SecCKKSActionAdd)
1875 state:SecCKKSStateNew];
1876 [iqe saveToDatabase:&iqeerror];
1878 ckkserror("ckks", self, "Couldn't save modified incoming queue entry: %@", iqeerror);
1880 ckksdebug("ckks", self, "CKKSIncomingQueueEntry was created: %@", iqe);
1883 // A remote change has occured for this record. Delete any pending local changes; they will be overwritten.
1884 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry tryFromDatabase:ckme.uuid state: SecCKKSStateNew zoneID:self.zoneID error: &error];
1886 ckkserror("ckks", self, "Couldn't load OutgoingQueueEntry: %@", error);
1889 [self _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateDeleted error:&error];
1892 // Reencryptions are pending changes too
1893 oqe = [CKKSOutgoingQueueEntry tryFromDatabase:ckme.uuid state: SecCKKSStateReencrypt zoneID:self.zoneID error: &error];
1895 ckkserror("ckks", self, "Couldn't load reencrypted OutgoingQueueEntry: %@", error);
1898 [oqe deleteFromDatabase:&error];
1900 ckkserror("ckks", self, "Couldn't delete reencrypted oqe(%@): %@", oqe, error);
1905 - (void)_onqueueCKRecordKeyChanged:(CKRecord*)record resync:(bool)resync {
1906 dispatch_assert_queue(self.queue);
1908 NSError* error = nil;
1911 NSError* resyncerror = nil;
1913 CKKSKey* key = [CKKSKey tryFromDatabaseAnyState:record.recordID.recordName zoneID:self.zoneID error:&resyncerror];
1915 ckkserror("ckksresync", self, "error loading key: %@", resyncerror);
1918 ckkserror("ckksresync", self, "BUG: No sync key matching resynced CloudKit record: %@", record);
1919 } else if(![key matchesCKRecord:record]) {
1920 ckkserror("ckksresync", self, "BUG: Local sync key doesn't match resynced CloudKit record(s): %@ %@", key, record);
1922 ckksnotice("ckksresync", self, "Already know about this sync key, skipping update: %@", record);
1927 // For now, drop into the synckeys table as a 'remote' key, then ask for a rekey operation.
1928 CKKSKey* remotekey = [[CKKSKey alloc] initWithCKRecord: record];
1930 // We received this from an update. Don't use, yet.
1931 remotekey.state = SecCKKSProcessedStateRemote;
1932 remotekey.currentkey = false;
1934 [remotekey saveToDatabase:&error];
1936 ckkserror("ckkskey", self, "Couldn't save key record to database: %@: %@", remotekey, error);
1937 ckksinfo("ckkskey", self, "CKRecord was %@", record);
1940 // We've saved a new key in the database; trigger a rekey operation.
1941 [self _onqueueKeyStateMachineRequestProcess];
1944 - (void)_onqueueCKRecordCurrentKeyPointerChanged:(CKRecord*)record resync:(bool)resync {
1945 dispatch_assert_queue(self.queue);
1948 NSError* ckperror = nil;
1949 CKKSCurrentKeyPointer* ckp = [CKKSCurrentKeyPointer tryFromDatabase:((CKKSKeyClass*) record.recordID.recordName) zoneID:self.zoneID error:&ckperror];
1951 ckkserror("ckksresync", self, "error loading ckp: %@", ckperror);
1954 ckkserror("ckksresync", self, "BUG: No current key pointer matching resynced CloudKit record: %@", record);
1955 } else if(![ckp matchesCKRecord:record]) {
1956 ckkserror("ckksresync", self, "BUG: Local current key pointer doesn't match resynced CloudKit record: %@ %@", ckp, record);
1958 ckksnotice("ckksresync", self, "Already know about this current key pointer, skipping update: %@", record);
1963 NSError* error = nil;
1964 CKKSCurrentKeyPointer* currentkey = [[CKKSCurrentKeyPointer alloc] initWithCKRecord: record];
1966 [currentkey saveToDatabase: &error];
1968 ckkserror("ckkskey", self, "Couldn't save current key pointer to database: %@: %@", currentkey, error);
1969 ckksinfo("ckkskey", self, "CKRecord was %@", record);
1972 // We've saved a new key in the database; trigger a rekey operation.
1973 [self _onqueueKeyStateMachineRequestProcess];
1976 - (void)_onqueueCKRecordCurrentItemPointerChanged:(CKRecord*)record resync:(bool)resync {
1977 dispatch_assert_queue(self.queue);
1980 NSError* ciperror = nil;
1981 CKKSCurrentItemPointer* localcip = [CKKSCurrentItemPointer tryFromDatabase:record.recordID.recordName state:SecCKKSProcessedStateLocal zoneID:self.zoneID error:&ciperror];
1982 CKKSCurrentItemPointer* remotecip = [CKKSCurrentItemPointer tryFromDatabase:record.recordID.recordName state:SecCKKSProcessedStateRemote zoneID:self.zoneID error:&ciperror];
1984 ckkserror("ckksresync", self, "error loading cip: %@", ciperror);
1986 if(!(localcip || remotecip)) {
1987 ckkserror("ckksresync", self, "BUG: No current item pointer matching resynced CloudKit record: %@", record);
1988 } else if(! ([localcip matchesCKRecord:record] || [remotecip matchesCKRecord:record]) ) {
1989 ckkserror("ckksresync", self, "BUG: Local current item pointer doesn't match resynced CloudKit record(s): %@ %@ %@", localcip, remotecip, record);
1991 ckksnotice("ckksresync", self, "Already know about this current item pointer, skipping update: %@", record);
1996 NSError* error = nil;
1997 CKKSCurrentItemPointer* cip = [[CKKSCurrentItemPointer alloc] initWithCKRecord: record];
1998 cip.state = SecCKKSProcessedStateRemote;
2000 [cip saveToDatabase: &error];
2002 ckkserror("currentitem", self, "Couldn't save current item pointer to database: %@: %@ %@", cip, error, record);
2006 - (void)_onqueueCKRecordManifestChanged:(CKRecord*)record resync:(bool)resync
2008 NSError* error = nil;
2009 CKKSPendingManifest* manifest = [[CKKSPendingManifest alloc] initWithCKRecord:record];
2010 [manifest saveToDatabase:&error];
2012 ckkserror("CKKS", self, "Failed to save fetched manifest record to database: %@: %@", manifest, error);
2013 ckksinfo("CKKS", self, "manifest CKRecord was %@", record);
2017 - (void)_onqueueCKRecordManifestLeafChanged:(CKRecord*)record resync:(bool)resync
2019 NSError* error = nil;
2020 CKKSManifestLeafRecord* manifestLeaf = [[CKKSManifestPendingLeafRecord alloc] initWithCKRecord:record];
2021 [manifestLeaf saveToDatabase:&error];
2023 ckkserror("CKKS", self, "Failed to save fetched manifest leaf record to database: %@: %@", manifestLeaf, error);
2024 ckksinfo("CKKS", self, "manifest leaf CKRecord was %@", record);
2028 - (void)_onqueueCKRecordDeviceStateChanged:(CKRecord*)record resync:(bool)resync {
2030 NSError* dserror = nil;
2031 CKKSDeviceStateEntry* cdse = [CKKSDeviceStateEntry tryFromDatabase:record.recordID.recordName zoneID:self.zoneID error:&dserror];
2033 ckkserror("ckksresync", self, "error loading cdse: %@", dserror);
2036 ckkserror("ckksresync", self, "BUG: No current device state entry matching resynced CloudKit record: %@", record);
2037 } else if(![cdse matchesCKRecord:record]) {
2038 ckkserror("ckksresync", self, "BUG: Local current device state entry doesn't match resynced CloudKit record(s): %@ %@", cdse, record);
2040 ckksnotice("ckksresync", self, "Already know about this current item pointer, skipping update: %@", record);
2045 NSError* error = nil;
2046 CKKSDeviceStateEntry* cdse = [[CKKSDeviceStateEntry alloc] initWithCKRecord:record];
2047 [cdse saveToDatabase:&error];
2049 ckkserror("ckksdevice", self, "Failed to save device record to database: %@: %@ %@", cdse, error, record);
2053 - (bool)_onqueueChangeOutgoingQueueEntry: (CKKSOutgoingQueueEntry*) oqe toState: (NSString*) state error: (NSError* __autoreleasing*) error {
2054 dispatch_assert_queue(self.queue);
2056 NSError* localerror = nil;
2058 if([state isEqualToString: SecCKKSStateDeleted]) {
2059 // Hurray, this must be a success
2060 SecBoolNSErrorCallback callback = self.pendingSyncCallbacks[oqe.uuid];
2062 callback(true, nil);
2065 [oqe deleteFromDatabase: &localerror];
2067 ckkserror("ckks", self, "Couldn't delete %@: %@", oqe, localerror);
2072 [oqe saveToDatabase: &localerror];
2074 ckkserror("ckks", self, "Couldn't save %@ as %@: %@", oqe, state, localerror);
2078 if(error && localerror) {
2079 *error = localerror;
2081 return localerror == nil;
2084 - (bool)_onqueueErrorOutgoingQueueEntry: (CKKSOutgoingQueueEntry*) oqe itemError: (NSError*) itemError error: (NSError* __autoreleasing*) error {
2085 dispatch_assert_queue(self.queue);
2087 SecBoolNSErrorCallback callback = self.pendingSyncCallbacks[oqe.uuid];
2089 callback(false, itemError);
2091 NSError* localerror = nil;
2093 oqe.state = SecCKKSStateError;
2094 [oqe saveToDatabase: &localerror];
2096 ckkserror("ckks", self, "Couldn't set %@ as error: %@", oqe, localerror);
2099 if(error && localerror) {
2100 *error = localerror;
2102 return localerror == nil;
2105 - (bool)_onQueueUpdateLatestManifestWithError:(NSError**)error
2107 dispatch_assert_queue(self.queue);
2108 CKKSManifest* manifest = [CKKSManifest latestTrustedManifestForZone:self.zoneName error:error];
2110 self.latestManifest = manifest;
2118 - (bool)checkTLK: (CKKSKey*) proposedTLK error: (NSError * __autoreleasing *) error {
2119 // Until we have Octagon Trust, accept this TLK iff we have its actual AES key in the keychain
2121 if([proposedTLK loadKeyMaterialFromKeychain:error]) {
2129 - (void) dispatchAsync: (bool (^)(void)) block {
2130 // We need to call kc_with_dbt, which blocks. Route up through a global queue...
2131 __weak __typeof(self) weakSelf = self;
2133 dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
2134 [weakSelf dispatchSync:block];
2138 // Use this if you have a potential database connection already
2139 - (void) dispatchSyncWithConnection: (SecDbConnectionRef) dbconn block: (bool (^)(void)) block {
2141 dispatch_sync(self.queue, ^{
2142 CFErrorRef cferror = NULL;
2143 kc_transaction_type(dbconn, kSecDbExclusiveRemoteCKKSTransactionType, &cferror, block);
2146 ckkserror("ckks", self, "error doing database transaction (sync), major problems ahead: %@", cferror);
2150 [self dispatchSync: block];
2154 - (void) dispatchSync: (bool (^)(void)) block {
2155 // important enough to block this thread. Must get a connection first, though!
2156 __weak __typeof(self) weakSelf = self;
2158 CFErrorRef cferror = NULL;
2159 kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
2160 __strong __typeof(weakSelf) strongSelf = weakSelf;
2162 ckkserror("ckks", strongSelf, "received callback for released object");
2166 __block bool ok = false;
2167 __block CFErrorRef cferror = NULL;
2169 dispatch_sync(strongSelf.queue, ^{
2170 ok = kc_transaction_type(dbt, kSecDbExclusiveRemoteCKKSTransactionType, &cferror, block);
2175 ckkserror("ckks", self, "error getting database connection (sync), major problems ahead: %@", cferror);
2179 - (void)dispatchSyncWithAccountQueue:(bool (^)(void))block
2181 [SOSAccount performOnAccountQueue:^{
2182 [CKKSManifest performWithAccountInfo:^{
2183 [self dispatchSync:^bool{
2184 __block bool result = false;
2185 [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
2194 #pragma mark - CKKSZoneUpdateReceiver
2196 - (void)notifyZoneChange: (CKRecordZoneNotification*) notification {
2197 ckksinfo("ckks", self, "hurray, got a zone change for %@ %@", self, notification);
2199 [self fetchAndProcessCKChanges:CKKSFetchBecauseAPNS];
2202 // Must be on the queue when this is called
2203 - (void)handleCKLogin {
2204 dispatch_assert_queue(self.queue);
2206 if(!self.setupStarted) {
2207 [self _onqueueInitializeZone];
2209 ckksinfo("ckks", self, "ignoring login as setup has already started");
2213 - (void)handleCKLogout {
2214 NSBlockOperation* logout = [NSBlockOperation blockOperationWithBlock: ^{
2215 [self dispatchSync:^bool {
2216 ckksnotice("ckks", self, "received a notification of CK logout for %@", self.zoneName);
2217 NSError* error = nil;
2219 [self _onqueueResetLocalData: &error];
2222 ckkserror("ckks", self, "error while resetting local data: %@", error);
2228 logout.name = @"cloudkit-logout";
2229 [self scheduleAccountStatusOperation: logout];
2232 #pragma mark - CKKSChangeFetcherErrorOracle
2234 - (bool) isFatalCKFetchError: (NSError*) error {
2235 __weak __typeof(self) weakSelf = self;
2237 // Again, note that this handles exactly one zone. Mutli-zone errors are not supported.
2238 bool isChangeTokenExpiredError = false;
2239 if([error.domain isEqualToString:CKErrorDomain] && (error.code == CKErrorChangeTokenExpired)) {
2240 isChangeTokenExpiredError = true;
2241 } else if([error.domain isEqualToString:CKErrorDomain] && (error.code == CKErrorPartialFailure)) {
2242 NSDictionary* partialErrors = error.userInfo[CKPartialErrorsByItemIDKey];
2243 for(NSError* partialError in partialErrors.allValues) {
2244 if([partialError.domain isEqualToString:CKErrorDomain] && (partialError.code == CKErrorChangeTokenExpired)) {
2245 isChangeTokenExpiredError = true;
2250 if(isChangeTokenExpiredError) {
2251 ckkserror("ckks", self, "Received notice that our change token is out of date. Resetting local data...");
2252 [self cancelAllOperations];
2253 CKKSResultOperation* resetOp = [self resetLocalData];
2254 CKKSResultOperation* resetHandler = [CKKSResultOperation named:@"local-reset-handler" withBlock:^{
2255 __strong __typeof(self) strongSelf = weakSelf;
2257 ckkserror("ckks", strongSelf, "received callback for released object");
2262 ckksnotice("ckks", strongSelf, "CloudKit-inspired local reset of %@ ended with error: %@", strongSelf.zoneID, error);
2264 ckksnotice("ckksreset", strongSelf, "re-initializing zone %@", strongSelf.zoneID);
2265 [strongSelf initializeZone];
2269 [resetHandler addDependency:resetOp];
2270 [self scheduleOperation:resetHandler];
2274 bool isDeletedZoneError = false;
2275 if([error.domain isEqualToString:CKErrorDomain] && ((error.code == CKErrorUserDeletedZone) || (error.code == CKErrorZoneNotFound))) {
2276 isDeletedZoneError = true;
2277 } else if([error.domain isEqualToString:CKErrorDomain] && (error.code == CKErrorPartialFailure)) {
2278 NSDictionary* partialErrors = error.userInfo[CKPartialErrorsByItemIDKey];
2279 for(NSError* partialError in partialErrors.allValues) {
2280 if([partialError.domain isEqualToString:CKErrorDomain] && ((partialError.code == CKErrorUserDeletedZone) || (partialError.code == CKErrorZoneNotFound))) {
2281 isDeletedZoneError = true;
2286 if(isDeletedZoneError) {
2287 ckkserror("ckks", self, "Received notice that our zone does not exist. Resetting all data.");
2288 [self cancelAllOperations];
2289 CKKSResultOperation* resetOp = [self resetCloudKitZone];
2290 CKKSResultOperation* resetHandler = [CKKSResultOperation named:@"reset-handler" withBlock:^{
2291 __strong __typeof(self) strongSelf = weakSelf;
2293 ckkserror("ckks", strongSelf, "received callback for released object");
2298 ckksnotice("ckks", strongSelf, "CloudKit-inspired zone reset of %@ ended with error: %@", strongSelf.zoneID, resetOp.error);
2300 ckksnotice("ckksreset", strongSelf, "re-initializing zone %@", strongSelf.zoneID);
2301 [strongSelf initializeZone];
2305 [resetHandler addDependency:resetOp];
2306 [self scheduleOperation:resetHandler];
2310 if([error.domain isEqualToString:CKErrorDomain] && (error.code == CKErrorBadContainer)) {
2311 ckkserror("ckks", self, "Received notice that our container does not exist. Nothing to do.");
2318 #pragma mark - Test Support
2320 - (bool) outgoingQueueEmpty: (NSError * __autoreleasing *) error {
2321 __block bool ret = false;
2322 [self dispatchSync: ^bool{
2323 NSArray* queueEntries = [CKKSOutgoingQueueEntry all: error];
2324 ret = queueEntries && ([queueEntries count] == 0);
2331 - (CKKSResultOperation*)waitForFetchAndIncomingQueueProcessing {
2332 if(!SecCKKSIsEnabled()) {
2333 ckksinfo("ckks", self, "Due to disabled CKKS, returning fast from waitForFetchAndIncomingQueueProcessing");
2337 CKKSResultOperation* op = [self fetchAndProcessCKChanges:CKKSFetchBecauseTesting];
2338 [op waitUntilFinished];
2342 - (void)waitForKeyHierarchyReadiness {
2343 if(self.keyStateReadyDependency) {
2344 [self.keyStateReadyDependency waitUntilFinished];
2348 - (void)cancelAllOperations {
2349 [self.zoneSetupOperation cancel];
2350 [self.keyStateMachineOperation cancel];
2351 [self.keyStateReadyDependency cancel];
2352 [self.zoneChangeFetcher cancel];
2354 [super cancelAllOperations];
2356 [self dispatchSync:^bool{
2357 [self _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateCancelled withError: nil];
2362 - (NSDictionary*)status {
2363 #define stringify(obj) CKKSNilToNSNull([obj description])
2364 #define boolstr(obj) (!!(obj) ? @"yes" : @"no")
2365 __block NSDictionary* ret = nil;
2366 __block NSError* error = nil;
2367 CKKSManifest* manifest = [CKKSManifest latestTrustedManifestForZone:self.zoneName error:&error];
2368 [self dispatchSync: ^bool {
2370 NSString* uuidTLK = [CKKSKey currentKeyForClass:SecCKKSKeyClassTLK zoneID:self.zoneID error:&error].uuid;
2371 NSString* uuidClassA = [CKKSKey currentKeyForClass:SecCKKSKeyClassA zoneID:self.zoneID error:&error].uuid;
2372 NSString* uuidClassC = [CKKSKey currentKeyForClass:SecCKKSKeyClassC zoneID:self.zoneID error:&error].uuid;
2374 NSString* manifestGeneration = manifest ? [NSString stringWithFormat:@"%lu", (unsigned long)manifest.generationCount] : nil;
2377 ckkserror("ckks", self, "error during status: %@", error);
2379 // We actually don't care about this error, especially if it's "no current key pointers"...
2382 // Map deviceStates to strings to avoid NSXPC issues. Obj-c, why is this so hard?
2383 NSArray* deviceStates = [CKKSDeviceStateEntry allInZone:self.zoneID error:&error];
2384 NSMutableArray<NSString*>* mutDeviceStates = [[NSMutableArray alloc] init];
2385 [deviceStates enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
2386 [mutDeviceStates addObject: [obj description]];
2390 @"view": CKKSNilToNSNull(self.zoneName),
2391 @"ckaccountstatus": self.accountStatus == CKAccountStatusCouldNotDetermine ? @"could not determine" :
2392 self.accountStatus == CKAccountStatusAvailable ? @"logged in" :
2393 self.accountStatus == CKAccountStatusRestricted ? @"restricted" :
2394 self.accountStatus == CKAccountStatusNoAccount ? @"logged out" : @"unknown",
2395 @"lockstatetracker": stringify(self.lockStateTracker),
2396 @"accounttracker": stringify(self.accountTracker),
2397 @"fetcher": stringify(self.zoneChangeFetcher),
2398 @"setup": boolstr(self.setupComplete),
2399 @"zoneCreated": boolstr(self.zoneCreated),
2400 @"zoneCreatedError": stringify(self.zoneCreatedError),
2401 @"zoneSubscribed": boolstr(self.zoneSubscribed),
2402 @"zoneSubscribedError": stringify(self.zoneSubscribedError),
2403 @"zoneInitializeScheduler": stringify(self.initializeScheduler),
2404 @"keystate": CKKSNilToNSNull(self.keyHierarchyState),
2405 @"keyStateError": stringify(self.keyHierarchyError),
2406 @"statusError": stringify(error),
2407 @"oqe": CKKSNilToNSNull([CKKSOutgoingQueueEntry countsByState:self.zoneID error:&error]),
2408 @"iqe": CKKSNilToNSNull([CKKSIncomingQueueEntry countsByState:self.zoneID error:&error]),
2409 @"ckmirror": CKKSNilToNSNull([CKKSMirrorEntry countsByParentKey:self.zoneID error:&error]),
2410 @"devicestates": CKKSNilToNSNull(mutDeviceStates),
2411 @"keys": CKKSNilToNSNull([CKKSKey countsByClass:self.zoneID error:&error]),
2412 @"currentTLK": CKKSNilToNSNull(uuidTLK),
2413 @"currentClassA": CKKSNilToNSNull(uuidClassA),
2414 @"currentClassC": CKKSNilToNSNull(uuidClassC),
2415 @"currentManifestGen": CKKSNilToNSNull(manifestGeneration),
2417 @"zoneSetupOperation": stringify(self.zoneSetupOperation),
2418 @"viewSetupOperation": stringify(self.viewSetupOperation),
2419 @"keyStateOperation": stringify(self.keyStateMachineOperation),
2420 @"lastIncomingQueueOperation": stringify(self.lastIncomingQueueOperation),
2421 @"lastNewTLKOperation": stringify(self.lastNewTLKOperation),
2422 @"lastOutgoingQueueOperation": stringify(self.lastOutgoingQueueOperation),
2423 @"lastRecordZoneChangesOperation": stringify(self.lastRecordZoneChangesOperation),
2424 @"lastProcessReceivedKeysOperation": stringify(self.lastProcessReceivedKeysOperation),
2425 @"lastReencryptOutgoingItemsOperation":stringify(self.lastReencryptOutgoingItemsOperation),
2426 @"lastScanLocalItemsOperation": stringify(self.lastScanLocalItemsOperation),
2435 #endif /* OCTAGON */