CKKSKeychainView.m   [plain text]

/*
* Copyright (c) 2016 Apple Inc. All Rights Reserved.
*
* @APPLE_LICENSE_HEADER_START@
*
* This file contains Original Code and/or Modifications of Original Code
* as defined in and that are subject to the Apple Public Source License
* Version 2.0 (the 'License'). You may not use this file except in
* compliance with the License. Please obtain a copy of the License at
* http://www.opensource.apple.com/apsl/ and read it before using this
* file.
*
* The Original Code and all software distributed under the License are
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
* Please see the License for the specific language governing rights and
* limitations under the License.
*
* @APPLE_LICENSE_HEADER_END@
*/

#import "CKKSKeychainView.h"

#if OCTAGON
#import "CloudKitDependencies.h"
#import <CloudKit/CloudKit.h>
#import <CloudKit/CloudKit_Private.h>
#endif

#import "CKKS.h"
#import "CKKSAPSReceiver.h"
#import "CKKSIncomingQueueEntry.h"
#import "CKKSOutgoingQueueEntry.h"
#import "CKKSCurrentKeyPointer.h"
#import "CKKSKey.h"
#import "CKKSMirrorEntry.h"
#import "CKKSZoneStateEntry.h"
#import "CKKSItemEncrypter.h"
#import "CKKSIncomingQueueOperation.h"
#import "CKKSNewTLKOperation.h"
#import "CKKSProcessReceivedKeysOperation.h"
#import "CKKSZone.h"
#import "CKKSFetchAllRecordZoneChangesOperation.h"
#import "CKKSHealKeyHierarchyOperation.h"
#import "CKKSReencryptOutgoingItemsOperation.h"
#import "CKKSScanLocalItemsOperation.h"
#import "CKKSSynchronizeOperation.h"
#import "CKKSRateLimiter.h"
#import "CKKSManifest.h"
#import "CKKSManifestLeafRecord.h"
#import "CKKSZoneChangeFetcher.h"
#import "CKKSAnalytics.h"
#import "keychain/ckks/CKKSDeviceStateEntry.h"
#import "keychain/ckks/CKKSNearFutureScheduler.h"
#import "keychain/ckks/CKKSCurrentItemPointer.h"
#import "keychain/ckks/CKKSUpdateCurrentItemPointerOperation.h"
#import "keychain/ckks/CKKSUpdateDeviceStateOperation.h"
#import "keychain/ckks/CKKSNotifier.h"
#import "keychain/ckks/CloudKitCategories.h"
#import "keychain/ckks/CKKSTLKShare.h"
#import "keychain/ckks/CKKSHealTLKSharesOperation.h"
#import "keychain/ckks/CKKSLocalSynchronizeOperation.h"
#import "keychain/categories/NSError+UsefulConstructors.h"

#include <utilities/SecCFWrappers.h>
#include <utilities/SecDb.h>
#include <securityd/SecDbItem.h>
#include <securityd/SecItemDb.h>
#include <securityd/SecItemSchema.h>
#include <securityd/SecItemServer.h>
#include <utilities/debugging.h>
#include <Security/SecItemPriv.h>
#include <Security/SecureObjectSync/SOSAccountTransaction.h>
#include <utilities/SecADWrapper.h>
#include <utilities/SecPLWrappers.h>
#include <os/transaction_private.h>

#if OCTAGON
@interface CKKSKeychainView()
@property bool keyStateFetchRequested;
@property bool keyStateFullRefetchRequested;
@property bool keyStateProcessRequested;
@property bool trustedPeersSetChanged;

@property bool keyStateCloudKitDeleteRequested;
@property NSHashTable<CKKSResultOperation*>* cloudkitDeleteZoneOperations;

@property bool keyStateLocalResetRequested;
@property NSHashTable<CKKSResultOperation*>* localResetOperations;

@property (atomic) NSString *activeTLK;

@property (readonly) Class<CKKSNotifier> notifierClass;

@property CKKSNearFutureScheduler* initializeScheduler;

// Slows down all outgoing queue operations
@property CKKSNearFutureScheduler* outgoingQueueOperationScheduler;

@property CKKSResultOperation* processIncomingQueueAfterNextUnlockOperation;
@property CKKSResultOperation* resultsOfNextIncomingQueueOperationOperation;

@property NSMutableDictionary<NSString*, SecBoolNSErrorCallback>* pendingSyncCallbacks;

@property id<CKKSPeerProvider> currentPeerProvider;

// An extra queue for semaphore-waiting-based NSOperations
@property NSOperationQueue* waitingQueue;

// Make these readwrite
@property (nonatomic, readwrite) CKKSSelves* currentSelfPeers;
@property (nonatomic, readwrite) NSError* currentSelfPeersError;
@property (nonatomic, readwrite) NSSet<id<CKKSPeer>>* currentTrustedPeers;
@property (nonatomic, readwrite) NSError* currentTrustedPeersError;
@end
#endif

@implementation CKKSKeychainView
#if OCTAGON

- (instancetype)initWithContainer: (CKContainer*) container
zoneName: (NSString*) zoneName
accountTracker:(CKKSCKAccountStateTracker*) accountTracker
lockStateTracker:(CKKSLockStateTracker*) lockStateTracker
reachabilityTracker:(CKKSReachabilityTracker *)reachabilityTracker
changeFetcher:(CKKSZoneChangeFetcher*)fetcher
savedTLKNotifier:(CKKSNearFutureScheduler*) savedTLKNotifier
peerProvider:(id<CKKSPeerProvider>)peerProvider
fetchRecordZoneChangesOperationClass: (Class<CKKSFetchRecordZoneChangesOperation>) fetchRecordZoneChangesOperationClass
fetchRecordsOperationClass: (Class<CKKSFetchRecordsOperation>)fetchRecordsOperationClass
queryOperationClass:(Class<CKKSQueryOperation>)queryOperationClass
modifySubscriptionsOperationClass: (Class<CKKSModifySubscriptionsOperation>) modifySubscriptionsOperationClass
modifyRecordZonesOperationClass: (Class<CKKSModifyRecordZonesOperation>) modifyRecordZonesOperationClass
apsConnectionClass: (Class<CKKSAPSConnection>) apsConnectionClass
notifierClass: (Class<CKKSNotifier>) notifierClass
{

if(self = [super initWithContainer:container
zoneName:zoneName
accountTracker:accountTracker
reachabilityTracker:reachabilityTracker
fetchRecordZoneChangesOperationClass:fetchRecordZoneChangesOperationClass
fetchRecordsOperationClass:fetchRecordsOperationClass
queryOperationClass:queryOperationClass
modifySubscriptionsOperationClass:modifySubscriptionsOperationClass
modifyRecordZonesOperationClass:modifyRecordZonesOperationClass
apsConnectionClass:apsConnectionClass]) {
__weak __typeof(self) weakSelf = self;

_loggedIn = [[CKKSCondition alloc] init];
_loggedOut = [[CKKSCondition alloc] init];
_accountStateKnown = [[CKKSCondition alloc] init];

_incomingQueueOperations = [NSHashTable weakObjectsHashTable];
_outgoingQueueOperations = [NSHashTable weakObjectsHashTable];
_cloudkitDeleteZoneOperations = [NSHashTable weakObjectsHashTable];
_localResetOperations = [NSHashTable weakObjectsHashTable];

_zoneChangeFetcher = fetcher;
[fetcher registerClient:self];

_notifierClass = notifierClass;
_notifyViewChangedScheduler = [[CKKSNearFutureScheduler alloc] initWithName:[NSString stringWithFormat: @"%@-notify-scheduler", self.zoneName]
initialDelay:250*NSEC_PER_MSEC
continuingDelay:1*NSEC_PER_SEC
keepProcessAlive:true
dependencyDescriptionCode:CKKSResultDescriptionPendingViewChangedScheduling
block:^{
__strong __typeof(self) strongSelf = weakSelf;
[strongSelf.notifierClass post:[NSString stringWithFormat:@"com.apple.security.view-change.%@", strongSelf.zoneName]];

// Ugly, but: the Manatee and Engram views need to send a fake 'PCS' view change.
// TODO: make this data-driven somehow
if([strongSelf.zoneName isEqualToString:@"Manatee"] ||
[strongSelf.zoneName isEqualToString:@"Engram"] ||
[strongSelf.zoneName isEqualToString:@"ApplePay"] ||
[strongSelf.zoneName isEqualToString:@"LimitedPeersAllowed"]) {
[strongSelf.notifierClass post:@"com.apple.security.view-change.PCS"];
}
}];

_pendingSyncCallbacks = [[NSMutableDictionary alloc] init];

_lockStateTracker = lockStateTracker;
_savedTLKNotifier = savedTLKNotifier;
_currentPeerProvider = peerProvider;
[_currentPeerProvider registerForPeerChangeUpdates:self];

_keyHierarchyConditions = [[NSMutableDictionary alloc] init];
[CKKSZoneKeyStateMap() enumerateKeysAndObjectsUsingBlock:^(CKKSZoneKeyState * _Nonnull key, NSNumber * _Nonnull obj, BOOL * _Nonnull stop) {
[self.keyHierarchyConditions setObject: [[CKKSCondition alloc] init] forKey:key];
}];

// Use the keyHierarchyState setter to modify the zone key state map
self.keyHierarchyState = SecCKKSZoneKeyStateLoggedOut;

_keyHierarchyError = nil;
_keyHierarchyOperationGroup = nil;
_keyStateMachineOperation = nil;
_keyStateFetchRequested = false;
_keyStateProcessRequested = false;

_waitingQueue = [[NSOperationQueue alloc] init];
_waitingQueue.maxConcurrentOperationCount = 5;

_keyStateReadyDependency = [self createKeyStateReadyDependency: @"Key state has become ready for the first time." ckoperationGroup:[CKOperationGroup CKKSGroupWithName:@"initial-key-state-ready-scan"]];

_keyStateNonTransientDependency = [self createKeyStateNontransientDependency];

dispatch_time_t initializeDelay = SecCKKSReduceRateLimiting() ? NSEC_PER_MSEC * 600 : NSEC_PER_SEC * 30;
_initializeScheduler = [[CKKSNearFutureScheduler alloc] initWithName:[NSString stringWithFormat: @"%@-zone-initializer", self.zoneName]
initialDelay:0
continuingDelay:initializeDelay
keepProcessAlive:false
dependencyDescriptionCode:CKKSResultDescriptionPendingZoneInitializeScheduling
block:^{}];

dispatch_time_t initialOutgoingQueueDelay = SecCKKSReduceRateLimiting() ? NSEC_PER_MSEC * 200 : NSEC_PER_SEC * 1;
dispatch_time_t continuingOutgoingQueueDelay = SecCKKSReduceRateLimiting() ? NSEC_PER_MSEC * 200 : NSEC_PER_SEC * 30;
_outgoingQueueOperationScheduler = [[CKKSNearFutureScheduler alloc] initWithName:[NSString stringWithFormat: @"%@-outgoing-queue-scheduler", self.zoneName]
initialDelay:initialOutgoingQueueDelay
continuingDelay:continuingOutgoingQueueDelay
keepProcessAlive:false
dependencyDescriptionCode:CKKSResultDescriptionPendingOutgoingQueueScheduling
block:^{}];

dispatch_time_t initialKeyHierachyPokeDelay = SecCKKSReduceRateLimiting() ? NSEC_PER_MSEC * 100 : NSEC_PER_MSEC * 500;
dispatch_time_t continuingKeyHierachyPokeDelay = SecCKKSReduceRateLimiting() ? NSEC_PER_MSEC * 200 : NSEC_PER_SEC * 5;
_pokeKeyStateMachineScheduler = [[CKKSNearFutureScheduler alloc] initWithName:[NSString stringWithFormat: @"%@-reprocess-scheduler", self.zoneName]
initialDelay:initialKeyHierachyPokeDelay
continuingDelay:continuingKeyHierachyPokeDelay
keepProcessAlive:true
dependencyDescriptionCode:CKKSResultDescriptionPendingKeyHierachyPokeScheduling
block:^{
__strong __typeof(self) strongSelf = weakSelf;
[strongSelf dispatchSyncWithAccountKeys: ^bool{
__strong __typeof(weakSelf) strongBlockSelf = weakSelf;

[strongBlockSelf _onqueueAdvanceKeyStateMachineToState:nil withError:nil];
return true;
}];
}];
}
return self;
}

- (NSString*)description {
return [NSString stringWithFormat:@"<%@: %@ (%@)>", NSStringFromClass([self class]), self.zoneName, self.keyHierarchyState];
}

- (NSString*)debugDescription {
return [NSString stringWithFormat:@"<%@: %@ (%@) %p>", NSStringFromClass([self class]), self.zoneName, self.keyHierarchyState, self];
}

- (CKKSZoneKeyState*)keyHierarchyState {
return _keyHierarchyState;
}

- (void)setKeyHierarchyState:(CKKSZoneKeyState *)keyHierarchyState {
if((keyHierarchyState == nil && _keyHierarchyState == nil) || ([keyHierarchyState isEqualToString:_keyHierarchyState])) {
// No change, do nothing.
} else {
// Fixup the condition variables as part of setting this state
if(_keyHierarchyState) {
self.keyHierarchyConditions[_keyHierarchyState] = [[CKKSCondition alloc] init];
}

_keyHierarchyState = keyHierarchyState;

if(keyHierarchyState) {
[self.keyHierarchyConditions[keyHierarchyState] fulfill];
}
}
}

- (NSString *)lastActiveTLKUUID
{
return self.activeTLK;
}

- (void)_onqueueResetSetup:(CKKSZoneKeyState*)newState resetMessage:(NSString*)resetMessage ckoperationGroup:(CKOperationGroup*)group {
[super resetSetup];

self.keyHierarchyState = newState;
self.keyHierarchyError = nil;

[self.keyStateMachineOperation cancel];
self.keyStateMachineOperation = nil;

self.keyStateFetchRequested = false;
self.keyStateProcessRequested = false;

self.keyHierarchyOperationGroup = group;

NSOperation* oldKSRD = self.keyStateReadyDependency;
self.keyStateReadyDependency = [self createKeyStateReadyDependency:resetMessage ckoperationGroup:self.keyHierarchyOperationGroup];
if(oldKSRD) {
[oldKSRD addDependency:self.keyStateReadyDependency];
[self.waitingQueue addOperation:oldKSRD];
}

NSOperation* oldKSNTD = self.keyStateNonTransientDependency;
self.keyStateNonTransientDependency = [self createKeyStateNontransientDependency];
if(oldKSNTD) {
[oldKSNTD addDependency:self.keyStateNonTransientDependency];
[self.waitingQueue addOperation:oldKSNTD];
}
}

- (CKKSResultOperation*)createPendingInitializationOperation {

__weak __typeof(self) weakSelf = self;
CKKSResultOperation* initializationOp = [CKKSGroupOperation named:@"view-initialization" withBlockTakingSelf:^(CKKSGroupOperation * _Nonnull strongOp) {
__strong __typeof(weakSelf) strongSelf = weakSelf;

__block CKKSResultOperation* zoneCreationOperation = nil;
[strongSelf dispatchSync:^bool {
CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state: self.zoneName];
zoneCreationOperation = [self handleCKLogin:ckse.ckzonecreated zoneSubscribed:ckse.ckzonesubscribed];
return true;
}];

CKKSResultOperation* viewInitializationOperation = [CKKSResultOperation named:@"view-initialization" withBlockTakingSelf:^(CKKSResultOperation * _Nonnull strongInternalOp) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
if(!strongSelf) {
ckkserror("ckks", strongSelf, "received callback for released object");
return;
}

[strongSelf dispatchSyncWithAccountKeys: ^bool {
ckksnotice("ckks", strongSelf, "Zone setup progress: %@ %d %@ %d %@",
[CKKSCKAccountStateTracker stringFromAccountStatus:strongSelf.accountStatus],
strongSelf.zoneCreated, strongSelf.zoneCreatedError, strongSelf.zoneSubscribed, strongSelf.zoneSubscribedError);

NSError* error = nil;
CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state: strongSelf.zoneName];
ckse.ckzonecreated = strongSelf.zoneCreated;
ckse.ckzonesubscribed = strongSelf.zoneSubscribed;

// Although, if the zone subscribed error says there's no zone, mark down that there's no zone
if(strongSelf.zoneSubscribedError &&
[strongSelf.zoneSubscribedError.domain isEqualToString:CKErrorDomain] && strongSelf.zoneSubscribedError.code == CKErrorPartialFailure) {
NSError* subscriptionError = strongSelf.zoneSubscribedError.userInfo[CKPartialErrorsByItemIDKey][strongSelf.zoneID];
if(subscriptionError && [subscriptionError.domain isEqualToString:CKErrorDomain] && subscriptionError.code == CKErrorZoneNotFound) {

ckkserror("ckks", strongSelf, "zone subscription error appears to say the zone doesn't exist, fixing status: %@", strongSelf.zoneSubscribedError);
ckse.ckzonecreated = false;
}
}

[ckse saveToDatabase: &error];
if(error) {
ckkserror("ckks", strongSelf, "couldn't save zone creation status for %@: %@", strongSelf.zoneName, error);
}

if(!strongSelf.zoneCreated || !strongSelf.zoneSubscribed) {
// Go into 'zonecreationfailed'
strongInternalOp.error = strongSelf.zoneCreatedError ? strongSelf.zoneCreatedError : strongSelf.zoneSubscribedError;
[strongSelf _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateZoneCreationFailed withError:strongInternalOp.error];

return true;
} else {
[strongSelf _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateInitialized withError:nil];
}

return true;
}];
}];

[viewInitializationOperation addDependency:zoneCreationOperation];
[strongOp runBeforeGroupFinished:viewInitializationOperation];
}];

return initializationOp;
}

- (void)_onqueuePerformKeyStateInitialized:(CKKSZoneStateEntry*)ckse {

// Check if we believe we've synced this zone before.
if(ckse.changeToken == nil) {
self.keyHierarchyOperationGroup = [CKOperationGroup CKKSGroupWithName:@"initial-setup"];

ckksnotice("ckks", self, "No existing change token; going to try to match local items with CloudKit ones.");

// Onboard this keychain: there's likely items in it that we haven't synced yet.
// But, there might be items in The Cloud that correspond to these items, with UUIDs that we don't know yet.
// First, fetch all remote items.
CKKSResultOperation* fetch = [self.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseInitialStart];
fetch.name = @"initial-fetch";

// Next, try to process them (replacing local entries)
CKKSIncomingQueueOperation* initialProcess = [self processIncomingQueue:true after:fetch];
initialProcess.name = @"initial-process-incoming-queue";

// If all that succeeds, iterate through all keychain items and find the ones which need to be uploaded
self.initialScanOperation = [self scanLocalItems:@"initial-scan-operation"
ckoperationGroup:self.keyHierarchyOperationGroup
after:initialProcess];

} else {
// Likely a restart of securityd!

// First off, are there any in-flight queue entries? If so, put them back into New.
// If they're truly in-flight, we'll "conflict" with ourselves, but that should be fine.
NSError* error = nil;
[self _onqueueResetAllInflightOQE:&error];
if(error) {
ckkserror("ckks", self, "Couldn't reset in-flight OQEs, bad behavior ahead: %@", error);
}

// Are there any fixups to run first?
self.lastFixupOperation = [CKKSFixups fixup:ckse.lastFixup for:self];
if(self.lastFixupOperation) {
ckksnotice("ckksfixup", self, "We have a fixup to perform: %@", self.lastFixupOperation);
[self scheduleOperation:self.lastFixupOperation];
}

self.keyHierarchyOperationGroup = [CKOperationGroup CKKSGroupWithName:@"restart-setup"];

if ([CKKSManifest shouldSyncManifests]) {
self.egoManifest = [CKKSEgoManifest tryCurrentEgoManifestForZone:self.zoneName];
}

// If it's been more than 24 hours since the last fetch, fetch and process everything.
// Otherwise, just kick off the local queue processing.

NSDate* now = [NSDate date];
NSDateComponents* offset = [[NSDateComponents alloc] init];
[offset setHour:-24];
NSDate* deadline = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:now options:0];

NSOperation* initialProcess = nil;
if(ckse.lastFetchTime == nil || [ckse.lastFetchTime compare: deadline] == NSOrderedAscending) {
initialProcess = [self fetchAndProcessCKChanges:CKKSFetchBecauseSecuritydRestart after:self.lastFixupOperation];

// Also, kick off a scan local items: it'll find any out-of-sync issues in the local keychain
self.initialScanOperation = [self scanLocalItems:@"24-hr-scan-operation"
ckoperationGroup:self.keyHierarchyOperationGroup
after:initialProcess];
} else {
initialProcess = [self processIncomingQueue:false after:self.lastFixupOperation];
}

if([CKKSManifest shouldSyncManifests]) {
if (!self.egoManifest && !self.initialScanOperation) {
ckksnotice("ckksmanifest", self, "No ego manifest on restart; rescanning");
self.initialScanOperation = [self scanLocalItems:@"initial-scan-operation"
ckoperationGroup:self.keyHierarchyOperationGroup
after:initialProcess];
}
}

// Process outgoing queue after re-start
[self processOutgoingQueueAfter:self.lastFixupOperation ckoperationGroup:self.keyHierarchyOperationGroup];
}
}

- (bool)_onqueueResetLocalData: (NSError * __autoreleasing *) error {
dispatch_assert_queue(self.queue);

NSError* localerror = nil;
bool setError = false; // Ugly, but this is the only way to return the first error given

CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state: self.zoneName];
ckse.ckzonecreated = false;
ckse.ckzonesubscribed = false; // I'm actually not sure about this: can you be subscribed to a non-existent zone?
ckse.changeToken = NULL;
[ckse saveToDatabase: &localerror];
if(localerror) {
ckkserror("ckks", self, "couldn't reset zone status for %@: %@", self.zoneName, localerror);
if(error && !setError) {
*error = localerror; setError = true;
}
}

[CKKSMirrorEntry deleteAll:self.zoneID error: &localerror];
if(localerror) {
ckkserror("ckks", self, "couldn't delete all CKKSMirrorEntry: %@", localerror);
if(error && !setError) {
*error = localerror; setError = true;
}
}

[CKKSOutgoingQueueEntry deleteAll:self.zoneID error: &localerror];
if(localerror) {
ckkserror("ckks", self, "couldn't delete all CKKSOutgoingQueueEntry: %@", localerror);
if(error && !setError) {
*error = localerror; setError = true;
}
}

[CKKSIncomingQueueEntry deleteAll:self.zoneID error: &localerror];
if(localerror) {
ckkserror("ckks", self, "couldn't delete all CKKSIncomingQueueEntry: %@", localerror);
if(error && !setError) {
*error = localerror; setError = true;
}
}

[CKKSKey deleteAll:self.zoneID error: &localerror];
if(localerror) {
ckkserror("ckks", self, "couldn't delete all CKKSKey: %@", localerror);
if(error && !setError) {
*error = localerror; setError = true;
}
}

[CKKSTLKShare deleteAll:self.zoneID error: &localerror];
if(localerror) {
ckkserror("ckks", self, "couldn't delete all CKKSTLKShare: %@", localerror);
if(error && !setError) {
*error = localerror; setError = true;
}
}

[CKKSCurrentKeyPointer deleteAll:self.zoneID error: &localerror];
if(localerror) {
ckkserror("ckks", self, "couldn't delete all CKKSCurrentKeyPointer: %@", localerror);
if(error && !setError) {
*error = localerror; setError = true;
}
}

[CKKSCurrentItemPointer deleteAll:self.zoneID error: &localerror];
if(localerror) {
ckkserror("ckks", self, "couldn't delete all CKKSCurrentItemPointer: %@", localerror);
if(error && !setError) {
*error = localerror; setError = true;
}
}

[CKKSDeviceStateEntry deleteAll:self.zoneID error:&localerror];
if(localerror) {
ckkserror("ckks", self, "couldn't delete all CKKSDeviceStateEntry: %@", localerror);
if(error && !setError) {
*error = localerror; setError = true;
}
}

return (localerror == nil && !setError);
}

- (CKKSResultOperation*)createPendingResetLocalDataOperation {
@synchronized(self.localResetOperations) {
CKKSResultOperation* pendingResetLocalOperation = (CKKSResultOperation*) [self findFirstPendingOperation:self.localResetOperations];
if(!pendingResetLocalOperation) {
__weak __typeof(self) weakSelf = self;
pendingResetLocalOperation = [CKKSResultOperation named:@"reset-local" withBlockTakingSelf:^(CKKSResultOperation * _Nonnull strongOp) {
__strong __typeof(self) strongSelf = weakSelf;
__block NSError* error = nil;

[strongSelf dispatchSync: ^bool{
[strongSelf _onqueueResetLocalData: &error];
return true;
}];

strongOp.error = error;
}];
[pendingResetLocalOperation linearDependencies:self.localResetOperations];
}
return pendingResetLocalOperation;
}
}

- (CKKSResultOperation*)resetLocalData {
// Not overly thread-safe, but a single read is okay
CKKSAccountStatus accountStatus = self.accountStatus;
ckksnotice("ckksreset", self, "Requesting local data reset");

// If we're currently signed in, the reset operation will be handled by the CKKS key state machine, and a reset should end up in 'ready'
if(accountStatus == CKKSAccountStatusAvailable) {
__weak __typeof(self) weakSelf = self;
CKKSGroupOperation* resetOperationGroup = [CKKSGroupOperation named:@"local-reset" withBlockTakingSelf:^(CKKSGroupOperation *strongOp) {
__strong __typeof(self) strongSelf = weakSelf;

__block CKKSResultOperation* resetOperation = nil;

[strongSelf dispatchSyncWithAccountKeys:^bool {
strongSelf.keyStateLocalResetRequested = true;
resetOperation = [strongSelf createPendingResetLocalDataOperation];
[strongSelf _onqueueAdvanceKeyStateMachineToState:nil withError:nil];
return true;
}];

[strongOp dependOnBeforeGroupFinished:resetOperation];
}];
[self scheduleOperationWithoutDependencies:resetOperationGroup];

CKKSGroupOperation* viewReset = [CKKSGroupOperation named:@"local-data-reset" withBlockTakingSelf:^(CKKSGroupOperation *strongOp) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
// Now that the local reset finished, wait for the key hierarchy state machine to churn
ckksnotice("ckksreset", strongSelf, "waiting for key hierarchy to become ready (after local reset)");
CKKSResultOperation* waitOp = [CKKSResultOperation named:@"waiting-for-local-reset" withBlock:^{}];
[waitOp timeout: 60*NSEC_PER_SEC];
[waitOp addNullableDependency:strongSelf.keyStateReadyDependency];

[strongOp runBeforeGroupFinished:waitOp];
}];
[viewReset addSuccessDependency:resetOperationGroup];

[self scheduleOperationWithoutDependencies:viewReset];
return viewReset;
} else {
// Since we're logged out, we must run the reset ourselves
__weak __typeof(self) weakSelf = self;
CKKSResultOperation* pendingResetLocalOperation = [CKKSResultOperation named:@"reset-local"
withBlockTakingSelf:^(CKKSResultOperation * _Nonnull strongOp) {
__strong __typeof(self) strongSelf = weakSelf;
__block NSError* error = nil;

[strongSelf dispatchSync: ^bool{
[strongSelf _onqueueResetLocalData: &error];
return true;
}];

strongOp.error = error;
}];
[self scheduleOperationWithoutDependencies:pendingResetLocalOperation];
return pendingResetLocalOperation;
}
}

- (CKKSResultOperation*)createPendingDeleteZoneOperation:(CKOperationGroup*)operationGroup {
@synchronized(self.cloudkitDeleteZoneOperations) {
CKKSResultOperation* pendingDeleteOperation = (CKKSResultOperation*) [self findFirstPendingOperation:self.cloudkitDeleteZoneOperations];
if(!pendingDeleteOperation) {
pendingDeleteOperation = [self deleteCloudKitZoneOperation:operationGroup];
[pendingDeleteOperation linearDependencies:self.cloudkitDeleteZoneOperations];
}
return pendingDeleteOperation;
}
}

- (CKKSResultOperation*)resetCloudKitZone:(CKOperationGroup*)operationGroup {
// Not overly thread-safe, but a single read is okay
if(self.accountStatus == CKKSAccountStatusAvailable) {
// Actually running the delete operation will be handled by the CKKS key state machine
ckksnotice("ckksreset", self, "Requesting reset of CK zone (logged in)");

__block CKKSResultOperation* deleteOperation = nil;
[self dispatchSyncWithAccountKeys:^bool {
self.keyStateCloudKitDeleteRequested = true;
deleteOperation = [self createPendingDeleteZoneOperation:operationGroup];
[self _onqueueAdvanceKeyStateMachineToState:nil withError:nil];
return true;
}];

__weak __typeof(self) weakSelf = self;
CKKSGroupOperation* viewReset = [CKKSGroupOperation named:[NSString stringWithFormat:@"cloudkit-view-reset-%@", self.zoneName]
withBlockTakingSelf:^(CKKSGroupOperation *strongOp) {
__strong __typeof(self) strongSelf = weakSelf;
// Now that the delete finished, wait for the key hierarchy state machine
ckksnotice("ckksreset", strongSelf, "waiting for key hierarchy to become ready (after cloudkit reset)");
CKKSResultOperation* waitOp = [CKKSResultOperation named:@"waiting-for-reset" withBlock:^{}];
[waitOp timeout: 60*NSEC_PER_SEC];
[waitOp addNullableDependency:strongSelf.keyStateReadyDependency];

[strongOp runBeforeGroupFinished:waitOp];
}];

[viewReset addDependency:deleteOperation];
[self.waitingQueue addOperation:viewReset];

return viewReset;
} else {
// Since we're logged out, we just need to run this ourselves
ckksnotice("ckksreset", self, "Requesting reset of CK zone (logged out)");
CKKSResultOperation* deleteOperation = [self createPendingDeleteZoneOperation:operationGroup];
[self scheduleOperationWithoutDependencies:deleteOperation];
return deleteOperation;
}
}

- (void)_onqueueKeyStateMachineRequestFetch {
dispatch_assert_queue(self.queue);

// We're going to set this flag, then nudge the key state machine.
// If it was idle, then it should launch a fetch. If there was an active process, this flag will stay high
// and the fetch will be launched later.

self.keyStateFetchRequested = true;
[self _onqueueAdvanceKeyStateMachineToState: nil withError: nil];
}

- (void)keyStateMachineRequestProcess {
// Since bools are atomic, we don't need to get on-queue here
// Just set the flag high and hope
self.keyStateProcessRequested = true;
[self.pokeKeyStateMachineScheduler trigger];
}

- (void)_onqueueKeyStateMachineRequestProcess {
dispatch_assert_queue(self.queue);

// Set the request flag, then nudge the key state machine.
// If it was idle, then it should launch a process. If there was an active process, this flag will stay high
// and the process will be launched later.

self.keyStateProcessRequested = true;
[self _onqueueAdvanceKeyStateMachineToState: nil withError: nil];
}

- (CKKSResultOperation*)createKeyStateReadyDependency:(NSString*)message ckoperationGroup:(CKOperationGroup*)group {
__weak __typeof(self) weakSelf = self;
CKKSResultOperation* keyStateReadyDependency = [CKKSResultOperation operationWithBlock:^{
__strong __typeof(self) strongSelf = weakSelf;
if(!strongSelf) {
return;
}
ckksnotice("ckkskey", strongSelf, "%@", message);

[strongSelf dispatchSync:^bool {
if(strongSelf.droppedItems) {
// While we weren't in 'ready', keychain modifications might have come in and were dropped on the floor. Find them!
ckksnotice("ckkskey", strongSelf, "Launching scan operation for missed items");
[self scanLocalItems:@"ready-again-scan" ckoperationGroup:group after:nil];
}
return true;
}];
}];
keyStateReadyDependency.name = [NSString stringWithFormat: @"%@-key-state-ready", self.zoneName];
keyStateReadyDependency.descriptionErrorCode = CKKSResultDescriptionPendingKeyReady;
return keyStateReadyDependency;
}

- (CKKSResultOperation*)createKeyStateNontransientDependency {
__weak __typeof(self) weakSelf = self;
return [CKKSResultOperation named:[NSString stringWithFormat: @"%@-key-state-nontransient", self.zoneName] withBlock:^{
__strong __typeof(self) strongSelf = weakSelf;
ckksnotice("ckkskey", strongSelf, "Key state is now non-transient");
}];
}

// The operations suggested by this state machine should call _onqueueAdvanceKeyStateMachineToState once they are complete.
// At no other time should keyHierarchyState be modified.

// Note that this function cannot rely on doing any database work; it might get rolled back, especially in an error state
- (void)_onqueueAdvanceKeyStateMachineToState: (CKKSZoneKeyState*) state withError: (NSError*) error {
dispatch_assert_queue(self.queue);
__weak __typeof(self) weakSelf = self;

// Resetting back to 'loggedout' takes all precedence.
if([state isEqual:SecCKKSZoneKeyStateLoggedOut]) {
ckksnotice("ckkskey", self, "Resetting the key hierarchy state machine back to '%@'", state);

[self _onqueueResetSetup:SecCKKSZoneKeyStateLoggedOut
resetMessage:@"Key state has become ready for the first time (after reset)."
ckoperationGroup:[CKOperationGroup CKKSGroupWithName:@"key-state-after-logout"]];

[self _onqueueHandleKeyStateNonTransientDependency];
return;
}

// Resetting back to 'initialized' also takes precedence
if([state isEqual:SecCKKSZoneKeyStateInitializing]) {
ckksnotice("ckkskey", self, "Resetting the key hierarchy state machine back to '%@'", state);

[self _onqueueResetSetup:SecCKKSZoneKeyStateInitializing
resetMessage:@"Key state has become ready for the first time (after re-initializing)."
ckoperationGroup:[CKOperationGroup CKKSGroupWithName:@"key-state-reset-to-initializing"]];

// Begin initialization, but rate-limit it
self.keyStateMachineOperation = [self createPendingInitializationOperation];
[self.keyStateMachineOperation addNullableDependency:self.initializeScheduler.operationDependency];
[self.initializeScheduler trigger];
[self scheduleOperation:self.keyStateMachineOperation];

[self _onqueueHandleKeyStateNonTransientDependency];
return;
}

// Cancels and error states take precedence
if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateError] ||
[self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateCancelled] ||
self.keyHierarchyError != nil) {
// Error state: nowhere to go. Early-exit.
ckkserror("ckkskey", self, "Asked to advance state machine from non-exit state %@ (to %@): %@", self.keyHierarchyState, state, self.keyHierarchyError);
return;
}

if([state isEqual: SecCKKSZoneKeyStateError]) {
// But wait! Is this a "we're locked" error?
if(error && [self.lockStateTracker isLockedError:error]) {
ckkserror("ckkskey", self, "advised of 'keychain locked' error, ignoring: coming from state (%@): %@", self.keyHierarchyState, error);
// After the next unlock, fake that we received the last zone transition
CKKSZoneKeyState* lastState = self.keyHierarchyState;
self.keyStateMachineOperation = [NSBlockOperation named:@"key-state-after-unlock" withBlock:^{
__strong __typeof(self) strongSelf = weakSelf;
if(!strongSelf) {
return;
}
[strongSelf dispatchSync:^bool{
[strongSelf _onqueueAdvanceKeyStateMachineToState:lastState withError:nil];
return true;
}];
}];
state = nil;

self.keyHierarchyState = SecCKKSZoneKeyStateWaitForUnlock;

[self.keyStateMachineOperation addNullableDependency:self.lockStateTracker.unlockDependency];
[self scheduleOperation:self.keyStateMachineOperation];

[self _onqueueHandleKeyStateNonTransientDependency];
return;

} else {
// Error state: record the error and exit early
ckkserror("ckkskey", self, "advised of error: coming from state (%@): %@", self.keyHierarchyState, error);

[[CKKSAnalytics logger] logUnrecoverableError:error
forEvent:CKKSEventStateError
inView:self
withAttributes:@{ @"previousKeyHierarchyState" : self.keyHierarchyState }];

self.keyHierarchyState = SecCKKSZoneKeyStateError;
self.keyHierarchyError = error;

[self _onqueueHandleKeyStateNonTransientDependency];
return;
}
}

if([state isEqual: SecCKKSZoneKeyStateCancelled]) {
ckkserror("ckkskey", self, "advised of cancel: coming from state (%@): %@", self.keyHierarchyState, error);
self.keyHierarchyState = SecCKKSZoneKeyStateCancelled;
self.keyHierarchyError = error;

// 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.
self.keyHierarchyOperationGroup = nil;
[self.keyStateReadyDependency cancel];
self.keyStateReadyDependency = nil;

[self.keyStateNonTransientDependency cancel];
self.keyStateNonTransientDependency = nil;
return;
}

// Now that the current or new state isn't an error or a cancel, proceed.
if(self.keyStateMachineOperation && ![self.keyStateMachineOperation isFinished]) {
if(state == nil) {
// 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
ckksnotice("ckkskey", self, "Not advancing state machine: waiting for %@", self.keyStateMachineOperation);
return;
}
}

if(state) {
ckksnotice("ckkskey", self, "Preparing to advance key hierarchy state machine from %@ to %@", self.keyHierarchyState, state);
self.keyStateMachineOperation = nil;
} else {
ckksnotice("ckkskey", self, "Key hierarchy state machine is being poked; currently %@", self.keyHierarchyState);
state = self.keyHierarchyState;
}

#if DEBUG
// During testing, keep the developer honest: this function should always have the self identities
if(self.currentSelfPeersError) {
NSAssert(self.currentSelfPeersError.code != CKKSNoPeersAvailable, @"Must have viable (or errored) self peers to advance key state");
}
#endif

// Do any of these state transitions below want to change which state we're in?
CKKSZoneKeyState* nextState = nil;
NSError* nextError = nil;

// Many of our decisions below will be based on what keys exist. Help them out.
CKKSCurrentKeySet* keyset = [[CKKSCurrentKeySet alloc] initForZone:self.zoneID];
NSError* localerror = nil;
NSArray<CKKSKey*>* localKeys = [CKKSKey localKeys:self.zoneID error:&localerror];
NSArray<CKKSKey*>* remoteKeys = [CKKSKey remoteKeys:self.zoneID error: &localerror];

// We also are checking for OutgoingQueueEntries in the reencrypt state; this is a sign that our key hierarchy is out of date.
NSInteger outdatedOQEs = [CKKSOutgoingQueueEntry countByState:SecCKKSStateReencrypt zone:self.zoneID error:&localerror];

SecADSetValueForScalarKey((__bridge CFStringRef) SecCKKSAggdViewKeyCount, [localKeys count]);

if(localerror) {
ckkserror("ckkskey", self, "couldn't fetch keys and OQEs from local database, entering error state: %@", localerror);
self.keyHierarchyState = SecCKKSZoneKeyStateError;
self.keyHierarchyError = localerror;
[self _onqueueHandleKeyStateNonTransientDependency];
return;
}

#if !defined(NDEBUG)
NSArray<CKKSKey*>* allKeys = [CKKSKey allKeys:self.zoneID error:&localerror];
ckksdebug("ckkskey", self, "All keys: %@", allKeys);
#endif

NSError* hierarchyError = nil;

if(self.keyStateCloudKitDeleteRequested || [state isEqualToString:SecCKKSZoneKeyStateResettingZone]) {
// CloudKit reset requests take precedence over all other state transitions
ckksnotice("ckkskey", self, "Deleting the CloudKit Zone");
CKKSGroupOperation* op = [[CKKSGroupOperation alloc] init];

CKKSResultOperation* deleteOp = [self createPendingDeleteZoneOperation:self.keyHierarchyOperationGroup];
[op runBeforeGroupFinished: deleteOp];

NSOperation* nextStateOp = [self operationToEnterState:SecCKKSZoneKeyStateResettingLocalData keyStateError:nil named:@"state-resetting-local"];
[nextStateOp addDependency:deleteOp];
[op runBeforeGroupFinished:nextStateOp];

self.keyStateMachineOperation = op;
self.keyStateCloudKitDeleteRequested = false;

// Also, pending operations should be cancelled
[self cancelPendingOperations];

} else if(self.keyStateLocalResetRequested || [state isEqualToString:SecCKKSZoneKeyStateResettingLocalData]) {
// Local reset requests take precedence over all other state transitions
ckksnotice("ckkskey", self, "Resetting local data");
CKKSGroupOperation* op = [[CKKSGroupOperation alloc] init];

CKKSResultOperation* resetOp = [self createPendingResetLocalDataOperation];
[op runBeforeGroupFinished: resetOp];

NSOperation* nextStateOp = [self operationToEnterState:SecCKKSZoneKeyStateInitializing keyStateError:nil named:@"state-resetting-initialize"];
[nextStateOp addDependency:resetOp];
[op runBeforeGroupFinished:nextStateOp];

self.keyStateMachineOperation = op;
self.keyStateLocalResetRequested = false;

} else if([state isEqualToString:SecCKKSZoneKeyStateZoneCreationFailed]) {
//Prepare to go back into initializing, as soon as the initializeScheduler is happy
self.keyStateMachineOperation = [self operationToEnterState:SecCKKSZoneKeyStateInitializing keyStateError:nil named:@"recover-from-cloudkit-failure"];
[self.keyStateMachineOperation addNullableDependency:self.initializeScheduler.operationDependency];
[self.initializeScheduler trigger];

} else if([state isEqualToString: SecCKKSZoneKeyStateReady]) {
if(self.keyStateProcessRequested || [remoteKeys count] > 0) {
// We've either received some remote keys from the last fetch, or someone has requested a reprocess.
ckksnotice("ckkskey", self, "Kicking off a key reprocess based on request:%d and remote key count %lu", self.keyStateProcessRequested, (unsigned long)[remoteKeys count]);
nextState = SecCKKSZoneKeyStateProcess;

} else if(self.keyStateFullRefetchRequested) {
// In ready, but someone has requested a full fetch. Kick it off.
ckksnotice("ckkskey", self, "Kicking off a full key refetch based on request:%d", self.keyStateFullRefetchRequested);
nextState = SecCKKSZoneKeyStateNeedFullRefetch;

} else if(self.keyStateFetchRequested) {
// In ready, but someone has requested a fetch. Kick it off.
ckksnotice("ckkskey", self, "Kicking off a key refetch based on request:%d", self.keyStateFetchRequested);
nextState = SecCKKSZoneKeyStateFetch; // Don't go to 'ready', go to 'initialized', since we want to fetch again
}
// TODO: kick off a key roll if one has been requested

if(!self.keyStateMachineOperation) {
// We think we're ready. Double check.
CKKSZoneKeyState* checkedstate = [self _onqueueEnsureKeyHierarchyHealth:keyset error:&hierarchyError];
if(![checkedstate isEqualToString:SecCKKSZoneKeyStateReady] || hierarchyError) {
// Things is bad. Kick off a heal to fix things up.
ckksnotice("ckkskey", self, "Thought we were ready, but the key hierarchy is %@: %@", checkedstate, hierarchyError);
nextState = checkedstate;
if([nextState isEqualToString:SecCKKSZoneKeyStateError]) {
nextError = hierarchyError;
}
}
}

} else if([state isEqualToString: SecCKKSZoneKeyStateInitialized]) {
// We're initialized and CloudKit is ready. See what needs done...

CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.zoneName];
[self _onqueuePerformKeyStateInitialized:ckse];

// We need to either:
// Wait for the fixup operation to occur
// Go into 'ready'
// Or start a key state fetch
if(self.lastFixupOperation && ![self.lastFixupOperation isFinished]) {
nextState = SecCKKSZoneKeyStateWaitForFixupOperation;
} else {
// Check if we have an existing key hierarchy in keyset
if(keyset.error && !([keyset.error.domain isEqual: @"securityd"] && keyset.error.code == errSecItemNotFound)) {
ckkserror("ckkskey", self, "Error examining existing key hierarchy: %@", error);
}

if(keyset.tlk && keyset.classA && keyset.classC && !keyset.error) {
// This is likely a restart of securityd, and we think we're ready. Double check.

CKKSZoneKeyState* checkedstate = [self _onqueueEnsureKeyHierarchyHealth:keyset error:&hierarchyError];
if([checkedstate isEqualToString:SecCKKSZoneKeyStateReady] && !hierarchyError) {
ckksnotice("ckkskey", self, "Already have existing key hierarchy for %@; using it.", self.zoneID.zoneName);
} else {
ckksnotice("ckkskey", self, "Initial scan shows key hierarchy is %@: %@", checkedstate, hierarchyError);
}
nextState = checkedstate;

} else {
// We have no local key hierarchy. One might exist in CloudKit, or it might not.
ckksnotice("ckkskey", self, "No existing key hierarchy for %@. Check if there's one in CloudKit...", self.zoneID.zoneName);
nextState = SecCKKSZoneKeyStateFetch;
}
}

} else if([state isEqualToString:SecCKKSZoneKeyStateFetch]) {
ckksnotice("ckkskey", self, "Starting a key hierarchy fetch");
[self _onqueueKeyHierarchyFetch];

} else if([state isEqualToString: SecCKKSZoneKeyStateNeedFullRefetch]) {
ckksnotice("ckkskey", self, "Starting a key hierarchy full refetch");
[self _onqueueKeyHierarchyRefetch];

} else if([state isEqualToString:SecCKKSZoneKeyStateWaitForFixupOperation]) {
// We should enter 'initialized' when the fixup operation completes
ckksnotice("ckkskey", self, "Waiting for the fixup operation: %@", self.lastFixupOperation);

self.keyStateMachineOperation = [NSBlockOperation named:@"key-state-after-fixup" withBlock:^{
__strong __typeof(self) strongSelf = weakSelf;
[strongSelf dispatchSyncWithAccountKeys:^bool{
ckksnotice("ckkskey", self, "Fixup operation complete! Restarting key hierarchy machinery");
[strongSelf _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateInitialized withError:nil];
return true;
}];
}];
[self.keyStateMachineOperation addNullableDependency:self.lastFixupOperation];

} else if([state isEqualToString: SecCKKSZoneKeyStateFetchComplete]) {
// We've just completed a fetch of everything. Are there any remote keys?
if(remoteKeys.count > 0u) {
// Process the keys we received.
self.keyStateMachineOperation = [[CKKSProcessReceivedKeysOperation alloc] initWithCKKSKeychainView: self];
} else if( (keyset.currentTLKPointer || keyset.currentClassAPointer || keyset.currentClassCPointer) &&
!(keyset.tlk && keyset.classA && keyset.classC)) {
// Huh. We appear to have current key pointers, but the keys themselves don't exist. That's weird.
// Transfer to the "unhealthy" state to request a fix
ckksnotice("ckkskey", self, "We appear to have current key pointers but no keys to match them. Moving to 'unhealthy'");
nextState = SecCKKSZoneKeyStateUnhealthy;
} else {
// No remote keys, and the pointers look sane? Do we have an existing key hierarchy?
CKKSZoneKeyState* checkedstate = [self _onqueueEnsureKeyHierarchyHealth:keyset error:&hierarchyError];
if([checkedstate isEqualToString:SecCKKSZoneKeyStateReady] && !hierarchyError) {
ckksnotice("ckkskey", self, "After fetch, everything looks good.");
nextState = checkedstate;
} else if(localKeys.count == 0 && remoteKeys.count == 0) {
ckksnotice("ckkskey", self, "After fetch, we don't have any key hierarchy. Making a new one: %@", hierarchyError);
self.keyStateMachineOperation = [[CKKSNewTLKOperation alloc] initWithCKKSKeychainView: self ckoperationGroup:self.keyHierarchyOperationGroup];
} else {
ckksnotice("ckkskey", self, "After fetch, we have a possibly unhealthy key hierarchy. Moving to %@: %@", checkedstate, hierarchyError);
nextState = checkedstate;
}
}

} else if([state isEqualToString: SecCKKSZoneKeyStateWaitForTLK]) {
// We're in a hold state: waiting for the TLK bytes to arrive.

if(self.keyStateProcessRequested) {
// Someone has requsted a reprocess! Go to the correct state.
ckksnotice("ckkskey", self, "Received a nudge that our TLK might be here! Reprocessing.");
nextState = SecCKKSZoneKeyStateProcess;

} else if(self.trustedPeersSetChanged) {
// Hmm, maybe this trust set change will cause us to recover this TLK (due to a previously-untrusted share becoming trusted). Worth a shot!
ckksnotice("ckkskey", self, "Received a nudge that the trusted peers set might have changed! Reprocessing.");
nextState = SecCKKSZoneKeyStateProcess;
self.trustedPeersSetChanged = false;

} else {
// Should we nuke this zone?
if([self _onqueueOtherDevicesReportHavingTLKs:keyset]) {
ckksnotice("ckkskey", self, "Other devices report having TLK(%@). Entering a waiting state", keyset.currentTLKPointer);
} else {
ckksnotice("ckkskey", self, "No other devices have TLK(%@). Beginning zone reset...", keyset.currentTLKPointer);
nextState = SecCKKSZoneKeyStateResettingZone;
}
}

} else if([state isEqualToString: SecCKKSZoneKeyStateWaitForUnlock]) {
ckksnotice("ckkskey", self, "Requested to enter waitforunlock");
self.keyStateMachineOperation = [self operationToEnterState:SecCKKSZoneKeyStateInitialized keyStateError:nil named:@"key-state-after-unlock"];
[self.keyStateMachineOperation addNullableDependency: self.lockStateTracker.unlockDependency];

} else if([state isEqualToString: SecCKKSZoneKeyStateReadyPendingUnlock]) {
ckksnotice("ckkskey", self, "Believe we're ready, but rechecking after unlock");
self.keyStateMachineOperation = [self operationToEnterState:SecCKKSZoneKeyStateInitialized keyStateError:nil named:@"key-state-after-unlock"];
[self.keyStateMachineOperation addNullableDependency: self.lockStateTracker.unlockDependency];

} else if([state isEqualToString: SecCKKSZoneKeyStateBadCurrentPointers]) {
// The current key pointers are broken, but we're not sure why.
ckksnotice("ckkskey", self, "Our current key pointers are reported broken. Attempting a fix!");
self.keyStateMachineOperation = [[CKKSHealKeyHierarchyOperation alloc] initWithCKKSKeychainView: self ckoperationGroup:self.keyHierarchyOperationGroup];

} else if([state isEqualToString: SecCKKSZoneKeyStateNewTLKsFailed]) {
ckksnotice("ckkskey", self, "Creating new TLKs didn't work. Attempting to refetch!");
[self _onqueueKeyHierarchyFetch];

} else if([state isEqualToString: SecCKKSZoneKeyStateHealTLKSharesFailed]) {
ckksnotice("ckkskey", self, "Creating new TLK shares didn't work. Attempting to refetch!");
[self _onqueueKeyHierarchyFetch];

} else if([state isEqualToString:SecCKKSZoneKeyStateUnhealthy]) {
ckksnotice("ckkskey", self, "Looks like the key hierarchy is unhealthy. Launching fix.");
self.keyStateMachineOperation = [[CKKSHealKeyHierarchyOperation alloc] initWithCKKSKeychainView:self ckoperationGroup:self.keyHierarchyOperationGroup];

} else if([state isEqualToString:SecCKKSZoneKeyStateHealTLKShares]) {
ckksnotice("ckksshare", self, "Key hierarchy is okay, but not shared appropriately. Launching fix.");
self.keyStateMachineOperation = [[CKKSHealTLKSharesOperation alloc] initWithCKKSKeychainView:self
ckoperationGroup:self.keyHierarchyOperationGroup];

} else if([state isEqualToString:SecCKKSZoneKeyStateProcess]) {
ckksnotice("ckksshare", self, "Launching key state process");
self.keyStateMachineOperation = [[CKKSProcessReceivedKeysOperation alloc] initWithCKKSKeychainView: self];

// Since we're starting a reprocess, this is answering all previous requests.
self.keyStateProcessRequested = false;

} else {
ckkserror("ckks", self, "asked to advance state machine to unknown state: %@", state);
self.keyHierarchyState = state;
[self _onqueueHandleKeyStateNonTransientDependency];
return;
}

// Handle the key state ready dependency
// If we're in ready and not entering a non-ready state, we should activate the ready dependency. Otherwise, we should create it.
if(([state isEqualToString:SecCKKSZoneKeyStateReady] || [state isEqualToString:SecCKKSZoneKeyStateReadyPendingUnlock]) &&
(nextState == nil || [nextState isEqualToString:SecCKKSZoneKeyStateReady] || [nextState isEqualToString:SecCKKSZoneKeyStateReadyPendingUnlock])) {

// Ready enough!
[[CKKSAnalytics logger] setDateProperty:[NSDate date] forKey:CKKSAnalyticsLastKeystateReady inView:self];
if(self.keyStateReadyDependency) {
[self scheduleOperation: self.keyStateReadyDependency];
self.keyStateReadyDependency = nil;
}

// If there are any OQEs waiting to be encrypted, launch an op to fix them
if(outdatedOQEs > 0) {
ckksnotice("ckksreencrypt", self, "Reencrypting outgoing items as the key hierarchy is ready");
CKKSReencryptOutgoingItemsOperation* op = [[CKKSReencryptOutgoingItemsOperation alloc] initWithCKKSKeychainView:self ckoperationGroup:self.keyHierarchyOperationGroup];
[self scheduleOperation:op];
}
} else {
// Not in ready: we need a key state ready dependency
if(self.keyStateReadyDependency == nil || [self.keyStateReadyDependency isFinished]) {
self.keyHierarchyOperationGroup = [CKOperationGroup CKKSGroupWithName:@"key-state-broken"];
self.keyStateReadyDependency = [self createKeyStateReadyDependency:@"Key state has become ready again." ckoperationGroup:self.keyHierarchyOperationGroup];
}
}

NSAssert(!((self.keyStateMachineOperation != nil) &&
(nextState != nil)),
@"Should have a machine operation or a next state, not both");

// Start any operations, or log that we aren't
if(self.keyStateMachineOperation) {
[self scheduleOperation: self.keyStateMachineOperation];
ckksnotice("ckkskey", self, "Now in key state: %@", state);
self.keyHierarchyState = state;

} else if([state isEqualToString:SecCKKSZoneKeyStateError]) {
ckksnotice("ckkskey", self, "Entering key state 'error'");
self.keyHierarchyState = state;

} else if(nextState == nil) {
ckksnotice("ckkskey", self, "Entering key state: %@", state);
self.keyHierarchyState = state;

} else if(![state isEqualToString: nextState]) {
ckksnotice("ckkskey", self, "Staying in state %@, but proceeding to %@ as soon as possible", self.keyHierarchyState, nextState);
self.keyStateMachineOperation = [self operationToEnterState:nextState keyStateError:nextError named:@"next-key-state"];
[self scheduleOperation: self.keyStateMachineOperation];

} else {
// Nothing to do and not in a waiting state? This is likely a bug, but, hey: pretend to be in ready!
if(!([state isEqualToString:SecCKKSZoneKeyStateReady] || [state isEqualToString:SecCKKSZoneKeyStateReadyPendingUnlock])) {
ckkserror("ckkskey", self, "No action to take in state %@; BUG, but: maybe we're ready?", state);
nextState = SecCKKSZoneKeyStateReady;
self.keyStateMachineOperation = [self operationToEnterState:nextState keyStateError:nil named:@"next-key-state"];
[self scheduleOperation: self.keyStateMachineOperation];
}
}

[self _onqueueHandleKeyStateNonTransientDependency];
}

- (void)_onqueueHandleKeyStateNonTransientDependency {
dispatch_assert_queue(self.queue);

if(CKKSKeyStateTransient(self.keyHierarchyState)) {
if(self.keyStateNonTransientDependency == nil || [self.keyStateNonTransientDependency isFinished]) {
self.keyStateNonTransientDependency = [self createKeyStateNontransientDependency];
}
} else {
// Nontransient: go for it
if(self.keyStateNonTransientDependency) {
[self scheduleOperation: self.keyStateNonTransientDependency];
self.keyStateNonTransientDependency = nil;
}
}
}

- (NSOperation*)operationToEnterState:(CKKSZoneKeyState*)state keyStateError:(NSError* _Nullable)keyStateError named:(NSString*)name {
__weak __typeof(self) weakSelf = self;

return [NSBlockOperation named:name withBlock:^{
__strong __typeof(self) strongSelf = weakSelf;
if(!strongSelf) {
return;
}
[strongSelf dispatchSyncWithAccountKeys:^bool{
[strongSelf _onqueueAdvanceKeyStateMachineToState:state withError:keyStateError];
return true;
}];
}];
}

- (bool)_onqueueOtherDevicesReportHavingTLKs:(CKKSCurrentKeySet*)keyset
{
dispatch_assert_queue(self.queue);

//Has there been any activity indicating that other trusted devices have keys in the past 45 days, or untrusted devices in the past 4?
// (We chose 4 as devices attempt to upload their device state every 3 days. If a device is unceremoniously kicked out of circle, we normally won't immediately reset.)
NSDate* now = [NSDate date];
NSDateComponents* trustedOffset = [[NSDateComponents alloc] init];
[trustedOffset setDay:-45];
NSDate* trustedDeadline = [[NSCalendar currentCalendar] dateByAddingComponents:trustedOffset toDate:now options:0];

NSDateComponents* untrustedOffset = [[NSDateComponents alloc] init];
[untrustedOffset setDay:-4];
NSDate* untrustedDeadline = [[NSCalendar currentCalendar] dateByAddingComponents:untrustedOffset toDate:now options:0];

NSMutableSet<NSString*>* trustedPeerIDs = [NSMutableSet set];
for(id<CKKSPeer> peer in self.currentTrustedPeers) {
[trustedPeerIDs addObject:peer.peerID];
}

NSError* localerror = nil;

NSArray<CKKSDeviceStateEntry*>* allDeviceStates = [CKKSDeviceStateEntry allInZone:self.zoneID error:&localerror];
if(localerror) {
ckkserror("ckkskey", self, "Error fetching device states: %@", localerror);
localerror = nil;
return true;
}
for(CKKSDeviceStateEntry* device in allDeviceStates) {
if(device.octagonPeerID != nil) {
ckksnotice("ckkskey", self, "An Octagon-capable device has been in this account; not resetting: (%@)", device);
return true;
}

if([trustedPeerIDs containsObject:device.circlePeerID] || [trustedPeerIDs containsObject:device.octagonPeerID]) {
// Is this a recent DSE? If it's older than the deadline, skip it
if([device.storedCKRecord.modificationDate compare:trustedDeadline] == NSOrderedAscending) {
ckksnotice("ckkskey", self, "Trusted device state (%@) is too old; ignoring", device);
continue;
}
} else {
// Device is untrusted. How does it fare with the untrustedDeadline?
if([device.storedCKRecord.modificationDate compare:untrustedDeadline] == NSOrderedAscending) {
ckksnotice("ckkskey", self, "Device (%@) is not trusted and from too long ago; ignoring device state (%@)", device.circlePeerID, device);
continue;
} else {
ckksnotice("ckkskey", self, "Device (%@) is not trusted, but very recent. Including in heuristic: %@", device.circlePeerID, device);
}
}

if([device.keyState isEqualToString:SecCKKSZoneKeyStateReady] ||
[device.keyState isEqualToString:SecCKKSZoneKeyStateReadyPendingUnlock]) {
ckksnotice("ckkskey", self, "Other device (%@) has keys; it should send them to us", device);
return true;
}
}

NSArray<CKKSTLKShare*>* tlkShares = [CKKSTLKShare allForUUID:keyset.currentTLKPointer.currentKeyUUID
zoneID:self.zoneID
error:&localerror];
if(localerror) {
ckkserror("ckkskey", self, "Error fetching device states: %@", localerror);
localerror = nil;
return false;
}

for(CKKSTLKShare* tlkShare in tlkShares) {
if([trustedPeerIDs containsObject:tlkShare.senderPeerID] &&
[tlkShare.storedCKRecord.modificationDate compare:trustedDeadline] == NSOrderedDescending) {
ckksnotice("ckkskey", self, "Trusted TLK Share (%@) created recently; other devices have keys and should send them to us", tlkShare);
return true;
}
}

// Okay, how about the untrusted deadline?
for(CKKSTLKShare* tlkShare in tlkShares) {
if([tlkShare.storedCKRecord.modificationDate compare:untrustedDeadline] == NSOrderedDescending) {
ckksnotice("ckkskey", self, "Untrusted TLK Share (%@) created very recently; other devices might have keys and should rejoin the circle (and send them to us)", tlkShare);
return true;
}
}

return false;
}

// For this key, who doesn't yet have a valid CKKSTLKShare for it?
// Note that we really want a record sharing the TLK to ourselves, so this function might return
// a non-empty set even if all peers have the TLK: it wants us to make a record for ourself.
- (NSSet<id<CKKSPeer>>*)_onqueueFindPeersMissingShare:(CKKSKey*)key error:(NSError* __autoreleasing*)error {
dispatch_assert_queue(self.queue);

if(!key) {
ckkserror("ckksshare", self, "Attempting to find missing shares for nil key");
return [NSSet set];
}

if(self.currentTrustedPeersError) {
ckkserror("ckksshare", self, "Couldn't find missing shares because trusted peers aren't available: %@", self.currentTrustedPeersError);
if(error) {
*error = self.currentTrustedPeersError;
}
return [NSSet set];
}
if(self.currentSelfPeersError) {
ckkserror("ckksshare", self, "Couldn't find missing shares because self peers aren't available: %@", self.currentSelfPeersError);
if(error) {
*error = self.currentSelfPeersError;
}
return [NSSet set];
}

NSMutableSet<id<CKKSPeer>>* peersMissingShares = [NSMutableSet set];

NSMutableSet<NSString*>* trustedPeerIDs = [NSMutableSet set];
for(id<CKKSPeer> peer in self.currentTrustedPeers) {
[trustedPeerIDs addObject:peer.peerID];
}

for(id<CKKSPeer> peer in self.currentTrustedPeers) {
NSError* peerError = nil;
// Find all the shares for this peer for this key
NSArray<CKKSTLKShare*>* currentPeerShares = [CKKSTLKShare allFor:peer.peerID
keyUUID:key.uuid
zoneID:self.zoneID
error:&peerError];

if(peerError) {
ckkserror("ckksshare", self, "Couldn't load shares for peer %@: %@", peer, peerError);
if(error) {
*error = peerError;
}
return nil;
}

// Determine if we think this peer has enough things shared to them
bool alreadyShared = false;
for(CKKSTLKShare* existingPeerShare in currentPeerShares) {
// If an SOS Peer sent this share, is its signature still valid? Or did the signing key change?
if([existingPeerShare.senderPeerID hasPrefix:CKKSSOSPeerPrefix]) {
NSError* signatureError = nil;
if(![existingPeerShare signatureVerifiesWithPeerSet:self.currentTrustedPeers error:&signatureError]) {
ckksnotice("ckksshare", self, "Existing TLKShare's signature doesn't verify with current peer set: %@ %@", signatureError, existingPeerShare);
continue;
}
}

if([existingPeerShare.tlkUUID isEqualToString: key.uuid] && [trustedPeerIDs containsObject:existingPeerShare.senderPeerID]) {

// Was this shared to us?
if([peer.peerID isEqualToString: self.currentSelfPeers.currentSelf.peerID]) {
// We only count this as 'found' if we did the sharing and it's to our current keys
if([existingPeerShare.senderPeerID isEqualToString:self.currentSelfPeers.currentSelf.peerID] &&
[existingPeerShare.receiver.publicEncryptionKey isEqual:self.currentSelfPeers.currentSelf.publicEncryptionKey]) {
ckksnotice("ckksshare", self, "Local peer %@ is shared %@ via self: %@", peer, key, existingPeerShare);
alreadyShared = true;
break;
} else {
ckksnotice("ckksshare", self, "Local peer %@ is shared %@ via trusted %@, but that's not good enough", peer, key, existingPeerShare);
}

} else {
// Was this shared to the remote peer's current keys?
if([peer.publicEncryptionKey isEqual: existingPeerShare.receiver.publicEncryptionKey]) {
// Some other peer has a trusted share. Cool!
ckksnotice("ckksshare", self, "Peer %@ is shared %@ via trusted %@", peer, key, existingPeerShare);
alreadyShared = true;
break;
} else {
ckksnotice("ckksshare", self, "Peer %@ has a share for %@, but to old keys: %@", peer, key, existingPeerShare);
}
}
}
}

if(!alreadyShared) {
// Add this peer to our set, if it has an encryption key to receive the share
if(peer.publicEncryptionKey) {
[peersMissingShares addObject:peer];
}
}
}

if(peersMissingShares.count > 0u) {
// Log each and every one of the things
ckksnotice("ckksshare", self, "Missing TLK shares for %lu peers: %@", (unsigned long)peersMissingShares.count, peersMissingShares);
ckksnotice("ckksshare", self, "Self peers are (%@) %@", self.currentSelfPeersError ?: @"no error", self.currentSelfPeers);
ckksnotice("ckksshare", self, "Trusted peers are (%@) %@", self.currentTrustedPeersError ?: @"no error", self.currentTrustedPeers);
}

return peersMissingShares;
}

- (NSSet<CKKSTLKShare*>*)_onqueueCreateMissingKeyShares:(CKKSKey*)key error:(NSError* __autoreleasing*)error {
dispatch_assert_queue(self.queue);

if(self.currentTrustedPeersError) {
ckkserror("ckksshare", self, "Couldn't create missing shares because trusted peers aren't available: %@", self.currentTrustedPeersError);
if(error) {
*error = self.currentTrustedPeersError;
}
return nil;
}
if(self.currentSelfPeersError) {
ckkserror("ckksshare", self, "Couldn't create missing shares because self peers aren't available: %@", self.currentSelfPeersError);
if(error) {
*error = self.currentSelfPeersError;
}
return nil;
}

NSSet<id<CKKSPeer>>* remainingPeers = [self _onqueueFindPeersMissingShare:key error:error];
NSMutableSet<CKKSTLKShare*>* newShares = [NSMutableSet set];

if(!remainingPeers) {
return nil;
}

NSError* localerror = nil;

if(![key ensureKeyLoaded:error]) {
return nil;
}

for(id<CKKSPeer> peer in remainingPeers) {
if(!peer.publicEncryptionKey) {
ckksnotice("ckksshare", self, "No need to make TLK for %@; they don't have any encryption keys", peer);
continue;
}

// Create a share for this peer.
ckksnotice("ckksshare", self, "Creating share of %@ as %@ for %@", key, self.currentSelfPeers.currentSelf, peer);
CKKSTLKShare* newShare = [CKKSTLKShare share:key
as:self.currentSelfPeers.currentSelf
to:peer
epoch:-1
poisoned:0
error:&localerror];

if(localerror) {
ckkserror("ckksshare", self, "Couldn't create new share for %@: %@", peer, localerror);
if(error) {
*error = localerror;
}
return nil;
}

[newShares addObject: newShare];
}

return newShares;
}

- (CKKSZoneKeyState*)_onqueueEnsureKeyHierarchyHealth:(CKKSCurrentKeySet*)set error:(NSError* __autoreleasing *)error {
dispatch_assert_queue(self.queue);

// Check keyset
if(!set.tlk || !set.classA || !set.classC) {
ckkserror("ckkskey", self, "Error examining existing key hierarchy (missing at least one key): %@", set);
if(error) {
*error = set.error;
}
return SecCKKSZoneKeyStateUnhealthy;
}

NSError* localerror = nil;
bool probablyOkIfUnlocked = false;

// keychain being locked is not a fatal error here
[set.tlk loadKeyMaterialFromKeychain:&localerror];
if(localerror && !([localerror.domain isEqual: @"securityd"] && localerror.code == errSecInteractionNotAllowed)) {
ckkserror("ckkskey", self, "Error loading TLK(%@): %@", set.tlk, localerror);
if(error) {
*error = localerror;
}
return SecCKKSZoneKeyStateUnhealthy;
} else if(localerror) {
ckkserror("ckkskey", self, "Soft error loading TLK(%@), maybe locked: %@", set.tlk, localerror);
probablyOkIfUnlocked = true;
}
localerror = nil;

// keychain being locked is not a fatal error here
[set.classA loadKeyMaterialFromKeychain:&localerror];
if(localerror && !([localerror.domain isEqual: @"securityd"] && localerror.code == errSecInteractionNotAllowed)) {
ckkserror("ckkskey", self, "Error loading classA key(%@): %@", set.classA, localerror);
if(error) {
*error = localerror;
}
return SecCKKSZoneKeyStateUnhealthy;
} else if(localerror) {
ckkserror("ckkskey", self, "Soft error loading classA key(%@), maybe locked: %@", set.classA, localerror);
probablyOkIfUnlocked = true;
}
localerror = nil;

// keychain being locked is a fatal error here, since this is class C
[set.classC loadKeyMaterialFromKeychain:&localerror];
if(localerror) {
ckkserror("ckkskey", self, "Error loading classC(%@): %@", set.classC, localerror);
if(error) {
*error = localerror;
}
return SecCKKSZoneKeyStateUnhealthy;
}

// Check that the classA and classC keys point to the current TLK
if(![set.classA.parentKeyUUID isEqualToString: set.tlk.uuid]) {
localerror = [NSError errorWithDomain:CKKSServerExtensionErrorDomain
code:CKKSServerUnexpectedSyncKeyInChain
userInfo:@{
NSLocalizedDescriptionKey: @"Current class A key does not wrap to current TLK",
}];
ckkserror("ckkskey", self, "Key hierarchy unhealthy: %@", localerror);
if(error) {
*error = localerror;
}
return SecCKKSZoneKeyStateUnhealthy;
}
if(![set.classC.parentKeyUUID isEqualToString: set.tlk.uuid]) {
localerror = [NSError errorWithDomain:CKKSServerExtensionErrorDomain
code:CKKSServerUnexpectedSyncKeyInChain
userInfo:@{
NSLocalizedDescriptionKey: @"Current class C key does not wrap to current TLK",
}];
ckkserror("ckkskey", self, "Key hierarchy unhealthy: %@", localerror);
if(error) {
*error = localerror;
}
return SecCKKSZoneKeyStateUnhealthy;
}

self.activeTLK = [set.tlk uuid];

// Now that we're pretty sure we have the keys, are they shared appropriately?
// Check that every trusted peer has at least one TLK share
NSSet<id<CKKSPeer>>* missingShares = [self _onqueueFindPeersMissingShare:set.tlk error:&localerror];
if(localerror && [self.lockStateTracker isLockedError: localerror]) {
ckkserror("ckkskey", self, "Couldn't find missing TLK shares due to lock state: %@", localerror);
probablyOkIfUnlocked = true;
} else if([localerror.domain isEqualToString:CKKSErrorDomain] && localerror.code == CKKSNoPeersAvailable) {
ckkserror("ckkskey", self, "Couldn't find missing TLK shares due to missing peers, likely due to lock state: %@", localerror);
probablyOkIfUnlocked = true;

} else if(localerror) {
if(error) {
*error = localerror;
}
ckkserror("ckkskey", self, "Error finding missing TLK shares: %@", localerror);
return SecCKKSZoneKeyStateError;
}

if(!missingShares || missingShares.count != 0u) {
localerror = [NSError errorWithDomain:CKKSErrorDomain code:CKKSMissingTLKShare
description:[NSString stringWithFormat:@"Missing shares for %lu peers", (unsigned long)missingShares.count]];
if(error) {
*error = localerror;
}
return SecCKKSZoneKeyStateHealTLKShares;
} else {
ckksnotice("ckksshare", self, "TLK (%@) is shared correctly", set.tlk);
}

// Got to the bottom? Cool! All keys are present and accounted for.
return probablyOkIfUnlocked ? SecCKKSZoneKeyStateReadyPendingUnlock : SecCKKSZoneKeyStateReady;
}

- (void)_onqueueKeyHierarchyFetch {
dispatch_assert_queue(self.queue);

__weak __typeof(self) weakSelf = self;
self.keyStateMachineOperation = [NSBlockOperation blockOperationWithBlock: ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
if(!strongSelf) {
ckkserror("ckks", strongSelf, "received callback for released object");
return;
}

[strongSelf dispatchSyncWithAccountKeys: ^bool{
[strongSelf _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateFetchComplete withError: nil];
return true;
}];
}];
self.keyStateMachineOperation.name = @"waiting-for-fetch";

NSOperation* fetchOp = [self.zoneChangeFetcher requestSuccessfulFetch: CKKSFetchBecauseKeyHierarchy];
[self.keyStateMachineOperation addDependency: fetchOp];

self.keyStateFetchRequested = false;
}

- (void)_onqueueKeyHierarchyRefetch {
dispatch_assert_queue(self.queue);

__weak __typeof(self) weakSelf = self;
self.keyStateMachineOperation = [NSBlockOperation blockOperationWithBlock: ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
if(!strongSelf) {
ckkserror("ckks", strongSelf, "received callback for released object");
return;
}

[strongSelf dispatchSyncWithAccountKeys: ^bool{
[strongSelf _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateFetchComplete withError: nil];
return true;
}];
}];
self.keyStateMachineOperation.name = @"waiting-for-refetch";

NSOperation* fetchOp = [self.zoneChangeFetcher requestSuccessfulFetchForManyReasons:[NSSet setWithObjects:CKKSFetchBecauseKeyHierarchy, CKKSFetchBecauseResync, nil]];
[self.keyStateMachineOperation addDependency: fetchOp];

self.keyStateMachineRefetched = true;
self.keyStateFullRefetchRequested = false;
self.keyStateFetchRequested = false;
}

- (void) handleKeychainEventDbConnection: (SecDbConnectionRef) dbconn
added: (SecDbItemRef) added
deleted: (SecDbItemRef) deleted
rateLimiter: (CKKSRateLimiter*) rateLimiter
syncCallback: (SecBoolNSErrorCallback) syncCallback {
if(!SecCKKSIsEnabled()) {
ckksnotice("ckks", self, "Skipping handleKeychainEventDbConnection due to disabled CKKS");
return;
}

__block NSError* error = nil;

// Tombstones come in as item modifications or item adds. Handle modifications here.
bool addedTombstone = added && SecDbItemIsTombstone(added);
bool deletedTombstone = deleted && SecDbItemIsTombstone(deleted);

bool addedSync = added && SecDbItemIsSyncable(added);
bool deletedSync = deleted && SecDbItemIsSyncable(deleted);

bool isAdd = ( added && !deleted) || (added && deleted && !addedTombstone && deletedTombstone) || (added && deleted && addedSync && !deletedSync);
bool isDelete = (!added && deleted) || (added && deleted && addedTombstone && !deletedTombstone) || (added && deleted && !addedSync && deletedSync);
bool isModify = ( added && deleted) && (!isAdd) && (!isDelete);

// 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.
// Therefore, we might receive an added tombstone here with no deleted item to accompany it. This should be considered a deletion.
if(addedTombstone && !deleted) {
isAdd = false;
isDelete = true;
isModify = false;

// Passed to withItem: below
deleted = added;
}

// If neither item is syncable, don't proceed further in the syncing system
bool proceed = addedSync || deletedSync;

if(!proceed) {
ckksnotice("ckks", self, "skipping sync of non-sync item (%d, %d)", addedSync, deletedSync);
return;
}

// Only synchronize items which can transfer between devices
NSString* protection = (__bridge NSString*)SecDbItemGetCachedValueWithName(added ? added : deleted, kSecAttrAccessible);
if(! ([protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleWhenUnlocked] ||
[protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAfterFirstUnlock] ||
[protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAlwaysPrivate])) {
ckksnotice("ckks", self, "skipping sync of device-bound(%@) item", protection);
return;
}

// Our caller gave us a database connection. We must get on the local queue to ensure atomicity
// Note that we're at the mercy of the surrounding db transaction, so don't try to rollback here
[self dispatchSyncWithConnection: dbconn block: ^bool {
// Schedule a "view changed" notification
[self.notifyViewChangedScheduler trigger];

if(self.accountStatus == CKKSAccountStatusNoAccount) {
// No account; CKKS shouldn't attempt anything.
self.droppedItems = true;

if(syncCallback) {
// We're positively not logged into CloudKit, and therefore don't expect this item to be synced anytime particularly soon.
[self callSyncCallbackWithErrorNoAccount: syncCallback];
}
return true;
}

// Always record the callback, even if we can't encrypt the item right now. Maybe we'll get to it soon!
if(syncCallback) {
CFErrorRef cferror = NULL;
NSString* uuid = (__bridge_transfer NSString*) CFRetain(SecDbItemGetValue(added, &v10itemuuid, &cferror));
if(!cferror && uuid) {
self.pendingSyncCallbacks[uuid] = syncCallback;
}
CFReleaseNull(cferror);
}

CKKSOutgoingQueueEntry* oqe = nil;
if (isAdd) {
oqe = [CKKSOutgoingQueueEntry withItem: added action: SecCKKSActionAdd ckks:self error: &error];
} else if(isDelete) {
oqe = [CKKSOutgoingQueueEntry withItem: deleted action: SecCKKSActionDelete ckks:self error: &error];
} else if(isModify) {
oqe = [CKKSOutgoingQueueEntry withItem: added action: SecCKKSActionModify ckks:self error: &error];
} else {
ckkserror("ckks", self, "processKeychainEventItemAdded given garbage: %@ %@", added, deleted);
return true;
}

CKOperationGroup* operationGroup = [CKOperationGroup CKKSGroupWithName:@"keychain-api-use"];

if(error) {
ckkserror("ckks", self, "Couldn't create outgoing queue entry: %@", error);
self.droppedItems = true;

// If the problem is 'no UUID', launch a scan operation to find and fix it
// We don't want to fix it up here, in the closing moments of a transaction
if([error.domain isEqualToString:CKKSErrorDomain] && error.code == CKKSNoUUIDOnItem) {
ckksnotice("ckks", self, "Launching scan operation to find UUID");
[self scanLocalItems:@"uuid-find-scan" ckoperationGroup:operationGroup after:nil];
}

// If the problem is 'couldn't load key', tell the key hierarchy state machine to fix it
if([error.domain isEqualToString:CKKSErrorDomain] && error.code == errSecItemNotFound) {
[self.pokeKeyStateMachineScheduler trigger];
}

return true;
}

if(rateLimiter) {
NSDate* limit = nil;
NSInteger value = [rateLimiter judge:oqe at:[NSDate date] limitTime:&limit];
if(limit) {
oqe.waitUntil = limit;
SecPLLogRegisteredEvent(@"CKKSSyncing", @{ @"ratelimit" : @(value), @"accessgroup" : oqe.accessgroup});
}
}

[oqe saveToDatabaseWithConnection: dbconn error: &error];
if(error) {
ckkserror("ckks", self, "Couldn't save outgoing queue entry to database: %@", error);
return true;
} else {
ckksnotice("ckks", self, "Saved %@ to outgoing queue", oqe);
}

// This update supercedes all other local modifications to this item (_except_ those in-flight).
// Delete all items in reencrypt or error.
CKKSOutgoingQueueEntry* reencryptOQE = [CKKSOutgoingQueueEntry tryFromDatabase:oqe.uuid state:SecCKKSStateReencrypt zoneID:self.zoneID error:&error];
if(error) {
ckkserror("ckks", self, "Couldn't load reencrypt OQE sibling for %@: %@", oqe, error);
}
if(reencryptOQE) {
[reencryptOQE deleteFromDatabase:&error];
if(error) {
ckkserror("ckks", self, "Couldn't delete reencrypt OQE sibling(%@) for %@: %@", reencryptOQE, oqe, error);
}
error = nil;
}

CKKSOutgoingQueueEntry* errorOQE = [CKKSOutgoingQueueEntry tryFromDatabase:oqe.uuid state:SecCKKSStateError zoneID:self.zoneID error:&error];
if(error) {
ckkserror("ckks", self, "Couldn't load error OQE sibling for %@: %@", oqe, error);
}
if(errorOQE) {
[errorOQE deleteFromDatabase:&error];
if(error) {
ckkserror("ckks", self, "Couldn't delete error OQE sibling(%@) for %@: %@", reencryptOQE, oqe, error);
}
}

[self processOutgoingQueue:operationGroup];

return true;
}];
}

-(void)setCurrentItemForAccessGroup:(NSData* _Nonnull)newItemPersistentRef
hash:(NSData*)newItemSHA1
accessGroup:(NSString*)accessGroup
identifier:(NSString*)identifier
replacing:(NSData* _Nullable)oldCurrentItemPersistentRef
hash:(NSData*)oldItemSHA1
complete:(void (^) (NSError* operror)) complete
{
if(accessGroup == nil || identifier == nil) {
NSError* error = [NSError errorWithDomain:CKKSErrorDomain
code:errSecParam
description:@"No access group or identifier given"];
ckkserror("ckkscurrent", self, "Cancelling request: %@", error);
complete(error);
return;
}

// Not being in a CloudKit account is an automatic failure.
// But, wait a good long while for the CloudKit account state to be known (in the case of daemon startup)
[self.accountStateKnown wait:(SecCKKSTestsEnabled() ? 1*NSEC_PER_SEC : 30*NSEC_PER_SEC)];

if(self.accountStatus != CKKSAccountStatusAvailable) {
NSError* error = [NSError errorWithDomain:CKKSErrorDomain
code:CKKSNotLoggedIn
description:@"User is not signed into iCloud."];
ckksnotice("ckkscurrent", self, "Rejecting current item pointer set since we don't have an iCloud account.");
complete(error);
return;
}

ckksnotice("ckkscurrent", self, "Starting change current pointer operation for %@-%@", accessGroup, identifier);
CKKSUpdateCurrentItemPointerOperation* ucipo = [[CKKSUpdateCurrentItemPointerOperation alloc] initWithCKKSKeychainView:self
newItem:newItemPersistentRef
hash:newItemSHA1
accessGroup:accessGroup
identifier:identifier
replacing:oldCurrentItemPersistentRef
hash:oldItemSHA1
ckoperationGroup:[CKOperationGroup CKKSGroupWithName:@"currentitem-api"]];

__weak __typeof(self) weakSelf = self;
CKKSResultOperation* returnCallback = [CKKSResultOperation operationWithBlock:^{
__strong __typeof(self) strongSelf = weakSelf;

if(ucipo.error) {
ckkserror("ckkscurrent", strongSelf, "Failed setting a current item pointer for %@ with %@", ucipo.currentPointerIdentifier, ucipo.error);
} else {
ckksnotice("ckkscurrent", strongSelf, "Finished setting a current item pointer for %@", ucipo.currentPointerIdentifier);
}
complete(ucipo.error);
}];
returnCallback.name = @"setCurrentItem-return-callback";
[returnCallback addDependency: ucipo];
[self scheduleOperation: returnCallback];

// Now, schedule ucipo. It modifies the CloudKit zone, so it should insert itself into the list of OutgoingQueueOperations.
// Then, we won't have simultaneous zone-modifying operations.
[ucipo linearDependencies:self.outgoingQueueOperations];

// If this operation hasn't started within 60 seconds, cancel it and return a "timed out" error.
[ucipo timeout:60*NSEC_PER_SEC];

[self scheduleOperation:ucipo];
return;
}

-(void)getCurrentItemForAccessGroup:(NSString*)accessGroup
identifier:(NSString*)identifier
fetchCloudValue:(bool)fetchCloudValue
complete:(void (^) (NSString* uuid, NSError* operror)) complete
{
if(accessGroup == nil || identifier == nil) {
ckksnotice("ckkscurrent", self, "Rejecting current item pointer get since no access group(%@) or identifier(%@) given", accessGroup, identifier);
complete(NULL, [NSError errorWithDomain:CKKSErrorDomain
code:errSecParam
description:@"No access group or identifier given"]);
return;
}

// Not being in a CloudKit account is an automatic failure.
// But, wait a good long while for the CloudKit account state to be known (in the case of daemon startup)
[self.accountStateKnown wait:(SecCKKSTestsEnabled() ? 1*NSEC_PER_SEC : 30*NSEC_PER_SEC)];

if(self.accountStatus != CKKSAccountStatusAvailable) {
ckksnotice("ckkscurrent", self, "Rejecting current item pointer get since we don't have an iCloud account.");
complete(NULL, [NSError errorWithDomain:CKKSErrorDomain
code:CKKSNotLoggedIn
description:@"User is not signed into iCloud."]);
return;
}

CKKSResultOperation* fetchAndProcess = nil;
if(fetchCloudValue) {
fetchAndProcess = [self fetchAndProcessCKChanges:CKKSFetchBecauseCurrentItemFetchRequest];
}

__weak __typeof(self) weakSelf = self;
CKKSResultOperation* getCurrentItem = [CKKSResultOperation named:@"get-current-item-pointer" withBlock:^{
if(fetchAndProcess.error) {
ckksnotice("ckkscurrent", self, "Rejecting current item pointer get since fetch failed: %@", fetchAndProcess.error);
complete(NULL, fetchAndProcess.error);
return;
}

__strong __typeof(self) strongSelf = weakSelf;

[strongSelf dispatchSync: ^bool {
NSError* error = nil;
NSString* currentIdentifier = [NSString stringWithFormat:@"%@-%@", accessGroup, identifier];

CKKSCurrentItemPointer* cip = [CKKSCurrentItemPointer fromDatabase:currentIdentifier
state:SecCKKSProcessedStateLocal
zoneID:strongSelf.zoneID
error:&error];
if(!cip || error) {
ckkserror("ckkscurrent", strongSelf, "No current item pointer for %@", currentIdentifier);
complete(nil, error);
return false;
}

if(!cip.currentItemUUID) {
ckkserror("ckkscurrent", strongSelf, "Current item pointer is empty %@", cip);
complete(nil, [NSError errorWithDomain:CKKSErrorDomain
code:errSecInternalError
description:@"Current item pointer is empty"]);
return false;
}

ckksinfo("ckkscurrent", strongSelf, "Retrieved current item pointer: %@", cip);
complete(cip.currentItemUUID, NULL);
return true;
}];
}];

[getCurrentItem addNullableDependency:fetchAndProcess];
[self scheduleOperation: getCurrentItem];
}

- (CKKSKey*) keyForItem: (SecDbItemRef) item error: (NSError * __autoreleasing *) error {
CKKSKeyClass* class = nil;

NSString* protection = (__bridge NSString*)SecDbItemGetCachedValueWithName(item, kSecAttrAccessible);
if([protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleWhenUnlocked]) {
class = SecCKKSKeyClassA;
} else if([protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAlwaysPrivate] ||
[protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAfterFirstUnlock]) {
class = SecCKKSKeyClassC;
} else {
NSError* localError = [NSError errorWithDomain:CKKSErrorDomain
code:CKKSInvalidKeyClass
description:[NSString stringWithFormat:@"can't pick key class for protection %@", protection]];
ckkserror("ckks", self, "can't pick key class: %@ %@", localError, item);
if(error) {
*error = localError;
}

return nil;
}

NSError* currentKeyError = nil;
CKKSKey* key = [CKKSKey currentKeyForClass: class zoneID:self.zoneID error:&currentKeyError];
if(!key || currentKeyError) {
ckkserror("ckks", self, "Couldn't find current key for %@: %@", class, currentKeyError);

if(error) {
*error = currentKeyError;
}
return nil;
}

// and make sure it's unwrapped.
NSError* loadedError = nil;
if(![key ensureKeyLoaded:&loadedError]) {
ckkserror("ckks", self, "Couldn't load key(%@): %@", key, loadedError);
if(error) {
*error = loadedError;
}
return nil;
}

return key;
}

// Use the following method to find the first pending operation in a weak collection
- (NSOperation*)findFirstPendingOperation: (NSHashTable*) table {
return [self findFirstPendingOperation:table ofClass:nil];
}

// Use the following method to find the first pending operation in a weak collection
- (NSOperation*)findFirstPendingOperation: (NSHashTable*) table ofClass:(Class)class {
@synchronized(table) {
for(NSOperation* op in table) {
if(op != nil && [op isPending] && (class == nil || [op isKindOfClass: class])) {
return op;
}
}
return nil;
}
}

// Use the following method to count the pending operations in a weak collection
- (int64_t)countPendingOperations: (NSHashTable*) table {
@synchronized(table) {
int count = 0;
for(NSOperation* op in table) {
if(op != nil && !([op isExecuting] || [op isFinished])) {
count++;
}
}
return count;
}
}

- (CKKSOutgoingQueueOperation*)processOutgoingQueue:(CKOperationGroup*)ckoperationGroup {
return [self processOutgoingQueueAfter:nil ckoperationGroup:ckoperationGroup];
}

- (CKKSOutgoingQueueOperation*)processOutgoingQueueAfter:(CKKSResultOperation*)after ckoperationGroup:(CKOperationGroup*)ckoperationGroup {
CKKSOutgoingQueueOperation* outgoingop =
(CKKSOutgoingQueueOperation*) [self findFirstPendingOperation:self.outgoingQueueOperations
ofClass:[CKKSOutgoingQueueOperation class]];
if(outgoingop) {
if(after) {
[outgoingop addDependency: after];
}
if([outgoingop isPending]) {
if(!outgoingop.ckoperationGroup && ckoperationGroup) {
outgoingop.ckoperationGroup = ckoperationGroup;
} else if(ckoperationGroup) {
ckkserror("ckks", self, "Throwing away CKOperationGroup(%@) in favor of (%@)", ckoperationGroup.name, outgoingop.ckoperationGroup.name);
}

// Will log any pending dependencies as well
ckksnotice("ckksoutgoing", self, "Returning existing %@", outgoingop);

// Shouldn't be necessary, but can't hurt
[self.outgoingQueueOperationScheduler trigger];
return outgoingop;
}
}

CKKSOutgoingQueueOperation* op = [[CKKSOutgoingQueueOperation alloc] initWithCKKSKeychainView:self ckoperationGroup:ckoperationGroup];
op.name = @"outgoing-queue-operation";
[op addNullableDependency:after];
[op addNullableDependency:self.outgoingQueueOperationScheduler.operationDependency];
[self.outgoingQueueOperationScheduler trigger];

[self scheduleOperation: op];
ckksnotice("ckksoutgoing", self, "Scheduled %@", op);
return op;
}

- (void)processIncomingQueueAfterNextUnlock {
// Thread races aren't so important here; we might end up with two or three copies of this operation, but that's okay.
if(![self.processIncomingQueueAfterNextUnlockOperation isPending]) {
__weak __typeof(self) weakSelf = self;

CKKSResultOperation* restartIncomingQueueOperation = [CKKSResultOperation operationWithBlock:^{
__strong __typeof(self) strongSelf = weakSelf;
// This IQO shouldn't error if the keybag has locked again. It will simply try again later.
[strongSelf processIncomingQueue:false];
}];

restartIncomingQueueOperation.name = @"reprocess-incoming-queue-after-unlock";
self.processIncomingQueueAfterNextUnlockOperation = restartIncomingQueueOperation;

[restartIncomingQueueOperation addNullableDependency:self.lockStateTracker.unlockDependency];
[self scheduleOperation: restartIncomingQueueOperation];
}
}

- (CKKSResultOperation*)resultsOfNextProcessIncomingQueueOperation {
if(self.resultsOfNextIncomingQueueOperationOperation && [self.resultsOfNextIncomingQueueOperationOperation isPending]) {
return self.resultsOfNextIncomingQueueOperationOperation;
}

// Else, make a new one.
self.resultsOfNextIncomingQueueOperationOperation = [CKKSResultOperation named:[NSString stringWithFormat:@"wait-for-next-incoming-queue-operation-%@", self.zoneName] withBlock:^{}];
return self.resultsOfNextIncomingQueueOperationOperation;
}

- (CKKSIncomingQueueOperation*)processIncomingQueue:(bool)failOnClassA {
return [self processIncomingQueue:failOnClassA after: nil];
}

- (CKKSIncomingQueueOperation*) processIncomingQueue:(bool)failOnClassA after: (CKKSResultOperation*) after {
CKKSIncomingQueueOperation* incomingop = (CKKSIncomingQueueOperation*) [self findFirstPendingOperation:self.incomingQueueOperations];
if(incomingop) {
ckksinfo("ckks", self, "Skipping processIncomingQueue due to at least one pending instance");
if(after) {
[incomingop addNullableDependency: after];
}

// check (again) for race condition; if the op has started we need to add another (for the dependency)
if([incomingop isPending]) {
incomingop.errorOnClassAFailure |= failOnClassA;
return incomingop;
}
}

CKKSIncomingQueueOperation* op = [[CKKSIncomingQueueOperation alloc] initWithCKKSKeychainView:self errorOnClassAFailure:failOnClassA];
op.name = @"incoming-queue-operation";
if(after != nil) {
[op addSuccessDependency: after];
}

if(self.resultsOfNextIncomingQueueOperationOperation) {
[self.resultsOfNextIncomingQueueOperationOperation addSuccessDependency:op];
[self scheduleOperation:self.resultsOfNextIncomingQueueOperationOperation];
}

[self scheduleOperation: op];
return op;
}

- (CKKSScanLocalItemsOperation*)scanLocalItems:(NSString*)operationName {
return [self scanLocalItems:operationName ckoperationGroup:nil after:nil];
}

- (CKKSScanLocalItemsOperation*)scanLocalItems:(NSString*)operationName ckoperationGroup:(CKOperationGroup*)operationGroup after:(NSOperation*)after {
CKKSScanLocalItemsOperation* scanOperation = [[CKKSScanLocalItemsOperation alloc] initWithCKKSKeychainView:self ckoperationGroup:operationGroup];
scanOperation.name = operationName;

[scanOperation addNullableDependency:self.lastFixupOperation];
[scanOperation addNullableDependency:self.lockStateTracker.unlockDependency];
[scanOperation addNullableDependency:self.keyStateReadyDependency];
[scanOperation addNullableDependency:after];

[self scheduleOperation: scanOperation];
return scanOperation;
}

- (CKKSUpdateDeviceStateOperation*)updateDeviceState:(bool)rateLimit
waitForKeyHierarchyInitialization:(uint64_t)timeout
ckoperationGroup:(CKOperationGroup*)ckoperationGroup {

__weak __typeof(self) weakSelf = self;

// If securityd just started, the key state might be in some transient early state. Wait a bit.
CKKSResultOperation* waitForKeyReady = [CKKSResultOperation named:@"device-state-wait" withBlock:^{
__strong __typeof(self) strongSelf = weakSelf;
ckksnotice("ckksdevice", strongSelf, "Finished waiting for key hierarchy transient state, currently %@", strongSelf.keyHierarchyState);
}];

[waitForKeyReady addNullableDependency:self.keyStateNonTransientDependency];
[waitForKeyReady timeout:timeout];
[self.waitingQueue addOperation:waitForKeyReady];

CKKSUpdateDeviceStateOperation* op = [[CKKSUpdateDeviceStateOperation alloc] initWithCKKSKeychainView:self rateLimit:rateLimit ckoperationGroup:ckoperationGroup];
op.name = @"device-state-operation";

[op addDependency: waitForKeyReady];

// op modifies the CloudKit zone, so it should insert itself into the list of OutgoingQueueOperations.
// Then, we won't have simultaneous zone-modifying operations and confuse ourselves.
// However, since we might have pending OQOs, it should try to insert itself at the beginning of the linearized list
[op linearDependenciesWithSelfFirst:self.outgoingQueueOperations];

// CKKSUpdateDeviceStateOperations are special: they should fire even if we don't believe we're in an iCloud account.
// They also shouldn't block or be blocked by any other operation; our wait operation above will handle that
[self scheduleOperationWithoutDependencies:op];
return op;
}

// There are some errors which won't be reported but will be reflected in the CDSE; any error coming out of here is fatal
- (CKKSDeviceStateEntry*)_onqueueCurrentDeviceStateEntry: (NSError* __autoreleasing*)error {
NSError* localerror = nil;

CKKSCKAccountStateTracker* accountTracker = self.accountTracker;

// We must have an iCloud account (with d2de on) to even create one of these
if(accountTracker.currentCKAccountInfo.accountStatus != CKAccountStatusAvailable || accountTracker.currentCKAccountInfo.supportsDeviceToDeviceEncryption != YES) {
ckkserror("ckksdevice", self, "No iCloud account active: %@", accountTracker.currentCKAccountInfo);
localerror = [NSError errorWithDomain:@"securityd"
code:errSecInternalError
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"No active HSA2 iCloud account: %@", accountTracker.currentCKAccountInfo]}];
if(error) {
*error = localerror;
}
return nil;
}

CKKSDeviceStateEntry* oldcdse = [CKKSDeviceStateEntry tryFromDatabase:accountTracker.ckdeviceID zoneID:self.zoneID error:&localerror];
if(localerror) {
ckkserror("ckksdevice", self, "Couldn't read old CKKSDeviceStateEntry from database: %@", localerror);
if(error) {
*error = localerror;
}
return nil;
}

// Find out what we think the current keys are
CKKSCurrentKeyPointer* currentTLKPointer = [CKKSCurrentKeyPointer tryFromDatabase: SecCKKSKeyClassTLK zoneID:self.zoneID error:&localerror];
CKKSCurrentKeyPointer* currentClassAPointer = [CKKSCurrentKeyPointer tryFromDatabase: SecCKKSKeyClassA zoneID:self.zoneID error:&localerror];
CKKSCurrentKeyPointer* currentClassCPointer = [CKKSCurrentKeyPointer tryFromDatabase: SecCKKSKeyClassC zoneID:self.zoneID error:&localerror];
if(localerror) {
// Things is broken, but the whole point of this record is to share the brokenness. Continue.
ckkserror("ckksdevice", self, "Couldn't read current key pointers from database: %@; proceeding", localerror);
localerror = nil;
}

CKKSKey* suggestedTLK = currentTLKPointer.currentKeyUUID ? [CKKSKey tryFromDatabase:currentTLKPointer.currentKeyUUID zoneID:self.zoneID error:&localerror] : nil;
CKKSKey* suggestedClassAKey = currentClassAPointer.currentKeyUUID ? [CKKSKey tryFromDatabase:currentClassAPointer.currentKeyUUID zoneID:self.zoneID error:&localerror] : nil;
CKKSKey* suggestedClassCKey = currentClassCPointer.currentKeyUUID ? [CKKSKey tryFromDatabase:currentClassCPointer.currentKeyUUID zoneID:self.zoneID error:&localerror] : nil;

if(localerror) {
// Things is broken, but the whole point of this record is to share the brokenness. Continue.
ckkserror("ckksdevice", self, "Couldn't read keys from database: %@; proceeding", localerror);
localerror = nil;
}

// Check if we posess the keys in the keychain
[suggestedTLK ensureKeyLoaded:&localerror];
if(localerror && [self.lockStateTracker isLockedError:localerror]) {
ckkserror("ckksdevice", self, "Device is locked; couldn't read TLK from keychain. Assuming it is present and continuing; error was %@", localerror);
localerror = nil;
} else if(localerror) {
ckkserror("ckksdevice", self, "Couldn't read TLK from keychain. We do not have a current TLK. Error was %@", localerror);
suggestedTLK = nil;
}

[suggestedClassAKey ensureKeyLoaded:&localerror];
if(localerror && [self.lockStateTracker isLockedError:localerror]) {
ckkserror("ckksdevice", self, "Device is locked; couldn't read ClassA key from keychain. Assuming it is present and continuing; error was %@", localerror);
localerror = nil;
} else if(localerror) {
ckkserror("ckksdevice", self, "Couldn't read ClassA key from keychain. We do not have a current ClassA key. Error was %@", localerror);
suggestedClassAKey = nil;
}

[suggestedClassCKey ensureKeyLoaded:&localerror];
// class C keys are stored class C, so uh, don't check lock state.
if(localerror) {
ckkserror("ckksdevice", self, "Couldn't read ClassC key from keychain. We do not have a current ClassC key. Error was %@", localerror);
suggestedClassCKey = nil;
}

// 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
if([accountTracker.accountCirclePeerIDInitialized wait:500*NSEC_PER_MSEC] != 0 && !accountTracker.accountCirclePeerID) {
ckkserror("ckksdevice", self, "No peer ID available");
}

// Reset the last unlock time to 'day' granularity in UTC
NSCalendar* calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierISO8601];
calendar.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
NSDate* lastUnlockDay = self.lockStateTracker.lastUnlockTime;
lastUnlockDay = lastUnlockDay ? [calendar startOfDayForDate:lastUnlockDay] : nil;

// We only really want the oldcdse for its encodedCKRecord, so make a new cdse here
CKKSDeviceStateEntry* newcdse = [[CKKSDeviceStateEntry alloc] initForDevice:accountTracker.ckdeviceID
osVersion:SecCKKSHostOSVersion()
lastUnlockTime:lastUnlockDay
octagonPeerID:nil
octagonStatus:nil
circlePeerID:accountTracker.accountCirclePeerID
circleStatus:accountTracker.currentCircleStatus.status
keyState:self.keyHierarchyState
currentTLKUUID:suggestedTLK.uuid
currentClassAUUID:suggestedClassAKey.uuid
currentClassCUUID:suggestedClassCKey.uuid
zoneID:self.zoneID
encodedCKRecord:oldcdse.encodedCKRecord];
return newcdse;
}

- (CKKSSynchronizeOperation*) resyncWithCloud {
CKKSSynchronizeOperation* op = [[CKKSSynchronizeOperation alloc] initWithCKKSKeychainView: self];
[self scheduleOperation: op];
return op;
}

- (CKKSLocalSynchronizeOperation*)resyncLocal {
CKKSLocalSynchronizeOperation* op = [[CKKSLocalSynchronizeOperation alloc] initWithCKKSKeychainView:self];
[self scheduleOperation: op];
return op;
}

- (CKKSResultOperation*)fetchAndProcessCKChanges:(CKKSFetchBecause*)because {
return [self fetchAndProcessCKChanges:because after:nil];
}

- (CKKSResultOperation*)fetchAndProcessCKChanges:(CKKSFetchBecause*)because after:(CKKSResultOperation*)after {
if(!SecCKKSIsEnabled()) {
ckksinfo("ckks", self, "Skipping fetchAndProcessCKChanges due to disabled CKKS");
return nil;
}

if(after) {
[self.zoneChangeFetcher holdFetchesUntil:after];
}

// We fetched some changes; try to process them!
return [self processIncomingQueue:false after:[self.zoneChangeFetcher requestSuccessfulFetch:because]];
}

- (CKKSResultOperation*)fetchAndProcessCKChangesDueToAPNS:(CKRecordZoneNotification*)notification {
if(!SecCKKSIsEnabled()) {
ckksinfo("ckks", self, "Skipping fetchAndProcessCKChanges due to disabled CKKS");
return nil;
}

// We fetched some changes; try to process them!
return [self processIncomingQueue:false after:[self.zoneChangeFetcher requestSuccessfulFetchDueToAPNS:notification]];
}

// Lets the view know about a failed CloudKit write. If the error is "already have one of these records", it will
// store the new records and kick off the new processing
//
// Note that you need to tell this function the records you wanted to save, so it can determine what needs deletion
- (bool)_onqueueCKWriteFailed:(NSError*)ckerror attemptedRecordsChanged:(NSDictionary<CKRecordID*, CKRecord*>*)savedRecords {
dispatch_assert_queue(self.queue);

NSDictionary<CKRecordID*,NSError*>* partialErrors = ckerror.userInfo[CKPartialErrorsByItemIDKey];
if([ckerror.domain isEqual:CKErrorDomain] && ckerror.code == CKErrorPartialFailure && partialErrors) {
// Check if this error was "you're out of date"
bool recordChanged = true;

for(NSError* error in partialErrors.allValues) {
if((![error.domain isEqual:CKErrorDomain]) || (error.code != CKErrorBatchRequestFailed && error.code != CKErrorServerRecordChanged && error.code != CKErrorUnknownItem)) {
// There's an error in there that isn't CKErrorServerRecordChanged, CKErrorBatchRequestFailed, or CKErrorUnknownItem. Don't handle nicely...
recordChanged = false;
}
}

if(recordChanged) {
ckksnotice("ckks", self, "Received a ServerRecordChanged error, attempting to update new records and delete unknown ones");

bool updatedRecord = false;

for(CKRecordID* recordID in partialErrors.allKeys) {
NSError* error = partialErrors[recordID];
if([error.domain isEqual:CKErrorDomain] && error.code == CKErrorServerRecordChanged) {
CKRecord* newRecord = error.userInfo[CKRecordChangedErrorServerRecordKey];
ckksnotice("ckks", self, "On error: updating our idea of: %@", newRecord);

updatedRecord |= [self _onqueueCKRecordChanged:newRecord resync:true];
} else if([error.domain isEqual:CKErrorDomain] && error.code == CKErrorUnknownItem) {
CKRecord* record = savedRecords[recordID];
ckksnotice("ckks", self, "On error: handling an unexpected delete of: %@ %@", recordID, record);

updatedRecord |= [self _onqueueCKRecordDeleted:recordID recordType:record.recordType resync:true];
}
}

if(updatedRecord) {
[self processIncomingQueue:false];
return true;
}
}

// Check if this error was the CKKS server extension rejecting the write
for(CKRecordID* recordID in partialErrors.allKeys) {
NSError* error = partialErrors[recordID];

NSError* underlyingError = error.userInfo[NSUnderlyingErrorKey];
NSError* thirdLevelError = underlyingError.userInfo[NSUnderlyingErrorKey];
ckksnotice("ckks", self, "Examining 'write failed' error: %@ %@ %@", error, underlyingError, thirdLevelError);

if([error.domain isEqualToString:CKErrorDomain] && error.code == CKErrorServerRejectedRequest &&
underlyingError && [underlyingError.domain isEqualToString:CKInternalErrorDomain] && underlyingError.code == CKErrorInternalPluginError &&
thirdLevelError && [thirdLevelError.domain isEqualToString:@"CloudkitKeychainService"]) {

if(thirdLevelError.code == CKKSServerUnexpectedSyncKeyInChain) {
// The server thinks the classA/C synckeys don't wrap directly the to top TLK, but we don't (otherwise, we would have fixed it).
// Issue a key hierarchy fetch and see what's what.
ckkserror("ckks", self, "CKKS Server extension has told us about %@ for record %@; requesting refetch and reprocess of key hierarchy", thirdLevelError, recordID);
[self _onqueueKeyStateMachineRequestFetch];
} else {
ckkserror("ckks", self, "CKKS Server extension has told us about %@ for record %@, but we don't currently handle this error", thirdLevelError, recordID);
}
}
}
}

return false;
}

- (bool)_onqueueCKRecordDeleted:(CKRecordID*)recordID recordType:(NSString*)recordType resync:(bool)resync {
dispatch_assert_queue(self.queue);

// TODO: resync doesn't really mean much here; what does it mean for a record to be 'deleted' if you're fetching from scratch?

if([recordType isEqual: SecCKRecordItemType]) {
ckksinfo("ckks", self, "CloudKit notification: deleted record(%@): %@", recordType, recordID);
NSError* error = nil;
NSError* iqeerror = nil;
CKKSMirrorEntry* ckme = [CKKSMirrorEntry fromDatabase: [recordID recordName] zoneID:self.zoneID error: &error];

// Deletes always succeed, not matter the generation count
if(ckme) {
[ckme deleteFromDatabase:&error];

CKKSIncomingQueueEntry* iqe = [[CKKSIncomingQueueEntry alloc] initWithCKKSItem:ckme.item action:SecCKKSActionDelete state:SecCKKSStateNew];
[iqe saveToDatabase:&iqeerror];
if(iqeerror) {
ckkserror("ckks", self, "Couldn't save incoming queue entry: %@", iqeerror);
}
}
ckksinfo("ckks", self, "CKKSMirrorEntry was deleted: %@ %@ error: %@", recordID, ckme, error);
// TODO: actually pass error back up
return (error == nil);

} else if([recordType isEqual: SecCKRecordCurrentItemType]) {
ckksinfo("ckks", self, "CloudKit notification: deleted current item pointer(%@): %@", recordType, recordID);
NSError* error = nil;

[[CKKSCurrentItemPointer tryFromDatabase:[recordID recordName] state:SecCKKSProcessedStateRemote zoneID:self.zoneID error:&error] deleteFromDatabase:&error];
[[CKKSCurrentItemPointer fromDatabase:[recordID recordName] state:SecCKKSProcessedStateLocal zoneID:self.zoneID error:&error] deleteFromDatabase:&error];

ckksinfo("ckks", self, "CKKSCurrentItemPointer was deleted: %@ error: %@", recordID, error);
return (error == nil);

} else if([recordType isEqual: SecCKRecordIntermediateKeyType]) {
// TODO: handle in some interesting way
return true;
} else if([recordType isEqual: SecCKRecordTLKShareType]) {
NSError* error = nil;
ckksinfo("ckks", self, "CloudKit notification: deleted tlk share record(%@): %@", recordType, recordID);
CKKSTLKShare* share = [CKKSTLKShare tryFromDatabaseFromCKRecordID:recordID error:&error];
[share deleteFromDatabase:&error];

if(error) {
ckkserror("ckks", self, "CK notification: Couldn't delete deleted TLKShare: %@ %@", recordID, error);
}
return (error == nil);

} else if([recordType isEqual: SecCKRecordDeviceStateType]) {
NSError* error = nil;
ckksinfo("ckks", self, "CloudKit notification: deleted device state record(%@): %@", recordType, recordID);

CKKSDeviceStateEntry* cdse = [CKKSDeviceStateEntry tryFromDatabaseFromCKRecordID:recordID error:&error];
[cdse deleteFromDatabase: &error];
ckksinfo("ckks", self, "CKKSCurrentItemPointer(%@) was deleted: %@ error: %@", cdse, recordID, error);

return (error == nil);

} else if ([recordType isEqualToString:SecCKRecordManifestType]) {
ckksinfo("ckks", self, "CloudKit notification: deleted manifest record (%@): %@", recordType, recordID);

NSError* error = nil;
CKKSManifest* manifest = [CKKSManifest manifestForRecordName:recordID.recordName error:&error];
if (manifest) {
[manifest deleteFromDatabase:&error];
}

ckksinfo("ckks", self, "CKKSManifest was deleted: %@ %@ error: %@", recordID, manifest, error);
// TODO: actually pass error back up
return error == nil;
}

else {
ckkserror("ckksfetch", self, "unknown record type: %@ %@", recordType, recordID);
return false;
}
}

- (bool)_onqueueCKRecordChanged:(CKRecord*)record resync:(bool)resync {
dispatch_assert_queue(self.queue);

@autoreleasepool {
ckksnotice("ckksfetch", self, "Processing record modification(%@): %@", record.recordType, record);

if([[record recordType] isEqual: SecCKRecordItemType]) {
[self _onqueueCKRecordItemChanged:record resync:resync];
return true;
} else if([[record recordType] isEqual: SecCKRecordCurrentItemType]) {
[self _onqueueCKRecordCurrentItemPointerChanged:record resync:resync];
return true;
} else if([[record recordType] isEqual: SecCKRecordIntermediateKeyType]) {
[self _onqueueCKRecordKeyChanged:record resync:resync];
return true;
} else if ([[record recordType] isEqual: SecCKRecordTLKShareType]) {
[self _onqueueCKRecordTLKShareChanged:record resync:resync];
return true;
} else if([[record recordType] isEqualToString: SecCKRecordCurrentKeyType]) {
[self _onqueueCKRecordCurrentKeyPointerChanged:record resync:resync];
return true;
} else if ([[record recordType] isEqualToString:SecCKRecordManifestType]) {
[self _onqueueCKRecordManifestChanged:record resync:resync];
return true;
} else if ([[record recordType] isEqualToString:SecCKRecordManifestLeafType]) {
[self _onqueueCKRecordManifestLeafChanged:record resync:resync];
return true;
} else if ([[record recordType] isEqualToString:SecCKRecordDeviceStateType]) {
[self _onqueueCKRecordDeviceStateChanged:record resync:resync];
return true;
} else {
ckkserror("ckksfetch", self, "unknown record type: %@ %@", [record recordType], record);
return false;
}
}
}

- (void)_onqueueCKRecordItemChanged:(CKRecord*)record resync:(bool)resync {
dispatch_assert_queue(self.queue);

NSError* error = nil;
// Find if we knew about this record in the past
bool update = false;
CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase: [[record recordID] recordName] zoneID:self.zoneID error:&error];

if(error) {
ckkserror("ckks", self, "error loading a CKKSMirrorEntry from database: %@", error);
// TODO: quit?
}

if(resync) {
if(!ckme) {
ckkserror("ckksresync", self, "BUG: No local item matching resynced CloudKit record: %@", record);
} else if(![ckme matchesCKRecord:record]) {
ckkserror("ckksresync", self, "BUG: Local item doesn't match resynced CloudKit record: %@ %@", ckme, record);
} else {
ckksnotice("ckksresync", self, "Already know about this item record, updating anyway: %@", record.recordID);
}
}

if(ckme && ckme.item && ckme.item.generationCount > [record[SecCKRecordGenerationCountKey] unsignedLongLongValue]) {
ckkserror("ckks", self, "received a record from CloudKit with a bad generation count: %@ (%ld > %@)", ckme.uuid,
(long) ckme.item.generationCount,
record[SecCKRecordGenerationCountKey]);
// Abort processing this record.
return;
}

// If we found an old version in the database; this might be an update
if(ckme) {
if([ckme matchesCKRecord:record] && !resync) {
// This is almost certainly a record we uploaded; CKFetchChanges sends them back as new records
ckksnotice("ckks", self, "CloudKit has told us of record we already know about; skipping update");
return;
}

update = true;
// Set the CKKSMirrorEntry's fields to be whatever this record holds
[ckme setFromCKRecord: record];
} else {
// Have to make a new CKKSMirrorEntry
ckme = [[CKKSMirrorEntry alloc] initWithCKRecord: record];
}

[ckme saveToDatabase: &error];

if(error) {
ckkserror("ckks", self, "couldn't save new CKRecord to database: %@ %@", record, error);
} else {
ckksdebug("ckks", self, "CKKSMirrorEntry was created: %@", ckme);
}

NSError* iqeerror = nil;
CKKSIncomingQueueEntry* iqe = [[CKKSIncomingQueueEntry alloc] initWithCKKSItem:ckme.item
action:(update ? SecCKKSActionModify : SecCKKSActionAdd)
state:SecCKKSStateNew];
[iqe saveToDatabase:&iqeerror];
if(iqeerror) {
ckkserror("ckks", self, "Couldn't save modified incoming queue entry: %@", iqeerror);
} else {
ckksdebug("ckks", self, "CKKSIncomingQueueEntry was created: %@", iqe);
}

// A remote change has occured for this record. Delete any pending local changes; they will be overwritten.
CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry tryFromDatabase:ckme.uuid state: SecCKKSStateNew zoneID:self.zoneID error: &error];
if(error) {
ckkserror("ckks", self, "Couldn't load OutgoingQueueEntry: %@", error);
}
if(oqe) {
[self _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateDeleted error:&error];
}

// Reencryptions are pending changes too
oqe = [CKKSOutgoingQueueEntry tryFromDatabase:ckme.uuid state: SecCKKSStateReencrypt zoneID:self.zoneID error: &error];
if(error) {
ckkserror("ckks", self, "Couldn't load reencrypted OutgoingQueueEntry: %@", error);
}
if(oqe) {
[oqe deleteFromDatabase:&error];
if(error) {
ckkserror("ckks", self, "Couldn't delete reencrypted oqe(%@): %@", oqe, error);
}
}
}

- (void)_onqueueCKRecordKeyChanged:(CKRecord*)record resync:(bool)resync {
dispatch_assert_queue(self.queue);

NSError* error = nil;

if(resync) {
NSError* resyncerror = nil;

CKKSKey* key = [CKKSKey tryFromDatabaseAnyState:record.recordID.recordName zoneID:self.zoneID error:&resyncerror];
if(resyncerror) {
ckkserror("ckksresync", self, "error loading key: %@", resyncerror);
}
if(!key) {
ckkserror("ckksresync", self, "BUG: No sync key matching resynced CloudKit record: %@", record);
} else if(![key matchesCKRecord:record]) {
ckkserror("ckksresync", self, "BUG: Local sync key doesn't match resynced CloudKit record(s): %@ %@", key, record);
} else {
ckksnotice("ckksresync", self, "Already know about this sync key, skipping update: %@", record);
return;
}
}

CKKSKey* remotekey = [[CKKSKey alloc] initWithCKRecord: record];

// Do we already know about this key?
CKKSKey* possibleLocalKey = [CKKSKey tryFromDatabase:remotekey.uuid zoneID:self.zoneID error:&error];
if(error) {
ckkserror("ckkskey", self, "Error findibg exsiting local key for %@: %@", remotekey, error);
// Go on, assuming there isn't a local key
} else if(possibleLocalKey && [possibleLocalKey matchesCKRecord:record]) {
// Okay, nothing new here. Update the CKRecord and move on.
// Note: If the new record doesn't match the local copy, we have to go through the whole dance below
possibleLocalKey.storedCKRecord = record;
[possibleLocalKey saveToDatabase:&error];

if(error) {
ckkserror("ckkskey", self, "Couldn't update existing key: %@: %@", possibleLocalKey, error);
}
return;
}

// Drop into the synckeys table as a 'remote' key, then ask for a rekey operation.
remotekey.state = SecCKKSProcessedStateRemote;
remotekey.currentkey = false;

[remotekey saveToDatabase:&error];
if(error) {
ckkserror("ckkskey", self, "Couldn't save key record to database: %@: %@", remotekey, error);
ckksinfo("ckkskey", self, "CKRecord was %@", record);
}

// We've saved a new key in the database; trigger a rekey operation.
[self _onqueueKeyStateMachineRequestProcess];
}

- (void)_onqueueCKRecordTLKShareChanged:(CKRecord*)record resync:(bool)resync {
dispatch_assert_queue(self.queue);

NSError* error = nil;
if(resync) {
// TODO fill in
}

// CKKSTLKShares get saved with no modification
CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:record];
[share saveToDatabase:&error];
if(error) {
ckkserror("ckksshare", self, "Couldn't save new TLK share to database: %@ %@", share, error);
}

[self _onqueueKeyStateMachineRequestProcess];
}

- (void)_onqueueCKRecordCurrentKeyPointerChanged:(CKRecord*)record resync:(bool)resync {
dispatch_assert_queue(self.queue);

// Pull out the old CKP, if it exists
NSError* ckperror = nil;
CKKSCurrentKeyPointer* oldckp = [CKKSCurrentKeyPointer tryFromDatabase:((CKKSKeyClass*) record.recordID.recordName) zoneID:self.zoneID error:&ckperror];
if(ckperror) {
ckkserror("ckkskey", self, "error loading ckp: %@", ckperror);
}

if(resync) {
if(!oldckp) {
ckkserror("ckksresync", self, "BUG: No current key pointer matching resynced CloudKit record: %@", record);
} else if(![oldckp matchesCKRecord:record]) {
ckkserror("ckksresync", self, "BUG: Local current key pointer doesn't match resynced CloudKit record: %@ %@", oldckp, record);
} else {
ckksnotice("ckksresync", self, "Current key pointer has 'changed', but it matches our local copy: %@", record);
}
}

NSError* error = nil;
CKKSCurrentKeyPointer* currentkey = [[CKKSCurrentKeyPointer alloc] initWithCKRecord: record];

[currentkey saveToDatabase: &error];
if(error) {
ckkserror("ckkskey", self, "Couldn't save current key pointer to database: %@: %@", currentkey, error);
ckksinfo("ckkskey", self, "CKRecord was %@", record);
}

if([oldckp matchesCKRecord:record]) {
ckksnotice("ckkskey", self, "Current key pointer modification doesn't change anything interesting; skipping reprocess: %@", record);
} else {
// We've saved a new key in the database; trigger a rekey operation.
[self _onqueueKeyStateMachineRequestProcess];
}
}

- (void)_onqueueCKRecordCurrentItemPointerChanged:(CKRecord*)record resync:(bool)resync {
dispatch_assert_queue(self.queue);

if(resync) {
NSError* ciperror = nil;
CKKSCurrentItemPointer* localcip = [CKKSCurrentItemPointer tryFromDatabase:record.recordID.recordName state:SecCKKSProcessedStateLocal zoneID:self.zoneID error:&ciperror];
CKKSCurrentItemPointer* remotecip = [CKKSCurrentItemPointer tryFromDatabase:record.recordID.recordName state:SecCKKSProcessedStateRemote zoneID:self.zoneID error:&ciperror];
if(ciperror) {
ckkserror("ckksresync", self, "error loading cip: %@", ciperror);
}
if(!(localcip || remotecip)) {
ckkserror("ckksresync", self, "BUG: No current item pointer matching resynced CloudKit record: %@", record);
} else if(! ([localcip matchesCKRecord:record] || [remotecip matchesCKRecord:record]) ) {
ckkserror("ckksresync", self, "BUG: Local current item pointer doesn't match resynced CloudKit record(s): %@ %@ %@", localcip, remotecip, record);
} else {
ckksnotice("ckksresync", self, "Already know about this current item pointer, skipping update: %@", record);
return;
}
}

NSError* error = nil;
CKKSCurrentItemPointer* cip = [[CKKSCurrentItemPointer alloc] initWithCKRecord: record];
cip.state = SecCKKSProcessedStateRemote;

[cip saveToDatabase: &error];
if(error) {
ckkserror("currentitem", self, "Couldn't save current item pointer to database: %@: %@ %@", cip, error, record);
}
}

- (void)_onqueueCKRecordManifestChanged:(CKRecord*)record resync:(bool)resync
{
NSError* error = nil;
CKKSPendingManifest* manifest = [[CKKSPendingManifest alloc] initWithCKRecord:record];
[manifest saveToDatabase:&error];
if (error) {
ckkserror("CKKS", self, "Failed to save fetched manifest record to database: %@: %@", manifest, error);
ckksinfo("CKKS", self, "manifest CKRecord was %@", record);
}
}

- (void)_onqueueCKRecordManifestLeafChanged:(CKRecord*)record resync:(bool)resync
{
NSError* error = nil;
CKKSManifestLeafRecord* manifestLeaf = [[CKKSManifestPendingLeafRecord alloc] initWithCKRecord:record];
[manifestLeaf saveToDatabase:&error];
if (error) {
ckkserror("CKKS", self, "Failed to save fetched manifest leaf record to database: %@: %@", manifestLeaf, error);
ckksinfo("CKKS", self, "manifest leaf CKRecord was %@", record);
}
}

- (void)_onqueueCKRecordDeviceStateChanged:(CKRecord*)record resync:(bool)resync {
if(resync) {
NSError* dserror = nil;
CKKSDeviceStateEntry* cdse = [CKKSDeviceStateEntry tryFromDatabase:record.recordID.recordName zoneID:self.zoneID error:&dserror];
if(dserror) {
ckkserror("ckksresync", self, "error loading cdse: %@", dserror);
}
if(!cdse) {
ckkserror("ckksresync", self, "BUG: No current device state entry matching resynced CloudKit record: %@", record);
} else if(![cdse matchesCKRecord:record]) {
ckkserror("ckksresync", self, "BUG: Local current device state entry doesn't match resynced CloudKit record(s): %@ %@", cdse, record);
} else {
ckksnotice("ckksresync", self, "Already know about this current item pointer, skipping update: %@", record);
return;
}
}

NSError* error = nil;
CKKSDeviceStateEntry* cdse = [[CKKSDeviceStateEntry alloc] initWithCKRecord:record];
[cdse saveToDatabase:&error];
if (error) {
ckkserror("ckksdevice", self, "Failed to save device record to database: %@: %@ %@", cdse, error, record);
}
}

- (bool)_onqueueResetAllInflightOQE:(NSError**)error {
NSError* localError = nil;

while(true) {
NSArray<CKKSOutgoingQueueEntry*> * inflightQueueEntries = [CKKSOutgoingQueueEntry fetch:SecCKKSOutgoingQueueItemsAtOnce
state:SecCKKSStateInFlight
zoneID:self.zoneID
error:&localError];

if(localError != nil) {
ckkserror("ckks", self, "Error finding inflight outgoing queue records: %@", localError);
if(error) {
*error = localError;
}
return false;
}

if([inflightQueueEntries count] == 0u) {
break;
}

for(CKKSOutgoingQueueEntry* oqe in inflightQueueEntries) {
[self _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateNew error:&localError];

if(localError) {
ckkserror("ckks", self, "Error fixing up inflight OQE(%@): %@", oqe, localError);
if(error) {
*error = localError;
}
return false;
}
}
}

return true;
}

- (bool)_onqueueChangeOutgoingQueueEntry: (CKKSOutgoingQueueEntry*) oqe toState: (NSString*) state error: (NSError* __autoreleasing*) error {
dispatch_assert_queue(self.queue);

NSError* localerror = nil;

if([state isEqualToString: SecCKKSStateDeleted]) {
// Hurray, this must be a success
SecBoolNSErrorCallback callback = self.pendingSyncCallbacks[oqe.uuid];
if(callback) {
callback(true, nil);
self.pendingSyncCallbacks[oqe.uuid] = nil;
}

[oqe deleteFromDatabase: &localerror];
if(localerror) {
ckkserror("ckks", self, "Couldn't delete %@: %@", oqe, localerror);
}

} else if([oqe.state isEqualToString:SecCKKSStateInFlight] && [state isEqualToString:SecCKKSStateNew]) {
// An in-flight OQE is moving to new? See if it's been superceded
CKKSOutgoingQueueEntry* newOQE = [CKKSOutgoingQueueEntry tryFromDatabase:oqe.uuid state:SecCKKSStateNew zoneID:self.zoneID error:&localerror];
if(localerror) {
ckkserror("ckksoutgoing", self, "Couldn't fetch an overwriting OQE, assuming one doesn't exist: %@", localerror);
newOQE = nil;
}

if(newOQE) {
ckksnotice("ckksoutgoing", self, "New modification has come in behind inflight %@; dropping failed change", oqe);
// recurse for that lovely code reuse
[self _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateDeleted error:&localerror];
if(localerror) {
ckkserror("ckksoutgoing", self, "Couldn't delete in-flight OQE: %@", localerror);
if(error) {
*error = localerror;
}
}
} else {
oqe.state = state;
[oqe saveToDatabase: &localerror];
if(localerror) {
ckkserror("ckks", self, "Couldn't save %@ as %@: %@", oqe, state, localerror);
}
}

} else {
oqe.state = state;
[oqe saveToDatabase: &localerror];
if(localerror) {
ckkserror("ckks", self, "Couldn't save %@ as %@: %@", oqe, state, localerror);
}
}

if(error && localerror) {
*error = localerror;
}
return localerror == nil;
}

- (bool)_onqueueErrorOutgoingQueueEntry: (CKKSOutgoingQueueEntry*) oqe itemError: (NSError*) itemError error: (NSError* __autoreleasing*) error {
dispatch_assert_queue(self.queue);

SecBoolNSErrorCallback callback = self.pendingSyncCallbacks[oqe.uuid];
if(callback) {
callback(false, itemError);
self.pendingSyncCallbacks[oqe.uuid] = nil;
}
NSError* localerror = nil;

// Now, delete the OQE: it's never coming back
[oqe deleteFromDatabase:&localerror];
if(localerror) {
ckkserror("ckks", self, "Couldn't delete %@ (due to error %@): %@", oqe, itemError, localerror);
}

if(error && localerror) {
*error = localerror;
}
return localerror == nil;
}

- (bool)_onqueueUpdateLatestManifestWithError:(NSError**)error
{
dispatch_assert_queue(self.queue);
CKKSManifest* manifest = [CKKSManifest latestTrustedManifestForZone:self.zoneName error:error];
if (manifest) {
self.latestManifest = manifest;
return true;
}
else {
return false;
}
}

- (bool)_onqueueWithAccountKeysCheckTLK:(CKKSKey*)proposedTLK error:(NSError* __autoreleasing *)error {
dispatch_assert_queue(self.queue);
// First, if we have a local identity, check for any TLK shares
NSError* localerror = nil;

if(![proposedTLK wrapsSelf]) {
ckkserror("ckksshare", self, "Potential TLK %@ does not wrap self; skipping TLK share checking", proposedTLK);
} else {
bool tlkShares = [self _onqueueWithAccountKeysCheckTLKFromShares:proposedTLK error:&localerror];
// We only want to error out if a positive error occurred. "No shares" is okay.
if(!tlkShares || localerror) {
bool noTrustedTLKShares = [localerror.domain isEqualToString:CKKSErrorDomain] && localerror.code == CKKSNoTrustedTLKShares;
bool noSelfPeer = [localerror.domain isEqualToString:CKKSErrorDomain] && localerror.code == CKKSNoEncryptionKey;

// If this error was something worse than 'couldn't unwrap for reasons including there not being data', report it
if(!(noTrustedTLKShares || noSelfPeer)) {
if(error) {
*error = localerror;
}
ckkserror("ckksshare", self, "Errored unwrapping TLK with TLKShares: %@", localerror);
return false;
} else {
ckkserror("ckksshare", self, "Non-fatal error unwrapping TLK with TLKShares: %@", localerror);
}
}
}

if([proposedTLK loadKeyMaterialFromKeychain:error]) {
// Hurray!
return true;
} else {
return false;
}
}

// This version only examines if this TLK is recoverable from TLK shares
- (bool)_onqueueWithAccountKeysCheckTLKFromShares:(CKKSKey*)proposedTLK error:(NSError* __autoreleasing *)error {
NSError* localerror = NULL;
if(!self.currentSelfPeers.currentSelf || self.currentSelfPeersError) {
ckkserror("ckksshare", self, "Couldn't fetch self peers: %@", self.currentSelfPeersError);
if(error) {
if([self.lockStateTracker isLockedError:self.currentSelfPeersError]) {
// Locked error should propagate
*error = self.currentSelfPeersError;
} else {
*error = [NSError errorWithDomain:CKKSErrorDomain
code:CKKSNoEncryptionKey
description:@"No current self peer"
underlying:self.currentSelfPeersError];
}
}
return false;
}

if(!self.currentTrustedPeers || self.currentTrustedPeersError) {
ckkserror("ckksshare", self, "Couldn't fetch trusted peers: %@", self.currentTrustedPeersError);
if(error) {
*error = [NSError errorWithDomain:CKKSErrorDomain
code:CKKSNoPeersAvailable
description:@"No trusted peers"
underlying:self.currentTrustedPeersError];
}
return false;
}

NSError* lastShareError = nil;

for(id<CKKSSelfPeer> selfPeer in self.currentSelfPeers.allSelves) {
NSArray<CKKSTLKShare*>* possibleShares = [CKKSTLKShare allFor:selfPeer.peerID
keyUUID:proposedTLK.uuid
zoneID:self.zoneID
error:&localerror];
if(localerror) {
ckkserror("ckksshare", self, "Error fetching CKKSTLKShares for %@: %@", selfPeer, localerror);
}

if(possibleShares.count == 0) {
ckksnotice("ckksshare", self, "No CKKSTLKShares to %@ for %@", selfPeer, proposedTLK);
continue;
}

for(CKKSTLKShare* possibleShare in possibleShares) {
NSError* possibleShareError = nil;
ckksnotice("ckksshare", self, "Checking possible TLK share %@ as %@", possibleShare, selfPeer);

CKKSKey* possibleKey = [possibleShare recoverTLK:selfPeer
trustedPeers:self.currentTrustedPeers
error:&possibleShareError];

if(possibleShareError) {
ckkserror("ckksshare", self, "Unable to unwrap TLKShare(%@) as %@: %@",
possibleShare, selfPeer, possibleShareError);
ckkserror("ckksshare", self, "Current trust set: %@", self.currentTrustedPeers);
lastShareError = possibleShareError;
continue;
}

bool result = [proposedTLK trySelfWrappedKeyCandidate:possibleKey.aessivkey error:&possibleShareError];
if(possibleShareError) {
ckkserror("ckksshare", self, "Unwrapped TLKShare(%@) does not unwrap proposed TLK(%@) as %@: %@",
possibleShare, proposedTLK, self.currentSelfPeers.currentSelf, possibleShareError);
lastShareError = possibleShareError;
continue;
}

if(result) {
ckksnotice("ckksshare", self, "TLKShare(%@) unlocked TLK(%@) as %@",
possibleShare, proposedTLK, selfPeer);

// The proposed TLK is trusted key material. Persist it as a "trusted" key.
[proposedTLK saveKeyMaterialToKeychain:true error:&possibleShareError];
if(possibleShareError) {
ckkserror("ckksshare", self, "Couldn't store the new TLK(%@) to the keychain: %@", proposedTLK, possibleShareError);
if(error) {
*error = possibleShareError;
}
return false;
}

return true;
}
}
}

if(error) {
*error = [NSError errorWithDomain:CKKSErrorDomain
code:CKKSNoTrustedTLKShares
description:[NSString stringWithFormat:@"No trusted TLKShares for %@", proposedTLK]
underlying:lastShareError];
}
return false;
}

- (bool)dispatchSyncWithConnection:(SecDbConnectionRef _Nonnull)dbconn block:(bool (^)(void))block {
CFErrorRef cferror = NULL;

// Take the DB transaction, then get on the local queue.
// In the case of exclusive DB transactions, we don't really _need_ the local queue, but, it's here for future use.
bool ret = kc_transaction_type(dbconn, kSecDbExclusiveRemoteCKKSTransactionType, &cferror, ^bool{
__block bool ok = false;

dispatch_sync(self.queue, ^{
ok = block();
});

return ok;
});

if(cferror) {
ckkserror("ckks", self, "error doing database transaction, major problems ahead: %@", cferror);
}
return ret;
}

- (void)dispatchSync: (bool (^)(void)) block {
// important enough to block this thread. Must get a connection first, though!

// Please don't jetsam us...
os_transaction_t transaction = os_transaction_create([[NSString stringWithFormat:@"com.apple.securityd.ckks.%@", self.zoneName] UTF8String]);

CFErrorRef cferror = NULL;
kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
return [self dispatchSyncWithConnection:dbt block:block];
});
if(cferror) {
ckkserror("ckks", self, "error getting database connection, major problems ahead: %@", cferror);
}

(void)transaction;
}

- (void)dispatchSyncWithAccountKeys:(bool (^)(void))block
{
[SOSAccount performOnQuietAccountQueue: ^{
NSError* selfPeersError = nil;
CKKSSelves* currentSelfPeers = [self.currentPeerProvider fetchSelfPeers:&selfPeersError];

NSError* trustedPeersError = nil;
NSSet<id<CKKSPeer>>* currentTrustedPeers = [self.currentPeerProvider fetchTrustedPeers:&trustedPeersError];

[self dispatchSync:^bool{
self.currentSelfPeers = currentSelfPeers;
self.currentSelfPeersError = selfPeersError;

self.currentTrustedPeers = currentTrustedPeers;
self.currentTrustedPeersError = trustedPeersError;

__block bool result = false;
[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
result = block();
}];

// Forget the peers; they might have class A key material
self.currentSelfPeers = nil;
self.currentSelfPeersError = [NSError errorWithDomain:CKKSErrorDomain code:CKKSNoPeersAvailable description:@"No current self peer available"];
self.currentTrustedPeers = nil;
self.currentTrustedPeersError = [NSError errorWithDomain:CKKSErrorDomain code:CKKSNoPeersAvailable description:@"No current trusted peers available"];

return result;
}];
}];
}

#pragma mark - CKKSZoneUpdateReceiver

- (void)notifyZoneChange: (CKRecordZoneNotification*) notification {
ckksnotice("ckks", self, "received a zone change notification for %@ %@", self, notification);

[self fetchAndProcessCKChangesDueToAPNS:notification];
}

- (void)superHandleCKLogin {
[super handleCKLogin];
}

- (void)handleCKLogin {
ckksnotice("ckks", self, "received a notification of CK login");
if(!SecCKKSIsEnabled()) {
ckksnotice("ckks", self, "Skipping CloudKit initialization due to disabled CKKS");
return;
}

__weak __typeof(self) weakSelf = self;
CKKSResultOperation* login = [CKKSResultOperation named:@"ckks-login" withBlock:^{
__strong __typeof(self) strongSelf = weakSelf;

[strongSelf dispatchSyncWithAccountKeys:^bool{
[strongSelf superHandleCKLogin];

// Reset key hierarchy state machine to initializing
[strongSelf _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateInitializing withError:nil];
return true;
}];

// Change our condition variables to reflect that we think we're logged in
strongSelf.loggedOut = [[CKKSCondition alloc] initToChain:strongSelf.loggedOut];
[strongSelf.loggedIn fulfill];
[strongSelf.accountStateKnown fulfill];
}];

[self scheduleAccountStatusOperation:login];
}

- (void)superHandleCKLogout {
[super handleCKLogout];
}

- (void)handleCKLogout {
__weak __typeof(self) weakSelf = self;
CKKSResultOperation* logout = [CKKSResultOperation named:@"ckks-logout" withBlock: ^{
__strong __typeof(self) strongSelf = weakSelf;
if(!strongSelf) {
return;
}
[strongSelf dispatchSync:^bool {
ckksnotice("ckks", strongSelf, "received a notification of CK logout");
[strongSelf superHandleCKLogout];

NSError* error = nil;
[strongSelf _onqueueResetLocalData: &error];
if(error) {
ckkserror("ckks", strongSelf, "error while resetting local data: %@", error);
}

[self _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateLoggedOut withError:nil];

strongSelf.loggedIn = [[CKKSCondition alloc] initToChain: strongSelf.loggedIn];
[strongSelf.loggedOut fulfill];
[strongSelf.accountStateKnown fulfill];

// Tell all pending sync clients that we don't expect to ever sync
for(NSString* callbackUUID in strongSelf.pendingSyncCallbacks.allKeys) {
[strongSelf callSyncCallbackWithErrorNoAccount:strongSelf.pendingSyncCallbacks[callbackUUID]];
strongSelf.pendingSyncCallbacks[callbackUUID] = nil;
}

return true;
}];
}];

[self scheduleAccountStatusOperation: logout];
}

- (void)callSyncCallbackWithErrorNoAccount:(SecBoolNSErrorCallback)syncCallback {
CKKSAccountStatus accountStatus = self.accountStatus;
dispatch_async(self.queue, ^{
syncCallback(false, [NSError errorWithDomain:@"securityd"
code:errSecNotLoggedIn
userInfo:@{NSLocalizedDescriptionKey:
[NSString stringWithFormat: @"No iCloud account available(%d); item is not expected to sync", (int)accountStatus]}]);
});
}

#pragma mark - CKKSChangeFetcherClient

- (CKKSCloudKitFetchRequest*)participateInFetch
{
__block CKKSCloudKitFetchRequest* request = [[CKKSCloudKitFetchRequest alloc] init];
[self dispatchSync: ^bool {
if(self.accountStatus != CKKSAccountStatusAvailable) {
ckksnotice("ckksfetch", self, "Not participating in fetch: not logged in");
request.participateInFetch = false;
return false;
}

if(!self.zoneCreated) {
ckksnotice("ckksfetch", self, "Not participating in fetch: zone not created yet");
request.participateInFetch = false;
return false;
}

request.participateInFetch = true;

if([self.keyHierarchyState isEqualToString:SecCKKSZoneKeyStateNeedFullRefetch]) {
// We want to return a nil change tag (to force a resync)
ckksnotice("ckksfetch", self, "Beginning refetch");
request.changeToken = nil;
} else {
CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.zoneName];
if(!ckse) {
ckkserror("ckksfetch", self, "couldn't fetch zone change token for %@", self.zoneName);
return false;
}
request.changeToken = ckse.changeToken;
}
return true;
}];

return request;
}

- (void)changesFetched:(NSArray<CKRecord*>*)changedRecords
deletedRecordIDs:(NSArray<CKKSCloudKitDeletion*>*)deletedRecords
oldChangeToken:(CKServerChangeToken*)oldChangeToken
newChangeToken:(CKServerChangeToken*)newChangeToken
{
[self dispatchSyncWithAccountKeys:^bool{
// This is a resync if we already have a change token, but this fetch didn't have one
CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state: self.zoneName];
bool resync = ckse.changeToken && (oldChangeToken == nil);

for (CKRecord* record in changedRecords) {
[self _onqueueCKRecordChanged:record resync:resync];
}

for (CKKSCloudKitDeletion* deletion in deletedRecords) {
[self _onqueueCKRecordDeleted:deletion.recordID recordType:deletion.recordType resync:resync];
}

NSError* error = nil;
if(resync) {
// Scan through all CKMirrorEntries and determine if any exist that CloudKit didn't tell us about
ckksnotice("ckksresync", self, "Comparing local UUIDs against the CloudKit list");
NSMutableArray<NSString*>* uuids = [[CKKSMirrorEntry allUUIDs:self.zoneID error:&error] mutableCopy];

for(NSString* uuid in uuids) {
CKRecord* record = nil;
CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:uuid zoneID:self.zoneID];
for(CKRecord* r in changedRecords) {
if([r.recordID isEqual:recordID]) {
record = r;
break;
}
}

if(record) {
ckksnotice("ckksresync", self, "UUID %@ is still in CloudKit; carry on.", uuid);
} else {
CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase:uuid zoneID:self.zoneID error:&error];
if(error != nil) {
ckkserror("ckksresync", self, "Couldn't read an item from the database, but it used to be there: %@ %@", uuid, error);
continue;
}
if(!ckme) {
ckkserror("ckksresync", self, "Couldn't read ckme(%@) from database; continuing", uuid);
continue;
}

ckkserror("ckksresync", self, "BUG: Local item %@ not found in CloudKit, deleting", uuid);
[self _onqueueCKRecordDeleted:ckme.item.storedCKRecord.recordID recordType:ckme.item.storedCKRecord.recordType resync:resync];
}
}
}

error = nil;

CKKSZoneStateEntry* state = [CKKSZoneStateEntry state:self.zoneName];
state.lastFetchTime = [NSDate date]; // The last fetch happened right now!
state.changeToken = newChangeToken;
[state saveToDatabase:&error];
if(error) {
ckkserror("ckksfetch", self, "Couldn't save new server change token: %@", error);
}

// Might as well kick off a IQO!
[self processIncomingQueue:false];

ckksnotice("ckksfetch", self, "Finished processing changes for %@", self.zoneID);

return true;
}];
}

// Return false if this is a 'fatal' error and we don't want another fetch to be tried
- (bool)notifyFetchError: (NSError*) error {
__weak __typeof(self) weakSelf = self;

bool isChangeTokenExpiredError = false;
if([error.domain isEqualToString:CKErrorDomain] && (error.code == CKErrorChangeTokenExpired)) {
isChangeTokenExpiredError = true;
} else if([error.domain isEqualToString:CKErrorDomain] && (error.code == CKErrorPartialFailure)) {
NSDictionary* partialErrors = error.userInfo[CKPartialErrorsByItemIDKey];
for(CKRecordZoneID* zoneID in partialErrors) {
NSError* partialError = partialErrors[zoneID];
if([zoneID isEqual:self.zoneID] && [partialError.domain isEqualToString:CKErrorDomain] && (partialError.code == CKErrorChangeTokenExpired)) {
isChangeTokenExpiredError = true;
}
}
}

if(isChangeTokenExpiredError) {
ckkserror("ckks", self, "Received notice that our change token is out of date (for %@). Resetting local data...", self.zoneID);
CKKSResultOperation* resetOp = [self resetLocalData];
CKKSResultOperation* resetHandler = [CKKSResultOperation named:@"local-reset-handler" withBlock:^{
__strong __typeof(self) strongSelf = weakSelf;
if(!strongSelf) {
ckkserror("ckks", strongSelf, "received callback for released object");
return;
}

if(resetOp.error) {
ckksnotice("ckksreset", strongSelf, "CloudKit-inspired local reset of %@ ended with error: %@", strongSelf.zoneID, error);
} else {
ckksnotice("ckksreset", strongSelf, "CloudKit-inspired local reset of %@ ended successfully", strongSelf.zoneID);
}
}];

[resetHandler addDependency:resetOp];
[self scheduleOperation:resetHandler];
return false;
}

bool isDeletedZoneError = false;
if([error.domain isEqualToString:CKErrorDomain] && ((error.code == CKErrorUserDeletedZone) || (error.code == CKErrorZoneNotFound))) {
isDeletedZoneError = true;
} else if([error.domain isEqualToString:CKErrorDomain] && (error.code == CKErrorPartialFailure)) {
NSDictionary* partialErrors = error.userInfo[CKPartialErrorsByItemIDKey];
for(CKRecordZoneID* zoneID in partialErrors) {
NSError* partialError = partialErrors[zoneID];
if([self.zoneID isEqual:zoneID] && [partialError.domain isEqualToString:CKErrorDomain] && ((partialError.code == CKErrorUserDeletedZone) || (partialError.code == CKErrorZoneNotFound))) {
isDeletedZoneError = true;
}
}
}

if(isDeletedZoneError) {
ckkserror("ckks", self, "Received notice that our zone(%@) does not exist. Resetting local data.", self.zoneID);
CKKSResultOperation* resetOp = [self resetLocalData];
CKKSResultOperation* resetHandler = [CKKSResultOperation named:@"reset-handler" withBlock:^{
__strong __typeof(self) strongSelf = weakSelf;
if(!strongSelf) {
ckkserror("ckksreset", strongSelf, "received callback for released object");
return;
}

if(resetOp.error) {
ckksnotice("ckksreset", strongSelf, "CloudKit-inspired local reset of %@ ended with error: %@", strongSelf.zoneID, resetOp.error);
} else {
ckksnotice("ckksreset", strongSelf, "CloudKit-inspired local reset of %@ ended successfully", strongSelf.zoneID);
}
}];

[resetHandler addDependency:resetOp];
[self scheduleOperation:resetHandler];
return false;
}

if([error.domain isEqualToString:CKErrorDomain] && (error.code == CKErrorBadContainer)) {
ckkserror("ckks", self, "Received notice that our container does not exist. Nothing to do.");
return false;
}

return true;
}

#pragma mark CKKSPeerUpdateListener

- (void)selfPeerChanged {
// Currently, we have no idea what to do with this. Kick off a key reprocess?
ckkserror("ckks", self, "Received update that our self identity has changed");
[self keyStateMachineRequestProcess];
}

- (void)trustedPeerSetChanged {
// We might need to share the TLK to some new people, or we might now trust the TLKs we have.
// The key state machine should handle that, so poke it.
ckkserror("ckks", self, "Received update that the trust set has changed");

self.trustedPeersSetChanged = true;
[self.pokeKeyStateMachineScheduler trigger];
}

#pragma mark - Test Support

- (bool) outgoingQueueEmpty: (NSError * __autoreleasing *) error {
__block bool ret = false;
[self dispatchSync: ^bool{
NSArray* queueEntries = [CKKSOutgoingQueueEntry all: error];
ret = queueEntries && ([queueEntries count] == 0);
return true;
}];

return ret;
}

- (CKKSResultOperation*)waitForFetchAndIncomingQueueProcessing {
CKKSResultOperation* op = [self fetchAndProcessCKChanges:CKKSFetchBecauseTesting];
[op waitUntilFinished];
return op;
}

- (void)waitForKeyHierarchyReadiness {
if(self.keyStateReadyDependency) {
[self.keyStateReadyDependency waitUntilFinished];
}
}

- (void)cancelPendingOperations {
@synchronized(self.outgoingQueueOperations) {
for(NSOperation* op in self.outgoingQueueOperations) {
[op cancel];
}
[self.outgoingQueueOperations removeAllObjects];
}

@synchronized(self.incomingQueueOperations) {
for(NSOperation* op in self.incomingQueueOperations) {
[op cancel];
}
[self.incomingQueueOperations removeAllObjects];
}

[super cancelAllOperations];
}

- (void)cancelAllOperations {
[self.zoneSetupOperation cancel];
[self.keyStateMachineOperation cancel];
[self.keyStateReadyDependency cancel];
[self.keyStateNonTransientDependency cancel];
[self.zoneChangeFetcher cancel];
[self.notifyViewChangedScheduler cancel];

[self cancelPendingOperations];

[self dispatchSync:^bool{
[self _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateCancelled withError: nil];
return true;
}];
}

- (void)halt {
[super halt];

// Don't send any more notifications, either
_notifierClass = nil;
}

- (NSDictionary*)status {
#define stringify(obj) CKKSNilToNSNull([obj description])
#define boolstr(obj) (!!(obj) ? @"yes" : @"no")
__block NSMutableDictionary* ret = nil;
__block NSError* error = nil;
CKKSManifest* manifest = nil;

ret = [[self fastStatus] mutableCopy];

manifest = [CKKSManifest latestTrustedManifestForZone:self.zoneName error:&error];
[self dispatchSync: ^bool {

CKKSCurrentKeySet* keyset = [[CKKSCurrentKeySet alloc] initForZone:self.zoneID];
if(keyset.error) {
error = keyset.error;
}

NSString* manifestGeneration = manifest ? [NSString stringWithFormat:@"%lu", (unsigned long)manifest.generationCount] : nil;

if(error) {
ckkserror("ckks", self, "error during status: %@", error);
}
// We actually don't care about this error, especially if it's "no current key pointers"...
error = nil;

// Map deviceStates to strings to avoid NSXPC issues. Obj-c, why is this so hard?
NSArray* deviceStates = [CKKSDeviceStateEntry allInZone:self.zoneID error:&error];
NSMutableArray<NSString*>* mutDeviceStates = [[NSMutableArray alloc] init];
[deviceStates enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[mutDeviceStates addObject: [obj description]];
}];

NSArray* tlkShares = [CKKSTLKShare allForUUID:keyset.currentTLKPointer.currentKeyUUID zoneID:self.zoneID error:&error];
NSMutableArray<NSString*>* mutTLKShares = [[NSMutableArray alloc] init];
[tlkShares enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[mutTLKShares addObject: [obj description]];
}];

[ret addEntriesFromDictionary:@{
@"statusError": stringify(error),
@"oqe": CKKSNilToNSNull([CKKSOutgoingQueueEntry countsByStateInZone:self.zoneID error:&error]),
@"iqe": CKKSNilToNSNull([CKKSIncomingQueueEntry countsByStateInZone:self.zoneID error:&error]),
@"ckmirror": CKKSNilToNSNull([CKKSMirrorEntry countsByParentKey:self.zoneID error:&error]),
@"devicestates": CKKSNilToNSNull(mutDeviceStates),
@"tlkshares": CKKSNilToNSNull(mutTLKShares),
@"keys": CKKSNilToNSNull([CKKSKey countsByClass:self.zoneID error:&error]),
@"currentTLK": CKKSNilToNSNull(keyset.tlk.uuid),
@"currentClassA": CKKSNilToNSNull(keyset.classA.uuid),
@"currentClassC": CKKSNilToNSNull(keyset.classC.uuid),
@"currentTLKPtr": CKKSNilToNSNull(keyset.currentTLKPointer.currentKeyUUID),
@"currentClassAPtr": CKKSNilToNSNull(keyset.currentClassAPointer.currentKeyUUID),
@"currentClassCPtr": CKKSNilToNSNull(keyset.currentClassCPointer.currentKeyUUID),
@"currentManifestGen": CKKSNilToNSNull(manifestGeneration),
}];
return false;
}];
return ret;
}

- (NSDictionary*)fastStatus {

__block NSDictionary* ret = nil;

[self dispatchSync: ^bool {

ret = @{
@"view": CKKSNilToNSNull(self.zoneName),
@"ckaccountstatus": self.accountStatus == CKAccountStatusCouldNotDetermine ? @"could not determine" :
self.accountStatus == CKAccountStatusAvailable ? @"logged in" :
self.accountStatus == CKAccountStatusRestricted ? @"restricted" :
self.accountStatus == CKAccountStatusNoAccount ? @"logged out" : @"unknown",
@"lockstatetracker": stringify(self.lockStateTracker),
@"accounttracker": stringify(self.accountTracker),
@"fetcher": stringify(self.zoneChangeFetcher),
@"zoneCreated": boolstr(self.zoneCreated),
@"zoneCreatedError": stringify(self.zoneCreatedError),
@"zoneSubscribed": boolstr(self.zoneSubscribed),
@"zoneSubscribedError": stringify(self.zoneSubscribedError),
@"zoneInitializeScheduler": stringify(self.initializeScheduler),
@"keystate": CKKSNilToNSNull(self.keyHierarchyState),
@"keyStateError": stringify(self.keyHierarchyError),
@"statusError": [NSNull null],

@"zoneSetupOperation": stringify(self.zoneSetupOperation),
@"keyStateOperation": stringify(self.keyStateMachineOperation),
@"lastIncomingQueueOperation": stringify(self.lastIncomingQueueOperation),
@"lastNewTLKOperation": stringify(self.lastNewTLKOperation),
@"lastOutgoingQueueOperation": stringify(self.lastOutgoingQueueOperation),
@"lastProcessReceivedKeysOperation": stringify(self.lastProcessReceivedKeysOperation),
@"lastReencryptOutgoingItemsOperation":stringify(self.lastReencryptOutgoingItemsOperation),
@"lastScanLocalItemsOperation": stringify(self.lastScanLocalItemsOperation),
};
return false;
}];

return ret;
}

#endif /* OCTAGON */
@end