#endif
#import "CKKS.h"
-#import "CKKSAPSReceiver.h"
+#import "keychain/ckks/CKKSStates.h"
+#import "OctagonAPSReceiver.h"
#import "CKKSIncomingQueueEntry.h"
#import "CKKSOutgoingQueueEntry.h"
#import "CKKSCurrentKeyPointer.h"
#import "CKKSIncomingQueueOperation.h"
#import "CKKSNewTLKOperation.h"
#import "CKKSProcessReceivedKeysOperation.h"
-#import "CKKSZone.h"
#import "CKKSFetchAllRecordZoneChangesOperation.h"
-#import "CKKSHealKeyHierarchyOperation.h"
+#import "keychain/ckks/CKKSHealKeyHierarchyOperation.h"
#import "CKKSReencryptOutgoingItemsOperation.h"
#import "CKKSScanLocalItemsOperation.h"
#import "CKKSSynchronizeOperation.h"
#import "CKKSManifest.h"
#import "CKKSManifestLeafRecord.h"
#import "CKKSZoneChangeFetcher.h"
+#import "CKKSAnalytics.h"
+#import "keychain/analytics/CKKSLaunchSequence.h"
+#import "keychain/ckks/CKKSCloudKitClassDependencies.h"
#import "keychain/ckks/CKKSDeviceStateEntry.h"
#import "keychain/ckks/CKKSNearFutureScheduler.h"
#import "keychain/ckks/CKKSCurrentItemPointer.h"
+#import "keychain/ckks/CKKSCreateCKZoneOperation.h"
+#import "keychain/ckks/CKKSDeleteCKZoneOperation.h"
#import "keychain/ckks/CKKSUpdateCurrentItemPointerOperation.h"
#import "keychain/ckks/CKKSUpdateDeviceStateOperation.h"
-#import "keychain/ckks/CKKSLockStateTracker.h"
#import "keychain/ckks/CKKSNotifier.h"
#import "keychain/ckks/CloudKitCategories.h"
+#import "keychain/ckks/CKKSTLKShareRecord.h"
+#import "keychain/ckks/CKKSHealTLKSharesOperation.h"
+#import "keychain/ckks/CKKSLocalSynchronizeOperation.h"
+#import "keychain/ckks/CKKSPeerProvider.h"
+#import "keychain/ckks/CKKSCheckKeyHierarchyOperation.h"
+#import "keychain/ckks/CKKSViewManager.h"
+#import "keychain/categories/NSError+UsefulConstructors.h"
+
+#import "keychain/ckks/CKKSLocalResetOperation.h"
+
+#import "keychain/ot/OTConstants.h"
+#import "keychain/ot/OTDefines.h"
+#import "keychain/ot/OctagonCKKSPeerAdapter.h"
+#import "keychain/ot/ObjCImprovements.h"
#include <utilities/SecCFWrappers.h>
+#include <utilities/SecTrace.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 "keychain/securityd/SecDbItem.h"
+#include "keychain/securityd/SecItemDb.h"
+#include "keychain/securityd/SecItemSchema.h"
+#include "keychain/securityd/SecItemServer.h"
#include <Security/SecItemPriv.h>
-#include <Security/SecureObjectSync/SOSAccountTransaction.h>
-#include <utilities/SecADWrapper.h>
+#include "keychain/SecureObjectSync/SOSAccountTransaction.h"
#include <utilities/SecPLWrappers.h>
+#include <os/transaction_private.h>
+
+#import "keychain/trust/TrustedPeers/TPSyncingPolicy.h"
+#import <Security/SecItemInternal.h>
#if OCTAGON
+
@interface CKKSKeychainView()
-@property bool setupSuccessful;
-@property bool keyStateFetchRequested;
-@property bool keyStateFullRefetchRequested;
-@property bool keyStateProcessRequested;
-@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;
+
+// An extra queue for semaphore-waiting-based NSOperations
+@property NSOperationQueue* waitingQueue;
+
+// Scratch space for resyncs
+@property (nullable) NSMutableSet<NSString*>* resyncRecordsSeen;
+
+
-@property NSMutableDictionary<NSString*, SecBoolNSErrorCallback>* pendingSyncCallbacks;
+@property NSOperationQueue* operationQueue;
+@property CKKSResultOperation* accountLoggedInDependency;
+@property BOOL halted;
+
+// Make these readwrite
+@property NSArray<CKKSPeerProviderState*>* currentTrustStates;
+
+@property NSMutableSet<CKKSFetchBecause*>* currentFetchReasons;
@end
#endif
@implementation CKKSKeychainView
#if OCTAGON
-- (instancetype)initWithContainer: (CKContainer*) container
- zoneName: (NSString*) zoneName
- accountTracker:(CKKSCKAccountStateTracker*) accountTracker
- lockStateTracker:(CKKSLockStateTracker*) lockStateTracker
- savedTLKNotifier:(CKKSNearFutureScheduler*) savedTLKNotifier
- fetchRecordZoneChangesOperationClass: (Class<CKKSFetchRecordZoneChangesOperation>) fetchRecordZoneChangesOperationClass
- modifySubscriptionsOperationClass: (Class<CKKSModifySubscriptionsOperation>) modifySubscriptionsOperationClass
- modifyRecordZonesOperationClass: (Class<CKKSModifyRecordZonesOperation>) modifyRecordZonesOperationClass
- apsConnectionClass: (Class<CKKSAPSConnection>) apsConnectionClass
- notifierClass: (Class<CKKSNotifier>) notifierClass
+- (instancetype)initWithContainer:(CKContainer*)container
+ zoneName:(NSString*)zoneName
+ accountTracker:(CKKSAccountStateTracker*)accountTracker
+ lockStateTracker:(CKKSLockStateTracker*)lockStateTracker
+ reachabilityTracker:(CKKSReachabilityTracker*)reachabilityTracker
+ changeFetcher:(CKKSZoneChangeFetcher*)fetcher
+ zoneModifier:(CKKSZoneModifier*)zoneModifier
+ savedTLKNotifier:(CKKSNearFutureScheduler*)savedTLKNotifier
+ cloudKitClassDependencies:(CKKSCloudKitClassDependencies*)cloudKitClassDependencies
{
- if(self = [super initWithContainer:container
- zoneName:zoneName
- accountTracker:accountTracker
- fetchRecordZoneChangesOperationClass:fetchRecordZoneChangesOperationClass
- modifySubscriptionsOperationClass:modifySubscriptionsOperationClass
- modifyRecordZonesOperationClass:modifyRecordZonesOperationClass
- apsConnectionClass:apsConnectionClass]) {
- __weak __typeof(self) weakSelf = self;
+ if((self = [super init])) {
+ WEAKIFY(self);
+
+ _container = container;
+ _zoneName = zoneName;
+ _accountTracker = accountTracker;
+ _reachabilityTracker = reachabilityTracker;
+ _cloudKitClassDependencies = cloudKitClassDependencies;
+
+ _halted = NO;
+
+ _database = [_container privateCloudDatabase];
+ _zoneID = [[CKRecordZoneID alloc] initWithZoneName:zoneName ownerName:CKCurrentUserDefaultName];
+
+ _accountStatus = CKKSAccountStatusUnknown;
+ _accountLoggedInDependency = [self createAccountLoggedInDependency:@"CloudKit account logged in."];
+
+ _queue = dispatch_queue_create([[NSString stringWithFormat:@"CKKSQueue.%@.zone.%@", container.containerIdentifier, zoneName] UTF8String], DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
+ _operationQueue = [[NSOperationQueue alloc] init];
+
+
+ _loggedIn = [[CKKSCondition alloc] init];
+ _loggedOut = [[CKKSCondition alloc] init];
+ _accountStateKnown = [[CKKSCondition alloc] init];
+
+ _initiatedLocalScan = NO;
+
+ _trustStatus = CKKSAccountStatusUnknown;
_incomingQueueOperations = [NSHashTable weakObjectsHashTable];
_outgoingQueueOperations = [NSHashTable weakObjectsHashTable];
- _zoneChangeFetcher = [[CKKSZoneChangeFetcher alloc] initWithCKKSKeychainView: self];
+ _scanLocalItemsOperations = [NSHashTable weakObjectsHashTable];
+
+ _currentTrustStates = @[];
+
+ _currentFetchReasons = [NSMutableSet set];
+
+ _launch = [[CKKSLaunchSequence alloc] initWithRocketName:@"com.apple.security.ckks.launch"];
+ [_launch addAttribute:@"view" value:zoneName];
+
+ _zoneChangeFetcher = fetcher;
+ [fetcher registerClient:self];
+
+ _resyncRecordsSeen = nil;
- _notifierClass = notifierClass;
+ _notifierClass = cloudKitClassDependencies.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;
- ckksnotice("ckks", strongSelf, "");
- [strongSelf.notifierClass post:[NSString stringWithFormat:@"com.apple.security.view-change.%@", strongSelf.zoneName]];
+ STRONGIFY(self);
+ [self.notifierClass post:[NSString stringWithFormat:@"com.apple.security.view-change.%@", self.zoneName]];
+ [self.notifierClass post:[NSString stringWithUTF8String:kSecServerKeychainChangedNotification]];
+
// 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.notifierClass post:@"com.apple.security.view-change.PCS"];
+ if([self.zoneName isEqualToString:@"Manatee"] ||
+ [self.zoneName isEqualToString:@"Engram"] ||
+ [self.zoneName isEqualToString:@"ApplePay"] ||
+ [self.zoneName isEqualToString:@"Home"] ||
+ [self.zoneName isEqualToString:@"LimitedPeersAllowed"]) {
+ [self.notifierClass post:@"com.apple.security.view-change.PCS"];
}
}];
- _pendingSyncCallbacks = [[NSMutableDictionary alloc] init];
-
- _lockStateTracker = lockStateTracker;
- _savedTLKNotifier = savedTLKNotifier;
-
- _setupSuccessful = false;
+ _notifyViewReadyScheduler = [[CKKSNearFutureScheduler alloc] initWithName:[NSString stringWithFormat: @"%@-ready-scheduler", self.zoneName]
+ initialDelay:250*NSEC_PER_MSEC
+ continuingDelay:120*NSEC_PER_SEC
+ keepProcessAlive:true
+ dependencyDescriptionCode:CKKSResultDescriptionPendingViewChangedScheduling
+ block:^{
+ STRONGIFY(self);
+ NSDistributedNotificationCenter *center = [self.cloudKitClassDependencies.nsdistributednotificationCenterClass defaultCenter];
- _keyHierarchyConditions = [[NSMutableDictionary alloc] init];
- [CKKSZoneKeyStateMap() enumerateKeysAndObjectsUsingBlock:^(CKKSZoneKeyState * _Nonnull key, NSNumber * _Nonnull obj, BOOL * _Nonnull stop) {
- [self.keyHierarchyConditions setObject: [[CKKSCondition alloc] init] forKey:key];
- }];
-
- self.keyHierarchyState = SecCKKSZoneKeyStateInitializing;
- _keyHierarchyError = nil;
- _keyHierarchyOperationGroup = nil;
- _keyStateMachineOperation = nil;
- _keyStateFetchRequested = false;
- _keyStateProcessRequested = false;
+ [center postNotificationName:@"com.apple.security.view-become-ready"
+ object:nil
+ userInfo:@{ @"view" : self.zoneName ?: @"unknown" }
+ options:0];
+ }];
- _keyStateReadyDependency = [CKKSResultOperation operationWithBlock:^{
- ckksnotice("ckkskey", weakSelf, "Key state has become ready for the first time.");
- }];
- self.keyStateReadyDependency.name = [NSString stringWithFormat: @"%@-key-state-ready", self.zoneName];
- dispatch_time_t initializeDelay = SecCKKSTestsEnabled() ? NSEC_PER_MSEC * 500 : NSEC_PER_SEC * 30;
- _initializeScheduler = [[CKKSNearFutureScheduler alloc] initWithName:[NSString stringWithFormat: @"%@-zone-initializer", self.zoneName]
- initialDelay:0
- continuingDelay:initializeDelay
- keepProcessAlive:false
- block:^{
- __strong __typeof(self) strongSelf = weakSelf;
- ckksnotice("ckks", strongSelf, "initialize-scheduler restarting setup");
- [strongSelf maybeRestartSetup];
- }];
+ _lockStateTracker = lockStateTracker;
+ _stateMachine = [[OctagonStateMachine alloc] initWithName:[NSString stringWithFormat:@"ckks-%@", self.zoneName]
+ states:[NSSet setWithArray:[CKKSZoneKeyStateMap() allKeys]]
+ flags:CKKSAllStateFlags()
+ initialState:SecCKKSZoneKeyStateWaitForCloudKitAccountStatus
+ queue:self.queue
+ stateEngine:self
+ lockStateTracker:lockStateTracker];
+ [_stateMachine startOperation];
+
+ _waitingQueue = [[NSOperationQueue alloc] init];
+ _waitingQueue.maxConcurrentOperationCount = 5;
+
+ _keyStateReadyDependency = [self createKeyStateReadyDependency: @"Key state has become ready for the first time."];
+
+ 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:^{}];
+
+ _operationDependencies = [[CKKSOperationDependencies alloc] initWithZoneID:self.zoneID
+ zoneModifier:zoneModifier
+ ckoperationGroup:nil
+ flagHandler:_stateMachine
+ launchSequence:_launch
+ lockStateTracker:_lockStateTracker
+ reachabilityTracker:reachabilityTracker
+ peerProviders:@[]
+ databaseProvider:self
+ notifyViewChangedScheduler:_notifyViewChangedScheduler
+ savedTLKNotifier:savedTLKNotifier];
}
return self;
}
- (NSString*)description {
- return [NSString stringWithFormat:@"<%@: %@>", NSStringFromClass([self class]), self.zoneName];
+ return [NSString stringWithFormat:@"<%@: %@ (%@)>", NSStringFromClass([self class]), self.zoneName, self.keyHierarchyState];
}
- (NSString*)debugDescription {
- return [NSString stringWithFormat:@"<%@: %@ %p>", NSStringFromClass([self class]), self.zoneName, self];
+ return [NSString stringWithFormat:@"<%@: %@ (%@) %p>", NSStringFromClass([self class]), self.zoneName, self.keyHierarchyState, self];
}
- (CKKSZoneKeyState*)keyHierarchyState {
- return _keyHierarchyState;
+ return self.stateMachine.currentState;
}
-- (void)setKeyHierarchyState:(CKKSZoneKeyState *)keyHierarchyState {
- if((keyHierarchyState == nil && _keyHierarchyState == nil) || ([keyHierarchyState isEqualToString:_keyHierarchyState])) {
- // No change, do nothing.
- } else {
- // Fixup the condition variables
- if(_keyHierarchyState) {
- self.keyHierarchyConditions[_keyHierarchyState] = [[CKKSCondition alloc] init];
- }
- if(keyHierarchyState) {
- [self.keyHierarchyConditions[keyHierarchyState] fulfill];
- }
- }
-
- _keyHierarchyState = keyHierarchyState;
-}
-
-- (NSString *)lastActiveTLKUUID
+- (NSMutableDictionary<CKKSZoneKeyState*, CKKSCondition*>*)keyHierarchyConditions
{
- return self.activeTLK;
-}
-
-- (void) initializeZone {
- // Unfortunate, but makes retriggering easy.
- [self.initializeScheduler trigger];
-}
-
-- (void)maybeRestartSetup {
- [self dispatchSync: ^bool{
- if(self.setupStarted && !self.setupComplete) {
- ckksdebug("ckks", self, "setup has restarted. Ignoring timer fire");
- return false;
- }
-
- if(self.setupSuccessful) {
- ckksdebug("ckks", self, "setup has completed successfully. Ignoring timer fire");
- return false;
- }
-
- [self resetSetup];
- [self _onqueueInitializeZone];
- return true;
- }];
-}
-
-- (void)resetSetup {
- [super resetSetup];
- self.setupSuccessful = false;
-
- // Key hierarchy state machine resets, too
- self.keyHierarchyState = SecCKKSZoneKeyStateInitializing;
- _keyHierarchyError = nil;
+ return self.stateMachine.stateConditions;
}
- - (void)_onqueueInitializeZone {
- if(!SecCKKSIsEnabled()) {
- ckksnotice("ckks", self, "Skipping CloudKit initialization due to disabled CKKS");
- return;
+- (void)ensureKeyStateReadyDependency:(NSString*)resetMessage {
+ NSOperation* oldKSRD = self.keyStateReadyDependency;
+ self.keyStateReadyDependency = [self createKeyStateReadyDependency:resetMessage];
+ if(oldKSRD) {
+ [oldKSRD addDependency:self.keyStateReadyDependency];
+ [self.waitingQueue addOperation:oldKSRD];
}
+}
- dispatch_assert_queue(self.queue);
-
- __weak __typeof(self) weakSelf = self;
-
- NSBlockOperation* afterZoneSetup = [NSBlockOperation blockOperationWithBlock: ^{
- __strong __typeof(weakSelf) strongSelf = weakSelf;
- if(!strongSelf) {
- ckkserror("ckks", strongSelf, "received callback for released object");
- return;
- }
-
- __block bool quit = false;
-
- [strongSelf dispatchSync: ^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 || strongSelf.accountStatus != CKAccountStatusAvailable) {
- // Something has gone very wrong. Error out and maybe retry.
- quit = true;
-
- // Note that CKKSZone has probably called [handleLogout]; which means we have a key hierarchy reset queued up. Error here anyway.
- NSError* realReason = strongSelf.zoneCreatedError ? strongSelf.zoneCreatedError : strongSelf.zoneSubscribedError;
- [strongSelf _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateError withError: realReason];
-
- // We're supposed to be up, but something has gone wrong. Blindly retry until it works.
- if(strongSelf.accountStatus == CKKSAccountStatusAvailable) {
- [strongSelf.initializeScheduler trigger];
- ckksnotice("ckks", strongSelf, "We're logged in, but setup didn't work. Scheduling retry for %@", strongSelf.initializeScheduler.nextFireTime);
- }
- return true;
- } else {
- strongSelf.setupSuccessful = true;
- }
-
- return true;
- }];
-
- if(quit) {
- ckkserror("ckks", strongSelf, "Quitting setup.");
- return;
- }
-
- // We can't enter the account queue until an account exists. Before this point, we don't know if one does.
- [strongSelf dispatchSyncWithAccountQueue: ^bool{
- CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state: strongSelf.zoneName];
+- (CKKSResultOperation<OctagonStateTransitionOperationProtocol>*)performInitializedOperation
+{
+ WEAKIFY(self);
+ return [OctagonStateTransitionOperation named:@"ckks-initialized-operation"
+ intending:SecCKKSZoneKeyStateBecomeReady
+ errorState:SecCKKSZoneKeyStateError
+ withBlockTakingSelf:^(OctagonStateTransitionOperation * _Nonnull op) {
+ STRONGIFY(self);
+ [self dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
+ CKKSOutgoingQueueOperation* outgoingOperation = nil;
+ CKKSIncomingQueueOperation* initialProcess = nil;
+ CKKSScanLocalItemsOperation* initialScan = nil;
+
+ CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.zoneName];
// Check if we believe we've synced this zone before.
if(ckse.changeToken == nil) {
- strongSelf.keyHierarchyOperationGroup = [CKOperationGroup CKKSGroupWithName:@"initial-setup"];
+ self.operationDependencies.ckoperationGroup = [CKOperationGroup CKKSGroupWithName:@"initial-setup"];
- ckksnotice("ckks", strongSelf, "No existing change token; going to try to match local items with CloudKit ones.");
+ 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 = [strongSelf.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseInitialStart];
- fetch.name = @"initial-fetch";
- // Next, try to process them (replacing local entries)
- CKKSIncomingQueueOperation* initialProcess = [strongSelf processIncomingQueue: true after: fetch ];
- initialProcess.name = @"initial-process-incoming-queue";
+ [self.currentFetchReasons addObject:CKKSFetchBecauseInitialStart];
+ op.nextState = SecCKKSZoneKeyStateBeginFetch;
+
+ // Next, try to process them (replacing local entries). This will wait for the key state to be ready.
+ initialProcess = [self processIncomingQueue:true after:nil];
// If all that succeeds, iterate through all keychain items and find the ones which need to be uploaded
- strongSelf.initialScanOperation = [[CKKSScanLocalItemsOperation alloc] initWithCKKSKeychainView:strongSelf ckoperationGroup:strongSelf.keyHierarchyOperationGroup];
- strongSelf.initialScanOperation.name = @"initial-scan-operation";
- [strongSelf.initialScanOperation addNullableDependency:strongSelf.lockStateTracker.unlockDependency];
- [strongSelf.initialScanOperation addDependency: initialProcess];
- [strongSelf scheduleOperation: strongSelf.initialScanOperation];
+ initialScan = [self scanLocalItems:@"initial-scan-operation"
+ ckoperationGroup:self.operationDependencies.ckoperationGroup
+ after:initialProcess];
} else {
// Likely a restart of securityd!
- strongSelf.keyHierarchyOperationGroup = [CKOperationGroup CKKSGroupWithName:@"restart-setup"];
+ // 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];
+ op.nextState = SecCKKSZoneKeyStateWaitForFixupOperation;
+ return CKKSDatabaseTransactionCommit;
+ }
+
+ // 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);
+ }
- if ([CKKSManifest shouldSyncManifests]) {
- strongSelf.egoManifest = [CKKSEgoManifest tryCurrentEgoManifestForZone:strongSelf.zoneName];
+ // Are there any entries waiting for reencryption? If so, set the flag.
+ error = nil;
+ NSArray<CKKSOutgoingQueueEntry*>* reencryptOQEs = [CKKSOutgoingQueueEntry allInState:SecCKKSStateReencrypt
+ zoneID:self.zoneID
+ error:&error];
+ if(error) {
+ ckkserror("ckks", self, "Couldn't load reencrypt OQEs, bad behavior ahead: %@", error);
+ }
+ if(reencryptOQEs.count > 0) {
+ [self.stateMachine _onqueueHandleFlag:CKKSFlagItemReencryptionNeeded];
}
+ self.operationDependencies.ckoperationGroup = [CKOperationGroup CKKSGroupWithName:@"restart-setup"];
+
// If it's been more than 24 hours since the last fetch, fetch and process everything.
+ // Or, if we think we were interrupted in the middle of fetching, fetch some more.
// Otherwise, just kick off the local queue processing.
NSDate* now = [NSDate date];
[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 = [strongSelf fetchAndProcessCKChanges:CKKSFetchBecauseSecuritydRestart];
+ if(ckse.lastFetchTime == nil ||
+ [ckse.lastFetchTime compare: deadline] == NSOrderedAscending ||
+ ckse.moreRecordsInCloudKit) {
+
+ op.nextState = SecCKKSZoneKeyStateBeginFetch;
+
} else {
- initialProcess = [strongSelf processIncomingQueue:false];
+ // Check if we have an existing key hierarchy in keyset
+ CKKSCurrentKeySet* keyset = [CKKSCurrentKeySet loadForZone:self.zoneID];
+ if(keyset.error && !([keyset.error.domain isEqual: @"securityd"] && keyset.error.code == errSecItemNotFound)) {
+ ckkserror("ckkskey", self, "Error examining existing key hierarchy: %@", keyset.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.
+ op.nextState = SecCKKSZoneKeyStateBecomeReady;
+
+ } else {
+ ckksnotice("ckkskey", self, "No existing key hierarchy for %@. Check if there's one in CloudKit...", self.zoneID.zoneName);
+ op.nextState = SecCKKSZoneKeyStateBeginFetch;
+ }
}
- if(!strongSelf.egoManifest) {
- ckksnotice("ckksmanifest", strongSelf, "No ego manifest on restart; rescanning");
- strongSelf.initialScanOperation = [[CKKSScanLocalItemsOperation alloc] initWithCKKSKeychainView:strongSelf ckoperationGroup:strongSelf.keyHierarchyOperationGroup];
- strongSelf.initialScanOperation.name = @"initial-scan-operation";
- [strongSelf.initialScanOperation addNullableDependency:strongSelf.lockStateTracker.unlockDependency];
- [strongSelf.initialScanOperation addDependency: initialProcess];
- [strongSelf scheduleOperation: strongSelf.initialScanOperation];
+ if(ckse.lastLocalKeychainScanTime == nil || [ckse.lastLocalKeychainScanTime compare:deadline] == NSOrderedAscending) {
+ // TODO handle with a state flow
+ ckksnotice("ckksscan", self, "CKKS scan last occurred at %@; beginning a new one", ckse.lastLocalKeychainScanTime);
+ initialScan = [self scanLocalItems:ckse.lastLocalKeychainScanTime == nil ? @"initial-scan-operation" : @"24-hr-scan-operation"
+ ckoperationGroup:self.operationDependencies.ckoperationGroup
+ after:nil];
}
- [strongSelf processOutgoingQueue:strongSelf.keyHierarchyOperationGroup];
+ // Process outgoing queue after re-start
+ outgoingOperation = [self processOutgoingQueueAfter:nil ckoperationGroup:self.operationDependencies.ckoperationGroup];
}
- // Tell the key state machine to fire off.
- [strongSelf _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateInitialized withError: nil];
- return true;
+ /*
+ * Launch time is determined by when the zone have:
+ * 1. keystate have become ready
+ * 2. scan local items (if needed)
+ * 3. processed all outgoing item (if needed)
+ * TODO: this should move, once queue processing becomes part of the state machine
+ */
+
+ WEAKIFY(self);
+ NSBlockOperation *seemReady = [NSBlockOperation named:[NSString stringWithFormat:@"seemsReadyForSyncing-%@", self.zoneName] withBlock:^void{
+ STRONGIFY(self);
+ NSError *error = nil;
+ ckksnotice("launch", self, "Launch complete");
+ NSNumber *zoneSize = [CKKSMirrorEntry counts:self.zoneID error:&error];
+ if (zoneSize) {
+ zoneSize = @(SecBucket1Significant([zoneSize longValue]));
+ [self.launch addAttribute:@"zonesize" value:zoneSize];
+ }
+ [self.launch launch];
+
+ /*
+ * Since we think we are ready, signal to CK that its to check for PCS identities again, and create the
+ * since before we completed this operation, we would probably have failed with a timeout because
+ * we where busy downloading items from CloudKit and then processing them.
+ */
+ [self.notifyViewReadyScheduler trigger];
+ }];
+
+ [seemReady addNullableDependency:self.keyStateReadyDependency];
+ [seemReady addNullableDependency:outgoingOperation];
+ [seemReady addNullableDependency:initialScan];
+ [seemReady addNullableDependency:initialProcess];
+ [self scheduleOperation:seemReady];
+
+ return CKKSDatabaseTransactionCommit;
}];
}];
- afterZoneSetup.name = @"view-setup";
+}
- CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state: self.zoneName];
- NSOperation* zoneSetupOperation = [self createSetupOperation: ckse.ckzonecreated zoneSubscribed: ckse.ckzonesubscribed];
+- (CKKSResultOperation*)resetLocalData {
+ ckksnotice("ckksreset", self, "Requesting local data reset");
+
+ return [self.stateMachine doWatchedStateMachineRPC:@"ckks-local-reset"
+ sourceStates:[NSSet setWithArray:@[
+ // TODO: possibly every state?
+ SecCKKSZoneKeyStateReady,
+ SecCKKSZoneKeyStateWaitForTLK,
+ SecCKKSZoneKeyStateWaitForTrust,
+ SecCKKSZoneKeyStateWaitForTLKUpload,
+ SecCKKSZoneKeyStateLoggedOut,
+ ]]
+ path:[OctagonStateTransitionPath pathFromDictionary:@{
+ SecCKKSZoneKeyStateResettingLocalData: @{
+ SecCKKSZoneKeyStateInitializing: @{
+ SecCKKSZoneKeyStateInitialized: [OctagonStateTransitionPathStep success],
+ SecCKKSZoneKeyStateLoggedOut: [OctagonStateTransitionPathStep success],
+ }
+ }
+ }]
+ reply:^(NSError * _Nonnull error) {}];
+}
+
+- (CKKSResultOperation*)resetCloudKitZone:(CKOperationGroup*)operationGroup
+{
+ [self.accountStateKnown wait:(SecCKKSTestsEnabled() ? 1*NSEC_PER_SEC : 10*NSEC_PER_SEC)];
- self.viewSetupOperation = [[CKKSGroupOperation alloc] init];
- self.viewSetupOperation.name = @"view-setup-group";
- if(!zoneSetupOperation.isFinished) {
- [self.viewSetupOperation runBeforeGroupFinished: zoneSetupOperation];
- }
+ // Not overly thread-safe, but a single read is okay
+ if(self.accountStatus != CKKSAccountStatusAvailable) {
+ // No CK account? goodbye!
+ ckksnotice("ckksreset", self, "Requesting reset of CK zone, but no CK account exists");
+ CKKSResultOperation* errorOp = [CKKSResultOperation named:@"fail" withBlockTakingSelf:^(CKKSResultOperation * _Nonnull op) {
+ op.error = [NSError errorWithDomain:CKKSErrorDomain
+ code:CKKSNotLoggedIn
+ description:@"User is not signed into iCloud."];
+ }];
+
+ [self scheduleOperationWithoutDependencies:errorOp];
+ return errorOp;
+ }
+
+ ckksnotice("ckksreset", self, "Requesting reset of CK zone (logged in)");
+
+ NSDictionary* localResetPath = @{
+ SecCKKSZoneKeyStateInitializing: @{
+ SecCKKSZoneKeyStateInitialized: [OctagonStateTransitionPathStep success],
+ SecCKKSZoneKeyStateLoggedOut: [OctagonStateTransitionPathStep success],
+ },
+ };
+
+ // If the zone delete doesn't work, try it up to two more times
+
+ return [self.stateMachine doWatchedStateMachineRPC:@"ckks-cloud-reset"
+ sourceStates:[NSSet setWithArray:@[
+ // TODO: possibly every state?
+ SecCKKSZoneKeyStateReady,
+ SecCKKSZoneKeyStateInitialized,
+ SecCKKSZoneKeyStateFetchComplete,
+ SecCKKSZoneKeyStateWaitForTLK,
+ SecCKKSZoneKeyStateWaitForTrust,
+ SecCKKSZoneKeyStateWaitForTLKUpload,
+ SecCKKSZoneKeyStateLoggedOut,
+ ]]
+ path:[OctagonStateTransitionPath pathFromDictionary:@{
+ SecCKKSZoneKeyStateResettingZone: @{
+ SecCKKSZoneKeyStateResettingLocalData: localResetPath,
+ SecCKKSZoneKeyStateResettingZone: @{
+ SecCKKSZoneKeyStateResettingLocalData: localResetPath,
+ SecCKKSZoneKeyStateResettingZone: @{
+ SecCKKSZoneKeyStateResettingLocalData: localResetPath,
+ }
+ }
+ }
+ }]
+ reply:^(NSError * _Nonnull error) {}];
+}
- [afterZoneSetup addDependency: zoneSetupOperation];
- [self.viewSetupOperation runBeforeGroupFinished: afterZoneSetup];
+- (void)keyStateMachineRequestProcess {
+ [self.stateMachine handleFlag:CKKSFlagKeyStateProcessRequested];
+}
+
+- (CKKSResultOperation*)createKeyStateReadyDependency:(NSString*)message {
+ WEAKIFY(self);
+ CKKSResultOperation* keyStateReadyDependency = [CKKSResultOperation operationWithBlock:^{
+ STRONGIFY(self);
+ ckksnotice("ckkskey", self, "CKKS became ready: %@", message);
+ }];
+ keyStateReadyDependency.name = [NSString stringWithFormat: @"%@-key-state-ready", self.zoneName];
+ keyStateReadyDependency.descriptionErrorCode = CKKSResultDescriptionPendingKeyReady;
+ return keyStateReadyDependency;
+}
- [self scheduleAccountStatusOperation: self.viewSetupOperation];
+- (void)_onqueuePokeKeyStateMachine
+{
+ dispatch_assert_queue(self.queue);
+ [self.stateMachine _onqueuePokeStateMachine];
}
-- (bool)_onqueueResetLocalData: (NSError * __autoreleasing *) error {
+- (CKKSResultOperation<OctagonStateTransitionOperationProtocol>* _Nullable)_onqueueNextStateMachineTransition:(OctagonState*)currentState
+ flags:(OctagonFlags*)flags
+ pendingFlags:(id<OctagonStateOnqueuePendingFlagHandler>)pendingFlagHandler
+{
dispatch_assert_queue(self.queue);
- NSError* localerror = nil;
- bool setError = false; // Ugly, but this is the only way to return the first error given
+ // Resetting back to 'loggedout' takes all precedence.
+ if([flags _onqueueContains:CKKSFlagCloudKitLoggedOut]) {
+ [flags _onqueueRemoveFlag:CKKSFlagCloudKitLoggedOut];
+ ckksnotice("ckkskey", self, "CK account is not present");
- 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;
- }
+ [self ensureKeyStateReadyDependency:@"cloudkit-account-not-present"];
+ return [[CKKSLocalResetOperation alloc] initWithDependencies:self.operationDependencies
+ intendedState:SecCKKSZoneKeyStateLoggedOut
+ errorState:SecCKKSZoneKeyStateError];
}
- [CKKSMirrorEntry deleteAll:self.zoneID error: &localerror];
- if(localerror) {
- ckkserror("ckks", self, "couldn't delete all CKKSMirrorEntry: %@", localerror);
- if(error && !setError) {
- *error = localerror; setError = true;
- }
+ if([flags _onqueueContains:CKKSFlagCloudKitZoneMissing]) {
+ [flags _onqueueRemoveFlag:CKKSFlagCloudKitZoneMissing];
+
+ [self ensureKeyStateReadyDependency:@"cloudkit-zone-missing"];
+ // The zone is gone! Let's reset our local state, which will feed into recreating the zone
+ return [OctagonStateTransitionOperation named:@"ck-zone-missing"
+ entering:SecCKKSZoneKeyStateResettingLocalData];
}
- [CKKSOutgoingQueueEntry deleteAll:self.zoneID error: &localerror];
- if(localerror) {
- ckkserror("ckks", self, "couldn't delete all CKKSOutgoingQueueEntry: %@", localerror);
- if(error && !setError) {
- *error = localerror; setError = true;
- }
+ if([flags _onqueueContains:CKKSFlagChangeTokenExpired]) {
+ [flags _onqueueRemoveFlag:CKKSFlagChangeTokenExpired];
+
+ [self ensureKeyStateReadyDependency:@"cloudkit-change-token-expired"];
+ // Our change token is invalid! We'll have to refetch the world, so let's delete everything locally.
+ return [OctagonStateTransitionOperation named:@"ck-token-expired"
+ entering:SecCKKSZoneKeyStateResettingLocalData];
}
- [CKKSIncomingQueueEntry deleteAll:self.zoneID error: &localerror];
- if(localerror) {
- ckkserror("ckks", self, "couldn't delete all CKKSIncomingQueueEntry: %@", localerror);
- if(error && !setError) {
- *error = localerror; setError = true;
+ if([currentState isEqualToString:SecCKKSZoneKeyStateLoggedOut]) {
+ if([flags _onqueueContains:CKKSFlagCloudKitLoggedIn] || self.accountStatus == CKKSAccountStatusAvailable) {
+ [flags _onqueueRemoveFlag:CKKSFlagCloudKitLoggedIn];
+
+ ckksnotice("ckkskey", self, "CloudKit account now present");
+ return [OctagonStateTransitionOperation named:@"ck-sign-in"
+ entering:SecCKKSZoneKeyStateInitializing];
}
- }
- [CKKSKey deleteAll:self.zoneID error: &localerror];
- if(localerror) {
- ckkserror("ckks", self, "couldn't delete all CKKSKey: %@", localerror);
- if(error && !setError) {
- *error = localerror; setError = true;
+ if([flags _onqueueContains:CKKSFlag24hrNotification]) {
+ [flags _onqueueRemoveFlag:CKKSFlag24hrNotification];
}
+ return nil;
}
- [CKKSCurrentKeyPointer deleteAll:self.zoneID error: &localerror];
- if(localerror) {
- ckkserror("ckks", self, "couldn't delete all CKKSCurrentKeyPointer: %@", localerror);
- if(error && !setError) {
- *error = localerror; setError = true;
+ if([currentState isEqualToString: SecCKKSZoneKeyStateWaitForCloudKitAccountStatus]) {
+ if([flags _onqueueContains:CKKSFlagCloudKitLoggedIn] || self.accountStatus == CKKSAccountStatusAvailable) {
+ [flags _onqueueRemoveFlag:CKKSFlagCloudKitLoggedIn];
+
+ ckksnotice("ckkskey", self, "CloudKit account now present");
+ return [OctagonStateTransitionOperation named:@"ck-sign-in"
+ entering:SecCKKSZoneKeyStateInitializing];
}
- }
- [CKKSDeviceStateEntry deleteAll:self.zoneID error:&localerror];
- if(localerror) {
- ckkserror("ckks", self, "couldn't delete all CKKSDeviceStateEntry: %@", localerror);
- if(error && !setError) {
- *error = localerror; setError = true;
+ if([flags _onqueueContains:CKKSFlagCloudKitLoggedOut]) {
+ [flags _onqueueRemoveFlag:CKKSFlagCloudKitLoggedOut];
+ ckksnotice("ckkskey", self, "No account available");
+
+ return [[CKKSLocalResetOperation alloc] initWithDependencies:self.operationDependencies
+ intendedState:SecCKKSZoneKeyStateLoggedOut
+ errorState:SecCKKSZoneKeyStateError];
}
+ return nil;
}
- [self _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateInitializing withError:nil];
+ [self.launch addEvent:currentState];
- return (localerror == nil && !setError);
-}
+ if([currentState isEqual:SecCKKSZoneKeyStateInitializing]) {
+ if(self.accountStatus == CKKSAccountStatusNoAccount) {
+ ckksnotice("ckkskey", self, "CloudKit account is missing. Departing!");
+ return [[CKKSLocalResetOperation alloc] initWithDependencies:self.operationDependencies
+ intendedState:SecCKKSZoneKeyStateLoggedOut
+ errorState:SecCKKSZoneKeyStateError];
+ }
-- (CKKSResultOperation*)resetLocalData {
- __weak __typeof(self) weakSelf = self;
+ // Begin zone creation, but rate-limit it
+ CKKSCreateCKZoneOperation* pendingInitializeOp = [[CKKSCreateCKZoneOperation alloc] initWithDependencies:self.operationDependencies
+ intendedState:SecCKKSZoneKeyStateInitialized
+ errorState:SecCKKSZoneKeyStateZoneCreationFailed];
+ [pendingInitializeOp addNullableDependency:self.operationDependencies.zoneModifier.cloudkitRetryAfter.operationDependency];
+ [self.operationDependencies.zoneModifier.cloudkitRetryAfter trigger];
+
+ return pendingInitializeOp;
+ }
- CKKSGroupOperation* resetFollowUp = [[CKKSGroupOperation alloc] init];
- resetFollowUp.name = @"local-reset-follow-up-group";
- __weak __typeof(resetFollowUp) weakResetFollowUp = resetFollowUp;
+ if([currentState isEqualToString:SecCKKSZoneKeyStateWaitForFixupOperation]) {
+ // TODO: fixup operations should become part of the state machine
+ ckksnotice("ckkskey", self, "Waiting for the fixup operation: %@", self.lastFixupOperation);
+ OctagonStateTransitionOperation* op = [OctagonStateTransitionOperation named:@"wait-for-fixup" entering:SecCKKSZoneKeyStateInitialized];
+ [op addNullableDependency:self.lastFixupOperation];
+ return op;
+ }
- CKKSResultOperation* op = [[CKKSResultOperation alloc] init];
- op.name = @"local-reset";
+ if([currentState isEqualToString:SecCKKSZoneKeyStateInitialized]) {
+ // We're initialized and CloudKit is ready. If we're trusted, see what needs done. Otherwise, wait.
+ return [self performInitializedOperation];
+ }
- __weak __typeof(op) weakOp = op;
- [op addExecutionBlock:^{
- __strong __typeof(self) strongSelf = weakSelf;
- __strong __typeof(op) strongOp = weakOp;
- __strong __typeof(resetFollowUp) strongResetFollowUp = weakResetFollowUp;
- if(!strongSelf || !strongOp || !strongResetFollowUp) {
- return;
+ // In error? You probably aren't getting out.
+ if([currentState isEqualToString:SecCKKSZoneKeyStateError]) {
+ if([flags _onqueueContains:CKKSFlagCloudKitLoggedIn]) {
+ [flags _onqueueRemoveFlag:CKKSFlagCloudKitLoggedIn];
+
+ // Worth one last shot. Reset everything locally, and try again.
+ return [[CKKSLocalResetOperation alloc] initWithDependencies:self.operationDependencies
+ intendedState:SecCKKSZoneKeyStateInitializing
+ errorState:SecCKKSZoneKeyStateError];
}
- __block NSError* error = nil;
+ ckkserror("ckkskey", self, "Staying in error state %@", currentState);
+ return nil;
+ }
- [strongSelf dispatchSync: ^bool{
- [self _onqueueResetLocalData: &error];
- return true;
- }];
+ if([currentState isEqualToString:SecCKKSZoneKeyStateResettingZone]) {
+ ckksnotice("ckkskey", self, "Deleting the CloudKit Zone");
- [strongSelf resetSetup];
+ [self ensureKeyStateReadyDependency:@"ck-zone-reset"];
+ return [[CKKSDeleteCKZoneOperation alloc] initWithDependencies:self.operationDependencies
+ intendedState:SecCKKSZoneKeyStateResettingLocalData
+ errorState:SecCKKSZoneKeyStateResettingZone];
+ }
- if(error) {
- ckksnotice("ckksreset", strongSelf, "Local reset finished with error %@", error);
- strongOp.error = error;
- } else {
- if(strongSelf.accountStatus == CKKSAccountStatusAvailable) {
- // Since we're logged in, we expect a reset to fix up the key hierarchy
- ckksnotice("ckksreset", strongSelf, "logged in; re-initializing zone");
- [strongSelf initializeZone];
+ if([currentState isEqualToString:SecCKKSZoneKeyStateResettingLocalData]) {
+ ckksnotice("ckkskey", self, "Resetting local data");
- ckksnotice("ckksreset", strongSelf, "waiting for key hierarchy to become ready");
- CKKSResultOperation* waitOp = [CKKSResultOperation named:@"waiting-for-key-hierarchy" withBlock:^{}];
- [waitOp timeout: 60*NSEC_PER_SEC];
- [waitOp addNullableDependency:strongSelf.keyStateReadyDependency];
+ [self ensureKeyStateReadyDependency:@"local-data-reset"];
+ return [[CKKSLocalResetOperation alloc] initWithDependencies:self.operationDependencies
+ intendedState:SecCKKSZoneKeyStateInitializing
+ errorState:SecCKKSZoneKeyStateError];
+ }
- [strongResetFollowUp runBeforeGroupFinished:waitOp];
- } else {
- ckksnotice("ckksreset", strongSelf, "logged out; not initializing zone");
- }
- }
- }];
+ if([currentState isEqualToString:SecCKKSZoneKeyStateZoneCreationFailed]) {
+ //Prepare to go back into initializing, as soon as the cloudkitRetryAfter is happy
+ OctagonStateTransitionOperation* op = [OctagonStateTransitionOperation named:@"recover-from-cloudkit-failure" entering:SecCKKSZoneKeyStateInitializing];
- [resetFollowUp runBeforeGroupFinished:op];
- [self scheduleOperationWithoutDependencies:resetFollowUp];
- return resetFollowUp;
-}
+ [op addNullableDependency:self.operationDependencies.zoneModifier.cloudkitRetryAfter.operationDependency];
+ [self.operationDependencies.zoneModifier.cloudkitRetryAfter trigger];
-- (CKKSResultOperation*)resetCloudKitZone {
- if(!SecCKKSIsEnabled()) {
- ckksinfo("ckks", self, "Skipping CloudKit reset due to disabled CKKS");
- return nil;
+ return op;
}
- CKKSResultOperation* reset = [super beginResetCloudKitZoneOperation];
+ if([currentState isEqualToString:SecCKKSZoneKeyStateLoseTrust]) {
+ if([flags _onqueueContains:CKKSFlagBeginTrustedOperation]) {
+ [flags _onqueueRemoveFlag:CKKSFlagBeginTrustedOperation];
+ // This was likely a race between some operation and the beginTrustedOperation call! Skip changing state and try again.
+ return [OctagonStateTransitionOperation named:@"begin-trusted-operation" entering:SecCKKSZoneKeyStateInitialized];
+ }
- __weak __typeof(self) weakSelf = self;
- CKKSGroupOperation* resetFollowUp = [[CKKSGroupOperation alloc] init];
- resetFollowUp.name = @"cloudkit-reset-follow-up-group";
+ // If our current state is "trusted", fall out
+ if(self.trustStatus == CKKSAccountStatusAvailable) {
+ self.trustStatus = CKKSAccountStatusUnknown;
+ }
+ return [OctagonStateTransitionOperation named:@"trust-loss" entering:SecCKKSZoneKeyStateWaitForTrust];
+ }
- __weak __typeof(resetFollowUp) weakResetFollowUp = resetFollowUp;
- [resetFollowUp runBeforeGroupFinished: [CKKSResultOperation named:@"cloudkit-reset-follow-up" withBlock: ^{
- __strong __typeof(weakSelf) strongSelf = weakSelf;
- if(!strongSelf) {
- ckkserror("ckks", strongSelf, "received callback for released object");
- return;
+ if([currentState isEqualToString:SecCKKSZoneKeyStateWaitForTrust]) {
+ if(self.trustStatus == CKKSAccountStatusAvailable) {
+ ckksnotice("ckkskey", self, "Beginning trusted state machine operation");
+ return [OctagonStateTransitionOperation named:@"begin-trusted-operation" entering:SecCKKSZoneKeyStateInitialized];
}
- __strong __typeof(resetFollowUp) strongResetFollowUp = weakResetFollowUp;
- if(!reset.error) {
- ckksnotice("ckks", strongSelf, "Successfully deleted zone %@", strongSelf.zoneName);
- __block NSError* error = nil;
+ if([flags _onqueueContains:CKKSFlagKeyStateProcessRequested]) {
+ [flags _onqueueRemoveFlag:CKKSFlagKeyStateProcessRequested];
+ return [OctagonStateTransitionOperation named:@"begin-trusted-operation" entering:SecCKKSZoneKeyStateProcess];
+ }
- [strongSelf dispatchSync: ^bool{
- [strongSelf _onqueueResetLocalData: &error];
- strongSelf.setupSuccessful = false;
- return true;
- }];
+ if([flags _onqueueContains:CKKSFlag24hrNotification]) {
+ [flags _onqueueRemoveFlag:CKKSFlag24hrNotification];
+ }
- if(strongSelf.accountStatus == CKKSAccountStatusAvailable) {
- // Since we're logged in, we expect a reset to fix up the key hierarchy
- ckksnotice("ckksreset", strongSelf, "re-initializing zone");
- [strongSelf initializeZone];
+ return nil;
+ }
- ckksnotice("ckksreset", strongSelf, "waiting for key hierarchy to become ready");
- CKKSResultOperation* waitOp = [CKKSResultOperation named:@"waiting-for-reset" withBlock:^{}];
- [waitOp timeout: 60*NSEC_PER_SEC];
- [waitOp addNullableDependency:strongSelf.keyStateReadyDependency];
+ if([currentState isEqualToString:SecCKKSZoneKeyStateBecomeReady]) {
+ return [[CKKSCheckKeyHierarchyOperation alloc] initWithDependencies:self.operationDependencies
+ intendedState:SecCKKSZoneKeyStateReady
+ errorState:SecCKKSZoneKeyStateError];
+ }
- [strongResetFollowUp runBeforeGroupFinished:waitOp];
- } else {
- ckksnotice("ckksreset", strongSelf, "logged out; not initializing zone");
- }
- } else {
- // Shouldn't ever happen, since reset is a successDependency
- ckkserror("ckks", strongSelf, "Couldn't reset zone %@: %@", strongSelf.zoneName, reset.error);
+ if([currentState isEqualToString:SecCKKSZoneKeyStateReady]) {
+ // If we're ready, we can ignore the begin trusted flag
+ [flags _onqueueRemoveFlag:CKKSFlagBeginTrustedOperation];
+
+ if(self.keyStateFullRefetchRequested) {
+ // In ready, but something has requested a full refetch.
+ ckksnotice("ckkskey", self, "Kicking off a full key refetch based on request:%d", self.keyStateFullRefetchRequested);
+ [self ensureKeyStateReadyDependency:@"key-state-full-refetch"];
+ return [OctagonStateTransitionOperation named:@"full-refetch" entering:SecCKKSZoneKeyStateNeedFullRefetch];
}
- }]];
- [resetFollowUp addSuccessDependency:reset];
- [self scheduleOperationWithoutDependencies:resetFollowUp];
- return resetFollowUp;
-}
+ if([flags _onqueueContains:CKKSFlagFetchRequested]) {
+ [flags _onqueueRemoveFlag:CKKSFlagFetchRequested];
+ ckksnotice("ckkskey", self, "Kicking off a key refetch based on request");
+ [self ensureKeyStateReadyDependency:@"key-state-fetch"];
+ return [OctagonStateTransitionOperation named:@"fetch-requested" entering:SecCKKSZoneKeyStateBeginFetch];
+ }
-- (void)advanceKeyStateMachine {
- __weak __typeof(self) weakSelf = self;
+ if([flags _onqueueContains:CKKSFlagKeyStateProcessRequested]) {
+ [flags _onqueueRemoveFlag:CKKSFlagKeyStateProcessRequested];
+ ckksnotice("ckkskey", self, "Kicking off a key reprocess based on request");
+ [self ensureKeyStateReadyDependency:@"key-state-process"];
+ return [OctagonStateTransitionOperation named:@"key-process" entering:SecCKKSZoneKeyStateProcess];
+ }
- [self dispatchAsync: ^bool{
- __strong __typeof(weakSelf) strongSelf = weakSelf;
- if(!strongSelf) {
- ckkserror("ckks", strongSelf, "received callback for released object");
- false;
+ if(self.trustStatus != CKKSAccountStatusAvailable) {
+ ckksnotice("ckkskey", self, "In ready, but there's no trust; going into waitfortrust");
+ [self ensureKeyStateReadyDependency:@"trust loss"];
+ return [OctagonStateTransitionOperation named:@"trust-gone" entering:SecCKKSZoneKeyStateLoseTrust];
}
- [strongSelf _onqueueAdvanceKeyStateMachineToState: nil withError: nil];
- return true;
- }];
-};
+ if([flags _onqueueContains:CKKSFlagTrustedPeersSetChanged]) {
+ [flags _onqueueRemoveFlag:CKKSFlagTrustedPeersSetChanged];
+ ckksnotice("ckkskey", self, "Received a nudge that the trusted peers set might have changed! Reprocessing.");
+ [self ensureKeyStateReadyDependency:@"Peer set changed"];
+ return [OctagonStateTransitionOperation named:@"trusted-peers-changed" entering:SecCKKSZoneKeyStateProcess];
+ }
-- (void)_onqueueKeyStateMachineRequestFetch {
- dispatch_assert_queue(self.queue);
+ if([flags _onqueueContains:CKKSFlag24hrNotification]) {
+ [flags _onqueueRemoveFlag:CKKSFlag24hrNotification];
- // 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.
+ // We'd like to trigger our 24-hr backup fetch and scan.
+ // That's currently part of the Initialized state, so head that way
+ return [OctagonStateTransitionOperation named:@"24-hr-check" entering:SecCKKSZoneKeyStateInitialized];
+ }
- self.keyStateFetchRequested = true;
- [self _onqueueAdvanceKeyStateMachineToState: nil withError: nil];
-}
+ if([flags _onqueueContains:CKKSFlagItemReencryptionNeeded]) {
+ [flags _onqueueRemoveFlag:CKKSFlagItemReencryptionNeeded];
-- (void)_onqueueKeyStateMachineRequestFullRefetch {
- dispatch_assert_queue(self.queue);
+ // TODO: this should be part of the state machine
+ CKKSReencryptOutgoingItemsOperation* op = [[CKKSReencryptOutgoingItemsOperation alloc] initWithDependencies:self.operationDependencies
+ ckks:self
+ intendedState:SecCKKSZoneKeyStateReady
+ errorState:SecCKKSZoneKeyStateError];
+ [self scheduleOperation:op];
+ // fall through.
+ }
- self.keyStateFullRefetchRequested = true;
- [self _onqueueAdvanceKeyStateMachineToState: nil withError: nil];
-}
+ if([flags _onqueueContains:CKKSFlagProcessIncomingQueue]) {
+ [flags _onqueueRemoveFlag:CKKSFlagProcessIncomingQueue];
+ // TODO: this should be part of the state machine
-- (void)keyStateMachineRequestProcess {
- __weak __typeof(self) weakSelf = self;
- [self dispatchAsync: ^bool{
- __strong __typeof(weakSelf) strongSelf = weakSelf;
- if(!strongSelf) {
- ckkserror("ckks", strongSelf, "received callback for released object");
- return false;
+ [self processIncomingQueue:true];
+ //return [OctagonStateTransitionOperation named:@"process-outgoing" entering:SecCKKSZoneKeyStateProcessIncomingQueue];
}
- [strongSelf _onqueueKeyStateMachineRequestProcess];
- return true;
- }];
-}
+ if([flags _onqueueContains:CKKSFlagScanLocalItems]) {
+ [flags _onqueueRemoveFlag:CKKSFlagScanLocalItems];
+ ckksnotice("ckkskey", self, "Launching a scan operation to find dropped items");
-- (void)_onqueueKeyStateMachineRequestProcess {
- dispatch_assert_queue(self.queue);
+ // TODO: this should be a state flow
+ [self scanLocalItems:@"per-request"];
+ // fall through
+ }
- // Set the request 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.
+ if([flags _onqueueContains:CKKSFlagProcessOutgoingQueue]) {
+ [flags _onqueueRemoveFlag:CKKSFlagProcessOutgoingQueue];
- self.keyStateProcessRequested = true;
- [self _onqueueAdvanceKeyStateMachineToState: nil withError: nil];
-}
+ [self processOutgoingQueue:nil];
+ // TODO: this should be a state flow.
+ //return [OctagonStateTransitionOperation named:@"process-outgoing" entering:SecCKKSZoneKeyStateProcessOutgoingQueue];
+ // fall through
+ }
-// The operations suggested by this state machine should call _onqueueAdvanceKeyStateMachineToState once they are complete.
-// At no other time should keyHierarchyState be modified.
+ // TODO: kick off a key roll if one has been requested
-// 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 'initializing' takes all precedence.
- if([state isEqual: SecCKKSZoneKeyStateInitializing]) {
- ckksnotice("ckkskey", self, "Resetting the key hierarchy state machine back to 'initializing'");
+ // If we reach this point, we're in ready, and will stay there.
+ // Tell the launch and the viewReadyScheduler about that.
- [self.keyStateMachineOperation cancel];
- self.keyStateMachineOperation = nil;
+ [self.launch launch];
- self.keyHierarchyState = SecCKKSZoneKeyStateInitializing;
- self.keyHierarchyError = nil;
- self.keyStateFetchRequested = false;
- self.keyStateProcessRequested = false;
+ [[CKKSAnalytics logger] setDateProperty:[NSDate date] forKey:CKKSAnalyticsLastKeystateReady zoneName:self.zoneName];
+ if(self.keyStateReadyDependency) {
+ [self scheduleOperation:self.keyStateReadyDependency];
+ self.keyStateReadyDependency = nil;
+ }
- self.keyHierarchyOperationGroup = [CKOperationGroup CKKSGroupWithName:@"key-state-reset"];
- self.keyStateReadyDependency = [CKKSResultOperation operationWithBlock:^{
- ckksnotice("ckkskey", weakSelf, "Key state has become ready for the first time (after reset).");
- }];
- self.keyStateReadyDependency.name = [NSString stringWithFormat: @"%@-key-state-ready", self.zoneName];
- return;
+ return nil;
}
- // 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 %@: %@", self.keyHierarchyState, self.keyHierarchyError);
- return;
- }
+ if([currentState isEqualToString:SecCKKSZoneKeyStateReadyPendingUnlock]) {
+ if([flags _onqueueContains:CKKSFlagDeviceUnlocked]) {
+ [flags _onqueueRemoveFlag:CKKSFlagDeviceUnlocked];
+ [self ensureKeyStateReadyDependency:@"Device unlocked"];
+ return [OctagonStateTransitionOperation named:@"key-state-ready-after-unlock" entering:SecCKKSZoneKeyStateBecomeReady];
+ }
- if(error != nil || [state isEqual: SecCKKSZoneKeyStateError]) {
- // But wait! Is this a "we're locked" error?
- if([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.keyStateMachineOperation addNullableDependency:self.lockStateTracker.unlockDependency];
- [self scheduleOperation:self.keyStateMachineOperation];
+ if([flags _onqueueContains:CKKSFlagProcessOutgoingQueue]) {
+ [flags _onqueueRemoveFlag:CKKSFlagProcessOutgoingQueue];
+ [self processOutgoingQueue:nil];
+ // TODO: this should become part of the key state hierarchy
+ }
- } else {
- // Error state: record the error and exit early
- ckkserror("ckkskey", self, "advised of error: coming from state (%@): %@", self.keyHierarchyState, error);
- self.keyHierarchyState = SecCKKSZoneKeyStateError;
- self.keyHierarchyError = error;
- return;
+ // Ready enough!
+
+ [[CKKSAnalytics logger] setDateProperty:[NSDate date] forKey:CKKSAnalyticsLastKeystateReady zoneName:self.zoneName];
+ if(self.keyStateReadyDependency) {
+ [self scheduleOperation:self.keyStateReadyDependency];
+ self.keyStateReadyDependency = nil;
}
+
+ OctagonPendingFlag* unlocked = [[OctagonPendingFlag alloc] initWithFlag:CKKSFlagDeviceUnlocked
+ conditions:OctagonPendingConditionsDeviceUnlocked];
+ [pendingFlagHandler _onqueueHandlePendingFlag:unlocked];
+ return nil;
}
- if([state isEqual: SecCKKSZoneKeyStateCancelled]) {
- ckkserror("ckkskey", self, "advised of cancel: coming from state (%@): %@", self.keyHierarchyState, error);
- self.keyHierarchyState = SecCKKSZoneKeyStateCancelled;
- self.keyHierarchyError = error;
+ if([currentState isEqualToString:SecCKKSZoneKeyStateBeginFetch]) {
+ ckksnotice("ckkskey", self, "Starting a key hierarchy fetch");
+ [flags _onqueueRemoveFlag:CKKSFlagFetchComplete];
- // 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;
- return;
- }
+ WEAKIFY(self);
- // Now that the current or new state isn't an error or a cancel, proceed.
+ NSSet<CKKSFetchBecause*>* fetchReasons = self.currentFetchReasons ?
+ [self.currentFetchReasons setByAddingObject:CKKSFetchBecauseKeyHierarchy] :
+ [NSSet setWithObject:CKKSFetchBecauseKeyHierarchy];
- 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;
- }
+ CKKSResultOperation* fetchOp = [self.zoneChangeFetcher requestSuccessfulFetchForManyReasons:fetchReasons];
+ CKKSResultOperation* flagOp = [CKKSResultOperation named:@"post-fetch"
+ withBlock:^{
+ STRONGIFY(self);
+ [self.stateMachine handleFlag:CKKSFlagFetchComplete];
+ }];
+ [flagOp addDependency:fetchOp];
+ [self scheduleOperation:flagOp];
+
+ return [OctagonStateTransitionOperation named:@"waiting-for-fetch" entering:SecCKKSZoneKeyStateFetch];
}
- if(state) {
- self.keyStateMachineOperation = nil;
+ if([currentState isEqualToString:SecCKKSZoneKeyStateFetch]) {
+ if([flags _onqueueContains:CKKSFlagFetchComplete]) {
+ [flags _onqueueRemoveFlag:CKKSFlagFetchComplete];
+ return [OctagonStateTransitionOperation named:@"fetch-complete" entering:SecCKKSZoneKeyStateFetchComplete];
+ }
+
+ // The flags CKKSFlagCloudKitZoneMissing and CKKSFlagChangeTokenOutdated are both handled at the top of this function
+ // So, we don't need to handle them here.
- ckksnotice("ckkskey", self, "Advancing key hierarchy state machine from %@ to %@", self.keyHierarchyState, state);
- self.keyHierarchyState = state;
+ return 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];
+ if([currentState isEqualToString:SecCKKSZoneKeyStateNeedFullRefetch]) {
+ ckksnotice("ckkskey", self, "Starting a key hierarchy full refetch");
+
+ //TODO use states here instead of flags
+ self.keyStateMachineRefetched = true;
+ self.keyStateFullRefetchRequested = false;
- // We also are checking for OutgoingQueueEntries in the reencrypt state; this is a sign that our key hierarchy is out of date.
- NSArray<CKKSOutgoingQueueEntry*>* outdatedOQEs = [CKKSOutgoingQueueEntry allInState: SecCKKSStateReencrypt zoneID:self.zoneID error: &localerror];
+ return [OctagonStateTransitionOperation named:@"fetch-complete" entering:SecCKKSZoneKeyStateResettingLocalData];
+ }
- SecADSetValueForScalarKey((__bridge CFStringRef) SecCKKSAggdViewKeyCount, [localKeys count]);
+ if([currentState isEqualToString:SecCKKSZoneKeyStateFetchComplete]) {
+ [self.launch addEvent:@"fetch-complete"];
+ [self.currentFetchReasons removeAllObjects];
- if(localerror) {
- ckkserror("ckkskey", self, "couldn't fetch keys and OQEs from local database, entering error state: %@", localerror);
- self.keyHierarchyState = SecCKKSZoneKeyStateError;
- self.keyHierarchyError = localerror;
- return;
+ return [OctagonStateTransitionOperation named:@"post-fetch-process" entering:SecCKKSZoneKeyStateProcess];
}
-#if !defined(NDEBUG)
- NSArray<CKKSKey*>* allKeys = [CKKSKey allKeys:self.zoneID error:&localerror];
- ckksdebug("ckkskey", self, "All keys: %@", allKeys);
-#endif
+ if([currentState isEqualToString:SecCKKSZoneKeyStateWaitForTLKCreation]) {
+ if([flags _onqueueContains:CKKSFlagKeyStateProcessRequested]) {
+ [flags _onqueueRemoveFlag:CKKSFlagKeyStateProcessRequested];
+ ckksnotice("ckkskey", self, "We believe we need to create TLKs but we also received a key nudge; moving to key state Process.");
+ return [OctagonStateTransitionOperation named:@"wait-for-tlk-creation-process" entering:SecCKKSZoneKeyStateProcess];
- NSError* hierarchyError = nil;
+ } else if([flags _onqueueContains:CKKSFlagFetchRequested]) {
+ [flags _onqueueRemoveFlag:CKKSFlagFetchRequested];
+ return [OctagonStateTransitionOperation named:@"fetch-requested" entering:SecCKKSZoneKeyStateBeginFetch];
- if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateInitializing]) {
- if(state != nil) {
- // Wait for CKKSZone to finish initialization.
- ckkserror("ckkskey", self, "Asked to advance state machine (to %@) while CK zone still initializing.", state);
- }
- return;
+ } else if([flags _onqueueContains:CKKSFlagTLKCreationRequested]) {
+ [flags _onqueueRemoveFlag:CKKSFlagTLKCreationRequested];
- } else if([self.keyHierarchyState 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]);
- [self _onqueueKeyHierarchyProcess];
+ // It's very likely that we're already untrusted at this point. But, sometimes we will be trusted right now, and can lose trust while waiting for the upload.
+ // This probably should be handled by a state increase.
+ [flags _onqueueRemoveFlag:CKKSFlagEndTrustedOperation];
- } else if(self.keyStateFullRefetchRequested) {
- // In ready, but someone has requested a full fetch. Kick it off.
- ckksnotice("ckkskey", self, "Kicking off a key refetch based on request:%d", self.keyStateFetchRequested);
- [self _onqueueKeyHierarchyRefetch];
+ ckksnotice("ckkskey", self, "TLK creation requested; kicking off operation");
+ return [[CKKSNewTLKOperation alloc] initWithDependencies:self.operationDependencies
+ ckks:self];
+ } else if(self.lastNewTLKOperation.keyset) {
+ // This means that we _have_ created new TLKs, and should wait for them to be uploaded. This is ugly and should probably be done with more states.
+ return [OctagonStateTransitionOperation named:@"" entering:SecCKKSZoneKeyStateWaitForTLKUpload];
- } 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);
- [self _onqueueKeyHierarchyFetch];
+ } else {
+ ckksnotice("ckkskey", self, "We believe we need to create TLKs; waiting for Octagon (via %@)", self.suggestTLKUpload);
+ [self.suggestTLKUpload trigger];
}
- // TODO: kick off a key roll if one has been requested
+ }
- if(!self.keyStateMachineOperation) {
- // We think we're ready. Double check.
- bool ready = [self _onqueueEnsureKeyHierarchyHealth:&hierarchyError];
- if(!ready || hierarchyError) {
- // Things is bad. Kick off a heal to fix things up.
- ckksnotice("ckkskey", self, "Thought we were ready, but the key hierarchy is unhealthy: %@", hierarchyError);
- self.keyHierarchyState = SecCKKSZoneKeyStateUnhealthy;
+ if([currentState isEqualToString:SecCKKSZoneKeyStateWaitForTLKUpload]) {
+ ckksnotice("ckkskey", self, "We believe we have TLKs that need uploading");
- } else {
- // In ready, nothing to do. Notify waiters and quit.
- self.keyHierarchyOperationGroup = nil;
- if(self.keyStateReadyDependency) {
- [self scheduleOperation: self.keyStateReadyDependency];
- self.keyStateReadyDependency = nil;
- }
+ if([flags _onqueueContains:CKKSFlagFetchRequested]) {
+ ckksnotice("ckkskey", self, "Received a nudge to refetch CKKS");
+ return [OctagonStateTransitionOperation named:@"tlk-upload-refetch" entering:SecCKKSZoneKeyStateBeginFetch];
+ }
- // If there are any OQEs waiting to be encrypted, launch an op to fix them
- if([outdatedOQEs count] > 0u) {
- ckksnotice("ckksreencrypt", self, "Reencrypting outgoing items as the key hierarchy is ready");
- CKKSReencryptOutgoingItemsOperation* op = [[CKKSReencryptOutgoingItemsOperation alloc] initWithCKKSKeychainView:self ckoperationGroup:self.keyHierarchyOperationGroup];
- [self scheduleOperation:op];
- }
+ if([flags _onqueueContains:CKKSFlagKeyStateTLKsUploaded]) {
+ [flags _onqueueRemoveFlag:CKKSFlagKeyStateTLKsUploaded];
- return;
- }
+ return [OctagonStateTransitionOperation named:@"wait-for-tlk-upload-process" entering:SecCKKSZoneKeyStateProcess];
}
- } else if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateInitialized]) {
- // We're initialized and CloudKit is ready. See what needs done...
+ if([flags _onqueueContains:CKKSFlagEndTrustedOperation]) {
+ [flags _onqueueRemoveFlag:CKKSFlagEndTrustedOperation];
- // Check if we have an existing key hierarchy
- CKKSKey* tlk = [CKKSKey currentKeyForClass:SecCKKSKeyClassTLK zoneID:self.zoneID error:&error];
- CKKSKey* classA = [CKKSKey currentKeyForClass:SecCKKSKeyClassA zoneID:self.zoneID error:&error];
- CKKSKey* classC = [CKKSKey currentKeyForClass:SecCKKSKeyClassC zoneID:self.zoneID error:&error];
+ return [OctagonStateTransitionOperation named:@"trust-loss" entering:SecCKKSZoneKeyStateLoseTrust];
+ }
- if(error && !([error.domain isEqual: @"securityd"] && error.code == errSecItemNotFound)) {
- ckkserror("ckkskey", self, "Error examining existing key hierarchy: %@", error);
+ if([flags _onqueueContains:CKKSFlagKeyStateProcessRequested]) {
+ return [OctagonStateTransitionOperation named:@"wait-for-tlk-fetch-process" entering:SecCKKSZoneKeyStateProcess];
}
- if(tlk && classA && classC && !error) {
- // This is likely a restart of securityd, and we think we're ready. Double check.
- bool ready = [self _onqueueEnsureKeyHierarchyHealth:&hierarchyError];
- if(ready && !hierarchyError) {
- ckksnotice("ckkskey", self, "Already have existing key hierarchy for %@; using it.", self.zoneID.zoneName);
- } else if(hierarchyError && [self.lockStateTracker isLockedError:hierarchyError]) {
- ckksnotice("ckkskey", self, "Initial scan shows key hierarchy is unavailable since keychain is locked: %@", hierarchyError);
- self.keyHierarchyState = SecCKKSZoneKeyStateWaitForUnlock;
- } else {
- ckksnotice("ckkskey", self, "Initial scan shows key hierarchy is unhealthy: %@", hierarchyError);
- self.keyHierarchyState = SecCKKSZoneKeyStateUnhealthy;
- }
+ // This is quite the hack, but it'll do for now.
+ [self.operationDependencies provideKeySet:self.lastNewTLKOperation.keyset];
- } 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);
+ ckksnotice("ckkskey", self, "Notifying Octagon again, just in case");
+ [self.suggestTLKUpload trigger];
+ }
- [self _onqueueKeyHierarchyFetch];
- }
+ if([currentState isEqualToString:SecCKKSZoneKeyStateTLKMissing]) {
+ return [self tlkMissingOperation:SecCKKSZoneKeyStateWaitForTLK];
+ }
+
+ if([currentState isEqualToString:SecCKKSZoneKeyStateWaitForTLK]) {
+ // We're in a hold state: waiting for the TLK bytes to arrive.
- } else if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateFetchComplete]) {
- // We're initializing this zone, and 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'");
- self.keyHierarchyState = SecCKKSZoneKeyStateUnhealthy;
+ if([flags _onqueueContains:CKKSFlagKeyStateProcessRequested]) {
+ [flags _onqueueRemoveFlag:CKKSFlagKeyStateProcessRequested];
+ // Someone has requsted a reprocess! Go to the correct state.
+ ckksnotice("ckkskey", self, "Received a nudge that our TLK might be here! Reprocessing.");
+ return [OctagonStateTransitionOperation named:@"wait-for-tlk-process" entering:SecCKKSZoneKeyStateProcess];
- } else if([remoteKeys count] == 0) {
- // No keys, no pointers? make some new ones!
- self.keyStateMachineOperation = [[CKKSNewTLKOperation alloc] initWithCKKSKeychainView: self ckoperationGroup:self.keyHierarchyOperationGroup];
+ } else if([flags _onqueueContains:CKKSFlagTrustedPeersSetChanged]) {
+ [flags _onqueueRemoveFlag:CKKSFlagTrustedPeersSetChanged];
+
+ // 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.");
+ return [OctagonStateTransitionOperation named:@"wait-for-tlk-peers" entering:SecCKKSZoneKeyStateProcess];
}
- } else if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateWaitForTLK]) {
- // We're in a hold state: waiting for the TLK bytes to arrive.
+ return nil;
+ }
- if(self.keyStateProcessRequested) {
- // Someone has requsted a reprocess! Run a ProcessReceivedKeysOperation.
- ckksnotice("ckkskey", self, "Received a nudge that our TLK might be here! Starting operation to check.");
- [self _onqueueKeyHierarchyProcess];
+ if([currentState isEqualToString:SecCKKSZoneKeyStateWaitForUnlock]) {
+ ckksnotice("ckkskey", self, "Requested to enter waitforunlock");
+
+ if([flags _onqueueContains:CKKSFlagDeviceUnlocked ]) {
+ [flags _onqueueRemoveFlag:CKKSFlagDeviceUnlocked];
+ return [OctagonStateTransitionOperation named:@"key-state-after-unlock" entering:SecCKKSZoneKeyStateInitialized];
}
- } else if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateWaitForUnlock]) {
- // We're in a hold state: waiting for the keybag to unlock so we can process the keys again.
+ OctagonPendingFlag* unlocked = [[OctagonPendingFlag alloc] initWithFlag:CKKSFlagDeviceUnlocked
+ conditions:OctagonPendingConditionsDeviceUnlocked];
+ [pendingFlagHandler _onqueueHandlePendingFlag:unlocked];
- [self _onqueueKeyHierarchyProcess];
- [self.keyStateMachineOperation addNullableDependency: self.lockStateTracker.unlockDependency];
+ return nil;
+ }
- } else if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateBadCurrentPointers]) {
+ if([currentState 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];
+ return [[CKKSHealKeyHierarchyOperation alloc] initWithDependencies:self.operationDependencies
+ ckks:self
+ intending:SecCKKSZoneKeyStateBecomeReady
+ errorState:SecCKKSZoneKeyStateError];
+ }
- } else if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateNewTLKsFailed]) {
+ if([currentState isEqualToString:SecCKKSZoneKeyStateNewTLKsFailed]) {
ckksnotice("ckkskey", self, "Creating new TLKs didn't work. Attempting to refetch!");
- [self _onqueueKeyHierarchyFetch];
-
- } else if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateNeedFullRefetch]) {
- ckksnotice("ckkskey", self, "Informed of request for full refetch");
- [self _onqueueKeyHierarchyRefetch];
-
- } else {
- ckkserror("ckks", self, "asked to advance state machine to unknown state: %@", self.keyHierarchyState);
- return;
+ return [OctagonStateTransitionOperation named:@"new-tlks-failed" entering:SecCKKSZoneKeyStateBeginFetch];
}
- if(self.keyStateMachineOperation) {
+ if([currentState isEqualToString:SecCKKSZoneKeyStateHealTLKSharesFailed]) {
+ ckksnotice("ckkskey", self, "Creating new TLK shares didn't work. Attempting to refetch!");
+ return [OctagonStateTransitionOperation named:@"heal-tlks-failed" entering:SecCKKSZoneKeyStateBeginFetch];
+ }
- if(self.keyStateReadyDependency == nil || [self.keyStateReadyDependency isFinished]) {
- ckksnotice("ckkskey", self, "reloading keyStateReadyDependency due to operation %@", self.keyStateMachineOperation);
+ if([currentState isEqualToString:SecCKKSZoneKeyStateUnhealthy]) {
+ if(self.trustStatus != CKKSAccountStatusAvailable) {
+ ckksnotice("ckkskey", self, "Looks like the key hierarchy is unhealthy, but we're untrusted.");
+ return [OctagonStateTransitionOperation named:@"unhealthy-lacking-trust" entering:SecCKKSZoneKeyStateLoseTrust];
- __weak __typeof(self) weakSelf = self;
- self.keyHierarchyOperationGroup = [CKOperationGroup CKKSGroupWithName:@"key-state-broken"];
- self.keyStateReadyDependency = [CKKSResultOperation operationWithBlock:^{
- ckksnotice("ckkskey", weakSelf, "Key state has become ready again.");
- }];
- self.keyStateReadyDependency.name = [NSString stringWithFormat: @"%@-key-state-ready", self.zoneName];
+ } else {
+ ckksnotice("ckkskey", self, "Looks like the key hierarchy is unhealthy. Launching fix.");
+ return [[CKKSHealKeyHierarchyOperation alloc] initWithDependencies:self.operationDependencies
+ ckks:self
+ intending:SecCKKSZoneKeyStateBecomeReady
+ errorState:SecCKKSZoneKeyStateError];
}
+ }
- [self scheduleOperation: self.keyStateMachineOperation];
- } else if([self.keyHierarchyState isEqualToString:SecCKKSZoneKeyStateWaitForTLK]) {
- ckksnotice("ckkskey", self, "Entering %@", self.keyHierarchyState);
+ if([currentState isEqualToString:SecCKKSZoneKeyStateHealTLKShares]) {
+ ckksnotice("ckksshare", self, "Key hierarchy is okay, but not shared appropriately. Launching fix.");
+ return [[CKKSHealTLKSharesOperation alloc] initWithOperationDependencies:self.operationDependencies
+ ckks:self];
+ }
- } else if([self.keyHierarchyState isEqualToString:SecCKKSZoneKeyStateUnhealthy]) {
- ckksnotice("ckkskey", self, "Looks like the key hierarchy is unhealthy. Launching fix.");
- self.keyStateMachineOperation = [[CKKSHealKeyHierarchyOperation alloc] initWithCKKSKeychainView:self ckoperationGroup:self.keyHierarchyOperationGroup];
- [self scheduleOperation: self.keyStateMachineOperation];
+ if([currentState isEqualToString:SecCKKSZoneKeyStateProcess]) {
+ [flags _onqueueRemoveFlag:CKKSFlagKeyStateProcessRequested];
- } else {
- // Nothing to do and not in a waiting state? Awesome; we must be in the ready state.
- if(![self.keyHierarchyState isEqual: SecCKKSZoneKeyStateReady]) {
- ckksnotice("ckkskey", self, "No action to take in state %@; we must be ready.", self.keyHierarchyState);
- self.keyHierarchyState = SecCKKSZoneKeyStateReady;
-
- self.keyHierarchyOperationGroup = nil;
- if(self.keyStateReadyDependency) {
- [self scheduleOperation: self.keyStateReadyDependency];
- self.keyStateReadyDependency = nil;
- }
- }
+ ckksnotice("ckksshare", self, "Launching key state process");
+ return [[CKKSProcessReceivedKeysOperation alloc] initWithDependencies:self.operationDependencies
+ intendedState:SecCKKSZoneKeyStateBecomeReady
+ errorState:SecCKKSZoneKeyStateError];
}
-}
-- (bool)_onqueueEnsureKeyHierarchyHealth:(NSError* __autoreleasing *)error {
- dispatch_assert_queue(self.queue);
+ return nil;
+}
- NSError* localerror = nil;
+- (OctagonStateTransitionOperation*)tlkMissingOperation:(CKKSZoneKeyState*)newState
+{
+ WEAKIFY(self);
+ return [OctagonStateTransitionOperation named:@"tlk-missing"
+ intending:newState
+ errorState:SecCKKSZoneKeyStateError
+ withBlockTakingSelf:^(OctagonStateTransitionOperation * _Nonnull op) {
+ STRONGIFY(self);
- // Check if we have an existing key hierarchy
- CKKSKey* tlk = [CKKSKey currentKeyForClass:SecCKKSKeyClassTLK zoneID:self.zoneID error:&localerror];
- CKKSKey* classA = [CKKSKey currentKeyForClass:SecCKKSKeyClassA zoneID:self.zoneID error:&localerror];
- CKKSKey* classC = [CKKSKey currentKeyForClass:SecCKKSKeyClassC zoneID:self.zoneID error:&localerror];
+ NSArray<CKKSPeerProviderState*>* trustStates = self.operationDependencies.currentTrustStates;
- if(localerror || !tlk || !classA || !classC) {
- ckkserror("ckkskey", self, "Error examining existing key hierarchy: %@", localerror);
- ckkserror("ckkskey", self, "Keys are: %@ %@ %@", tlk, classA, classC);
- if(error) {
- *error = localerror;
- }
- return false;
- }
+ [self.operationDependencies.databaseProvider dispatchSyncWithReadOnlySQLTransaction:^{
+ CKKSCurrentKeySet* keyset = [CKKSCurrentKeySet loadForZone:self.zoneID];
- // keychain being locked is not a fatal error here
- [tlk loadKeyMaterialFromKeychain:&localerror];
- if(localerror && !([localerror.domain isEqual: @"securityd"] && localerror.code == errSecInteractionNotAllowed)) {
- ckksinfo("ckkskey", self, "Error loading TLK(%@): %@", tlk, localerror);
- if(error) {
- *error = localerror;
- }
- return false;
- } else if(localerror) {
- ckksinfo("ckkskey", self, "Error loading TLK(%@), maybe locked: %@", tlk, localerror);
- }
- localerror = nil;
+ if(keyset.error) {
+ ckkserror("ckkskey", self, "Unable to load keyset: %@", keyset.error);
+ op.nextState = newState;
- // keychain being locked is not a fatal error here
- [classA loadKeyMaterialFromKeychain:&localerror];
- if(localerror && !([localerror.domain isEqual: @"securityd"] && localerror.code == errSecInteractionNotAllowed)) {
- ckksinfo("ckkskey", self, "Error loading classA key(%@): %@", classA, localerror);
- if(error) {
- *error = localerror;
- }
- return false;
- } else if(localerror) {
- ckksinfo("ckkskey", self, "Error loading classA key(%@), maybe locked: %@", classA, localerror);
- }
- localerror = nil;
+ [self.operationDependencies provideKeySet:keyset];
+ return;
+ }
- // keychain being locked is a fatal error here, since this is class C
- [classA loadKeyMaterialFromKeychain:&localerror];
- if(localerror) {
- ckksinfo("ckkskey", self, "Error loading classC(%@): %@", classC, localerror);
- if(error) {
- *error = localerror;
- }
- return false;
- }
+ if(!keyset.currentTLKPointer.currentKeyUUID) {
+ // In this case, there's no current TLK at all. Go into "wait for tlkcreation";
+ op.nextState = SecCKKSZoneKeyStateWaitForTLKCreation;
+ [self.operationDependencies provideKeySet:keyset];
+ return;
+ }
- self.activeTLK = [tlk uuid];
+ if(self.trustStatus != CKKSAccountStatusAvailable) {
+ ckksnotice("ckkskey", self, "TLK is missing, but no trust is present.");
+ op.nextState = SecCKKSZoneKeyStateLoseTrust;
- // Got to the bottom? Cool! All keys are present and accounted for.
- return true;
-}
+ [self.operationDependencies provideKeySet:keyset];
+ return;
+ }
-- (void)_onqueueKeyHierarchyFetch {
- dispatch_assert_queue(self.queue);
+ bool otherDevicesPresent = [self _onqueueOtherDevicesReportHavingTLKs:keyset
+ trustStates:trustStates];
+ if(otherDevicesPresent) {
+ // We expect this keyset to continue to exist. Send it to our listeners.
+ [self.operationDependencies provideKeySet:keyset];
- __weak __typeof(self) weakSelf = self;
- self.keyStateMachineOperation = [NSBlockOperation blockOperationWithBlock: ^{
- __strong __typeof(weakSelf) strongSelf = weakSelf;
- if(!strongSelf) {
- ckkserror("ckks", strongSelf, "received callback for released object");
+ op.nextState = newState;
+ } else {
+ ckksnotice("ckkskey", self, "No other devices claim to have the TLK. Resetting zone...");
+ op.nextState = SecCKKSZoneKeyStateResettingZone;
+ }
return;
- }
-
- [strongSelf dispatchSync: ^bool{
- [strongSelf _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateFetchComplete withError: nil];
- return true;
}];
}];
- self.keyStateMachineOperation.name = @"waiting-for-fetch";
+}
- NSOperation* fetchOp = [self.zoneChangeFetcher requestSuccessfulFetch: CKKSFetchBecauseKeyHierarchy];
- [self.keyStateMachineOperation addDependency: fetchOp];
+- (bool)_onqueueOtherDevicesReportHavingTLKs:(CKKSCurrentKeySet*)keyset
+ trustStates:(NSArray<CKKSPeerProviderState*>*)trustStates
+{
+ //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];
- self.keyStateFetchRequested = false;
-}
+ NSDateComponents* untrustedOffset = [[NSDateComponents alloc] init];
+ [untrustedOffset setDay:-4];
+ NSDate* untrustedDeadline = [[NSCalendar currentCalendar] dateByAddingComponents:untrustedOffset toDate:now options:0];
-- (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;
+ NSMutableSet<NSString*>* trustedPeerIDs = [NSMutableSet set];
+ for(CKKSPeerProviderState* trustState in trustStates) {
+ for(id<CKKSPeer> peer in trustState.currentTrustedPeers) {
+ [trustedPeerIDs addObject:peer.peerID];
}
+ }
- [strongSelf dispatchSync: ^bool{
- [strongSelf _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateFetchComplete withError: nil];
- return true;
- }];
- }];
- self.keyStateMachineOperation.name = @"waiting-for-refetch";
+ NSError* localerror = nil;
- NSOperation* fetchOp = [self.zoneChangeFetcher requestSuccessfulResyncFetch: CKKSFetchBecauseKeyHierarchy];
- [self.keyStateMachineOperation addDependency: fetchOp];
+ NSArray<CKKSDeviceStateEntry*>* allDeviceStates = [CKKSDeviceStateEntry allInZone:keyset.currentTLKPointer.zoneID error:&localerror];
+ if(localerror) {
+ ckkserror("ckkskey", self, "Error fetching device states: %@", localerror);
+ localerror = nil;
+ return true;
+ }
+ for(CKKSDeviceStateEntry* device in allDeviceStates) {
+ // The peerIDs in CDSEs aren't written with the peer prefix. Make sure we match both.
+ NSString* sosPeerID = device.circlePeerID ? [CKKSSOSPeerPrefix stringByAppendingString:device.circlePeerID] : nil;
+
+ if([trustedPeerIDs containsObject:device.circlePeerID] ||
+ [trustedPeerIDs containsObject:sosPeerID] ||
+ [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);
+ }
+ }
- self.keyStateMachineRefetched = true;
- self.keyStateFullRefetchRequested = false;
- self.keyStateFetchRequested = false;
-}
+ 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<CKKSTLKShareRecord*>* tlkShares = [CKKSTLKShareRecord allForUUID:keyset.currentTLKPointer.currentKeyUUID
+ zoneID:keyset.currentTLKPointer.zoneID
+ error:&localerror];
+ if(localerror) {
+ ckkserror("ckkskey", self, "Error fetching device states: %@", localerror);
+ localerror = nil;
+ return false;
+ }
-- (void)_onqueueKeyHierarchyProcess {
- dispatch_assert_queue(self.queue);
+ for(CKKSTLKShareRecord* 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;
+ }
+ }
- self.keyStateMachineOperation = [[CKKSProcessReceivedKeysOperation alloc] initWithCKKSKeychainView: self];
+ // Okay, how about the untrusted deadline?
+ for(CKKSTLKShareRecord* 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;
+ }
+ }
- // Since we're starting a reprocess, this is answering all previous requests.
- self.keyStateProcessRequested = false;
+ return false;
}
-- (void) handleKeychainEventDbConnection: (SecDbConnectionRef) dbconn
- added: (SecDbItemRef) added
- deleted: (SecDbItemRef) deleted
- rateLimiter: (CKKSRateLimiter*) rateLimiter
- syncCallback: (SecBoolNSErrorCallback) syncCallback {
+- (void)handleKeychainEventDbConnection:(SecDbConnectionRef) dbconn
+ source:(SecDbTransactionSource)txionSource
+ added:(SecDbItemRef) added
+ deleted:(SecDbItemRef) deleted
+ rateLimiter:(CKKSRateLimiter*) rateLimiter
+{
if(!SecCKKSIsEnabled()) {
- ckksinfo("ckks", self, "Skipping handleKeychainEventDbConnection due to disabled CKKS");
+ ckksnotice("ckks", self, "Skipping handleKeychainEventDbConnection due to disabled CKKS");
return;
}
__block NSError* error = nil;
- if(self.accountStatus != CKKSAccountStatusAvailable && syncCallback) {
- // We're not logged into CloudKit, and therefore don't expect this item to be synced anytime particularly soon.
- 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]}]);
- });
-
- syncCallback = 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 isTombstoneModification = addedTombstone && deletedTombstone;
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);
bool proceed = addedSync || deletedSync;
if(!proceed) {
- ckksinfo("ckks", self, "skipping sync of non-sync item");
+ ckksnotice("ckks", self, "skipping sync of non-sync item (%d, %d)", addedSync, deletedSync);
+ return;
+ }
+
+ if(isTombstoneModification) {
+ ckksnotice("ckks", self, "skipping syncing update of tombstone item (%d, %d)", addedTombstone, deletedTombstone);
+ return;
+ }
+
+ // It's possible to ask for an item to be deleted without adding a corresponding tombstone.
+ // This is arguably a bug, as it generates an out-of-sync state, but it is in the API contract.
+ // CKKS should ignore these, but log very upset messages.
+ if(isDelete && !addedTombstone) {
+ ckksnotice("ckks", self, "Client has asked for an item deletion to not sync. Keychain is now out of sync with account");
return;
}
NSString* protection = (__bridge NSString*)SecDbItemGetCachedValueWithName(added ? added : deleted, kSecAttrAccessible);
if(! ([protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleWhenUnlocked] ||
[protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAfterFirstUnlock] ||
- [protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAlways])) {
- ckksinfo("ckks", self, "skipping sync of device-bound item");
+ [protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAlwaysPrivate])) {
+ ckksnotice("ckks", self, "skipping sync of device-bound(%@) item", protection);
return;
}
+ if(txionSource == kSecDbSOSTransaction) {
+ NSString* addedUUID = (__bridge NSString*)SecDbItemGetValue(added, &v10itemuuid, NULL);
+ ckksnotice("ckks", self, "Received an incoming %@ from SOS (%@)",
+ isAdd ? @"addition" : (isModify ? @"modification" : @"deletion"),
+ addedUUID);
+ }
+
// 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 {
- if(![self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateReady]) {
- ckksnotice("ckks", self, "Key state not ready for new items; skipping");
- return true;
+ [self dispatchSyncWithConnection:dbconn
+ readWriteTxion:YES
+ block:^CKKSDatabaseTransactionResult {
+ // Schedule a "view changed" notification
+ [self.notifyViewChangedScheduler trigger];
+
+ if(self.accountStatus == CKKSAccountStatusNoAccount) {
+ // No account; CKKS shouldn't attempt anything.
+ [self.stateMachine _onqueueHandleFlag:CKKSFlagScanLocalItems];
+ ckksnotice("ckks", self, "Dropping sync item modification due to CK account state; will scan to find changes later");
+
+ // We're positively not logged into CloudKit, and therefore don't expect this item to be synced anytime particularly soon.
+ NSString* uuid = (__bridge NSString*)SecDbItemGetValue(added ? added : deleted, &v10itemuuid, NULL);
+
+ SecBoolNSErrorCallback syncCallback = [[CKKSViewManager manager] claimCallbackForUUID:uuid];
+ if(syncCallback) {
+ [CKKSViewManager callSyncCallbackWithErrorNoAccount: syncCallback];
+ }
+
+ return CKKSDatabaseTransactionCommit;
}
CKKSOutgoingQueueEntry* oqe = nil;
if (isAdd) {
- oqe = [CKKSOutgoingQueueEntry withItem: added action: SecCKKSActionAdd ckks:self error: &error];
+ oqe = [CKKSOutgoingQueueEntry withItem: added action: SecCKKSActionAdd zoneID:self.zoneID error: &error];
} else if(isDelete) {
- oqe = [CKKSOutgoingQueueEntry withItem: deleted action: SecCKKSActionDelete ckks:self error: &error];
+ oqe = [CKKSOutgoingQueueEntry withItem: deleted action: SecCKKSActionDelete zoneID:self.zoneID error: &error];
} else if(isModify) {
- oqe = [CKKSOutgoingQueueEntry withItem: added action: SecCKKSActionModify ckks:self error: &error];
+ oqe = [CKKSOutgoingQueueEntry withItem: added action: SecCKKSActionModify zoneID:self.zoneID error: &error];
} else {
ckkserror("ckks", self, "processKeychainEventItemAdded given garbage: %@ %@", added, deleted);
- return true;
+ return CKKSDatabaseTransactionCommit;
+ }
+
+ if(!self.itemSyncingEnabled) {
+ // Call any callback now; they're not likely to get the sync they wanted
+ SecBoolNSErrorCallback syncCallback = [[CKKSViewManager manager] claimCallbackForUUID:oqe.uuid];
+ if(syncCallback) {
+ syncCallback(false, [NSError errorWithDomain:CKKSErrorDomain
+ code:CKKSErrorViewIsPaused
+ description:@"View is paused; item is not expected to sync"]);
+ }
}
- CKOperationGroup* operationGroup = [CKOperationGroup CKKSGroupWithName:@"keychain-api-use"];
+ CKOperationGroup* operationGroup = txionSource == kSecDbSOSTransaction
+ ? [CKOperationGroup CKKSGroupWithName:@"sos-incoming-item"]
+ : [CKOperationGroup CKKSGroupWithName:@"keychain-api-use"];
if(error) {
ckkserror("ckks", self, "Couldn't create outgoing queue entry: %@", error);
-
- // 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:@"securityd"] && error.code == CKKSNoUUIDOnItem) {
- ckksnotice("ckks", self, "Launching scan operation");
- CKKSScanLocalItemsOperation* scanOperation = [[CKKSScanLocalItemsOperation alloc] initWithCKKSKeychainView: self ckoperationGroup:operationGroup];
- [self scheduleOperation: scanOperation];
- }
+ [self.stateMachine _onqueueHandleFlag:CKKSFlagScanLocalItems];
// If the problem is 'couldn't load key', tell the key hierarchy state machine to fix it
- // Then, launch a scan operation to find this item and upload it
- if([error.domain isEqualToString:@"securityd"] && error.code == errSecItemNotFound) {
- [self _onqueueAdvanceKeyStateMachineToState: nil withError: nil];
-
- ckksnotice("ckks", self, "Launching scan operation to refind %@", added);
- CKKSScanLocalItemsOperation* scanOperation = [[CKKSScanLocalItemsOperation alloc] initWithCKKSKeychainView: self ckoperationGroup:operationGroup];
- [scanOperation addNullableDependency:self.keyStateReadyDependency];
- [self scheduleOperation: scanOperation];
+ if([error.domain isEqualToString:CKKSErrorDomain] && error.code == errSecItemNotFound) {
+ [self.stateMachine _onqueueHandleFlag:CKKSFlagKeyStateProcessRequested];
}
- return true;
+ return CKKSDatabaseTransactionCommit;
+ } else if(!oqe) {
+ ckkserror("ckks", self, "Decided that no operation needs to occur for %@", error);
+ return CKKSDatabaseTransactionCommit;
}
if(rateLimiter) {
[oqe saveToDatabaseWithConnection: dbconn error: &error];
if(error) {
ckkserror("ckks", self, "Couldn't save outgoing queue entry to database: %@", error);
- return true;
+ return CKKSDatabaseTransactionCommit;
+ } 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];
+ NSArray<CKKSOutgoingQueueEntry*>* siblings = [CKKSOutgoingQueueEntry allWithUUID:oqe.uuid
+ states:@[SecCKKSStateReencrypt, SecCKKSStateError]
+ 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;
+ ckkserror("ckks", self, "Couldn't load OQE siblings for %@: %@", oqe, error);
}
- 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);
+ for(CKKSOutgoingQueueEntry* oqeSibling in siblings) {
+ NSError* deletionError = nil;
+ [oqeSibling deleteFromDatabase:&deletionError];
+ if(deletionError) {
+ ckkserror("ckks", self, "Couldn't delete OQE sibling(%@) for %@: %@", oqeSibling, oqe.uuid, deletionError);
}
}
- if(syncCallback) {
- self.pendingSyncCallbacks[oqe.uuid] = syncCallback;
+ // This update also supercedes any remote changes that are pending.
+ NSError* iqeError = nil;
+ CKKSIncomingQueueEntry* iqe = [CKKSIncomingQueueEntry tryFromDatabase:oqe.uuid zoneID:self.zoneID error:&iqeError];
+ if(iqeError) {
+ ckkserror("ckks", self, "Couldn't find IQE matching %@: %@", oqe.uuid, error);
+ } else if(iqe) {
+ [iqe deleteFromDatabase:&iqeError];
+ if(iqeError) {
+ ckkserror("ckks", self, "Couldn't delete IQE matching %@: %@", oqe.uuid, error);
+ } else {
+ ckksnotice("ckks", self, "Deleted IQE matching changed item %@", oqe.uuid);
+ }
}
- // Schedule a "view changed" notification
- [self.notifyViewChangedScheduler trigger];
-
[self processOutgoingQueue:operationGroup];
- return true;
+ return CKKSDatabaseTransactionCommit;
}];
}
--(void)setCurrentItemForAccessGroup:(SecDbItemRef)newItem
+-(void)setCurrentItemForAccessGroup:(NSData* _Nonnull)newItemPersistentRef
hash:(NSData*)newItemSHA1
accessGroup:(NSString*)accessGroup
identifier:(NSString*)identifier
- replacing:(SecDbItemRef)oldItem
+ replacing:(NSData* _Nullable)oldCurrentItemPersistentRef
hash:(NSData*)oldItemSHA1
complete:(void (^) (NSError* operror)) complete
{
if(accessGroup == nil || identifier == nil) {
- NSError* error = [NSError errorWithDomain:@"securityd" code:errSecParam userInfo:@{NSLocalizedDescriptionKey: @"No access group or identifier given"}];
+ NSError* error = [NSError errorWithDomain:CKKSErrorDomain
+ code:errSecParam
+ description:@"No access group or identifier given"];
ckkserror("ckkscurrent", self, "Cancelling request: %@", error);
complete(error);
return;
}
- __weak __typeof(self) weakSelf = self;
-
- [self dispatchSync:^bool {
- NSError* error = nil;
- CFErrorRef cferror = NULL;
-
- NSString* newItemUUID = nil;
- NSString* oldItemUUID = nil;
-
- // Now that we're on the db queue, ensure that the given hashes for the items match the hashes as they are now.
- // That is, the items haven't changed since the caller knew about the item.
- NSData* newItemComputedSHA1 = (NSData*) CFBridgingRelease(CFRetainSafe(SecDbItemGetSHA1(newItem, &cferror)));
- if(!newItemComputedSHA1 || cferror ||
- ![newItemComputedSHA1 isEqual:newItemSHA1]) {
- ckksnotice("ckkscurrent", self, "Hash mismatch for new item: %@ vs %@", newItemComputedSHA1, newItemSHA1);
- error = [NSError errorWithDomain:@"securityd" code:errSecItemInvalidValue userInfo:@{NSLocalizedDescriptionKey: @"New item has changed; hashes mismatch. Refetch and try again."}];
- complete(error);
- CFReleaseNull(cferror);
- return false;
- }
+ // 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)];
- newItemUUID = (NSString*) CFBridgingRelease(CFRetainSafe(SecDbItemGetValue(newItem, &v10itemuuid, &cferror)));
- if(!newItemUUID || cferror) {
- ckkserror("ckkscurrent", self, "Error fetching UUID for new item: %@", cferror);
- complete((__bridge NSError*) cferror);
- CFReleaseNull(cferror);
- return false;
- }
+ 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;
+ }
- if(oldItem) {
- NSData* oldItemComputedSHA1 = (NSData*) CFBridgingRelease(CFRetainSafe(SecDbItemGetSHA1(oldItem, &cferror)));
- if(!oldItemComputedSHA1 || cferror ||
- ![oldItemComputedSHA1 isEqual:oldItemSHA1]) {
- ckksnotice("ckkscurrent", self, "Hash mismatch for old item: %@ vs %@", oldItemComputedSHA1, oldItemSHA1);
- error = [NSError errorWithDomain:@"securityd" code:errSecItemInvalidValue userInfo:@{NSLocalizedDescriptionKey: @"Old item has changed; hashes mismatch. Refetch and try again."}];
- complete(error);
- CFReleaseNull(cferror);
- return false;
- }
+ 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"]];
- oldItemUUID = (NSString*) CFBridgingRelease(CFRetainSafe(SecDbItemGetValue(oldItem, &v10itemuuid, &cferror)));
- if(!oldItemUUID || cferror) {
- ckkserror("ckkscurrent", self, "Error fetching UUID for old item: %@", cferror);
- complete((__bridge NSError*) cferror);
- CFReleaseNull(cferror);
- return false;
- }
- }
+ WEAKIFY(self);
+ CKKSResultOperation* returnCallback = [CKKSResultOperation operationWithBlock:^{
+ STRONGIFY(self);
- // Not being in a CloudKit account is an automatic failure.
- if(self.accountStatus != CKKSAccountStatusAvailable) {
- ckksnotice("ckkscurrent", self, "Rejecting current item pointer set since we don't have an iCloud account.");
- error = [NSError errorWithDomain:@"securityd" code:errSecNotLoggedIn userInfo:@{NSLocalizedDescriptionKey: @"User is not signed into iCloud."}];
- complete(error);
- return false;
+ if(ucipo.error) {
+ ckkserror("ckkscurrent", self, "Failed setting a current item pointer for %@ with %@", ucipo.currentPointerIdentifier, ucipo.error);
+ } else {
+ ckksnotice("ckkscurrent", self, "Finished setting a current item pointer for %@", ucipo.currentPointerIdentifier);
}
+ complete(ucipo.error);
+ }];
+ returnCallback.name = @"setCurrentItem-return-callback";
+ [returnCallback addDependency: ucipo];
+ [self scheduleOperation: returnCallback];
- // At this point, we've completed all the checks we need for the SecDbItems. Try to launch this boat!
- NSString* currentIdentifier = [NSString stringWithFormat:@"%@-%@", accessGroup, identifier];
- ckksnotice("ckkscurrent", self, "Setting current pointer for %@ to %@ (from %@)", currentIdentifier, newItemUUID, oldItemUUID);
- CKKSUpdateCurrentItemPointerOperation* ucipo = [[CKKSUpdateCurrentItemPointerOperation alloc] initWithCKKSKeychainView:self
- currentPointer:(NSString*)currentIdentifier
- oldItemUUID:(NSString*)oldItemUUID
- newItemUUID:(NSString*)newItemUUID
- ckoperationGroup:[CKOperationGroup CKKSGroupWithName:@"currentitem-api"]];
- CKKSResultOperation* returnCallback = [CKKSResultOperation operationWithBlock:^{
- __strong __typeof(self) strongSelf = weakSelf;
-
- if(ucipo.error) {
- ckkserror("ckkscurrent", strongSelf, "Failed setting a current item pointer with %@", ucipo.error);
- } else {
- ckksnotice("ckkscurrent", strongSelf, "Finished setting a current item pointer");
- }
- 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];
+ // 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];
+ // 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 true;
- }];
+ [self scheduleOperation:ucipo];
return;
}
complete:(void (^) (NSString* uuid, NSError* operror)) complete
{
if(accessGroup == nil || identifier == nil) {
- complete(NULL, [NSError errorWithDomain:@"securityd" code:errSecParam userInfo:@{NSLocalizedDescriptionKey: @"No access group or identifier given"}]);
+ 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:@"securityd" code:errSecNotLoggedIn userInfo:@{NSLocalizedDescriptionKey: @"User is not signed into iCloud."}]);
+ complete(NULL, [NSError errorWithDomain:CKKSErrorDomain
+ code:CKKSNotLoggedIn
+ description:@"User is not signed into iCloud."]);
return;
}
fetchAndProcess = [self fetchAndProcessCKChanges:CKKSFetchBecauseCurrentItemFetchRequest];
}
- __weak __typeof(self) weakSelf = self;
- CKKSResultOperation* getCurrentItem = [CKKSResultOperation operationWithBlock:^{
+ WEAKIFY(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;
+ STRONGIFY(self);
- [strongSelf dispatchSync: ^bool {
+ [self dispatchSyncWithReadOnlySQLTransaction:^{
NSError* error = nil;
NSString* currentIdentifier = [NSString stringWithFormat:@"%@-%@", accessGroup, identifier];
CKKSCurrentItemPointer* cip = [CKKSCurrentItemPointer fromDatabase:currentIdentifier
state:SecCKKSProcessedStateLocal
- zoneID:strongSelf.zoneID
+ zoneID:self.zoneID
error:&error];
if(!cip || error) {
- ckkserror("ckkscurrent", strongSelf, "No current item pointer for %@", currentIdentifier);
+ if([error.domain isEqualToString:@"securityd"] && error.code == errSecItemNotFound) {
+ // This error is common and very, very noisy. Shorten it and don't log here (the framework should log for us)
+ ckksinfo("ckkscurrent", self, "No current item pointer for %@", currentIdentifier);
+ error = [NSError errorWithDomain:@"securityd" code:errSecItemNotFound description:[NSString stringWithFormat:@"No current item pointer found for %@", currentIdentifier]];
+ } else {
+ ckkserror("ckkscurrent", self, "No current item pointer for %@", currentIdentifier);
+ }
complete(nil, error);
- return false;
+ return;
}
if(!cip.currentItemUUID) {
- ckkserror("ckkscurrent", strongSelf, "Current item pointer is empty %@", cip);
- complete(nil, [NSError errorWithDomain:@"securityd"
+ ckkserror("ckkscurrent", self, "Current item pointer is empty %@", cip);
+ complete(nil, [NSError errorWithDomain:CKKSErrorDomain
code:errSecInternalError
- userInfo:@{NSLocalizedDescriptionKey: @"Current item pointer is empty"}]);
- return false;
+ description:@"Current item pointer is empty"]);
+ return;
}
+ ckksinfo("ckkscurrent", self, "Retrieved current item pointer: %@", cip);
complete(cip.currentItemUUID, NULL);
-
- return true;
+ return;
}];
}];
- getCurrentItem.name = @"get-current-item-pointer";
[getCurrentItem addNullableDependency:fetchAndProcess];
[self scheduleOperation: getCurrentItem];
}
-- (CKKSKey*) keyForItem: (SecDbItemRef) item error: (NSError * __autoreleasing *) error {
- CKKSKeyClass* class = nil;
+- (CKKSResultOperation<CKKSKeySetProviderOperationProtocol>*)findKeySet:(BOOL)refetchBeforeReturningKeySet
+{
+ __block CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp = nil;
+ __block BOOL moveFromWaitForTrust = NO;
- NSString* protection = (__bridge NSString*)SecDbItemGetCachedValueWithName(item, kSecAttrAccessible);
- if([protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleWhenUnlocked]) {
- class = SecCKKSKeyClassA;
- } else if([protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAlways] ||
- [protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAfterFirstUnlock]) {
- class = SecCKKSKeyClassC;
- } else {
- ckkserror("ckks", self, "can't pick key class for protection %@: %@", protection, item);
- if(error) {
- *error =[NSError errorWithDomain:@"securityd"
- code:5
- userInfo:@{NSLocalizedDescriptionKey:
- [NSString stringWithFormat:@"can't pick key class for protection %@: %@", protection, item]}];
+ [self dispatchSyncWithReadOnlySQLTransaction:^{
+ keysetOp = (CKKSProvideKeySetOperation*)[self findFirstPendingOperation:self.operationDependencies.keysetProviderOperations];
+ if(!keysetOp) {
+ keysetOp = [[CKKSProvideKeySetOperation alloc] initWithZoneName:self.zoneName];
+ [self.operationDependencies.keysetProviderOperations addObject:keysetOp];
+
+ // This is an abuse of operations: they should generally run when added to a queue, not wait, but this allows recipients to set timeouts
+ [self scheduleOperationWithoutDependencies:keysetOp];
}
- return nil;
+ if(refetchBeforeReturningKeySet) {
+ ckksnotice("ckks", self, "Refetch requested before returning key set!");
+
+ [self.stateMachine _onqueueHandleFlag:CKKSFlagFetchRequested];
+ [self.stateMachine _onqueueHandleFlag:CKKSFlagTLKCreationRequested];
+
+ if([self.stateMachine.currentState isEqualToString:SecCKKSZoneKeyStateWaitForTrust]) {
+ moveFromWaitForTrust = YES;
+ }
+ return;
+ }
+
+ CKKSCurrentKeySet* keyset = [CKKSCurrentKeySet loadForZone:self.zoneID];
+ if(keyset.currentTLKPointer.currentKeyUUID &&
+ (keyset.tlk.uuid ||
+ [self.stateMachine.currentState isEqualToString:SecCKKSZoneKeyStateWaitForTrust] ||
+ [self.stateMachine.currentState isEqualToString:SecCKKSZoneKeyStateWaitForTLK])) {
+ ckksnotice("ckks", self, "Already have keyset %@", keyset);
+
+ [keysetOp provideKeySet:keyset];
+ return;
+
+ } else if([self.stateMachine.currentState isEqualToString:SecCKKSZoneKeyStateWaitForTrust]) {
+ // No keyset exists, but we're in waitfortrust? Seems like a bug. Move us out of this state...
+
+ ckksnotice("ckks", self, "Received a keyset request in an odd state; forwarding to state machine");
+ [self.stateMachine _onqueueHandleFlag:CKKSFlagTLKCreationRequested];
+ moveFromWaitForTrust = YES;
+
+ } else {
+ // The key state machine will know what to do.
+ [self.stateMachine _onqueueHandleFlag:CKKSFlagTLKCreationRequested];
+ };
+ }];
+
+ if(moveFromWaitForTrust) {
+ [self.stateMachine handleExternalRequest:[[OctagonStateTransitionRequest alloc] init:@"fix-bug"
+ sourceStates:[NSSet setWithObject:SecCKKSZoneKeyStateWaitForTrust]
+ serialQueue:self.queue
+ timeout:5 * NSEC_PER_SEC
+ transitionOp:[OctagonStateTransitionOperation named:@"fix-bug"
+ entering:SecCKKSZoneKeyStateWaitForTLKCreation]]];
}
- CKKSKey* key = [CKKSKey currentKeyForClass: class zoneID:self.zoneID error:error];
+ return keysetOp;
+}
- // and make sure it's unwrapped.
- if(![key ensureKeyLoaded:error]) {
- return nil;
+- (void)receiveTLKUploadRecords:(NSArray<CKRecord*>*)records
+{
+ // First, filter for records matching this zone
+ NSMutableArray<CKRecord*>* zoneRecords = [NSMutableArray array];
+ for(CKRecord* record in records) {
+ if([record.recordID.zoneID isEqual:self.zoneID]) {
+ [zoneRecords addObject:record];
+ }
+ }
+
+ ckksnotice("ckkskey", self, "Received a set of %lu TLK upload records", (unsigned long)zoneRecords.count);
+
+ if(!zoneRecords || zoneRecords.count == 0) {
+ return;
}
- return key;
+ [self dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
+ for(CKRecord* record in zoneRecords) {
+ [self _onqueueCKRecordChanged:record resync:false];
+ }
+
+ [self.stateMachine _onqueueHandleFlag:CKKSFlagKeyStateTLKsUploaded];
+
+ return CKKSDatabaseTransactionCommit;
+ }];
+}
+
+- (BOOL)requiresTLKUpload
+{
+ __block BOOL requiresUpload = NO;
+ dispatch_sync(self.queue, ^{
+ // We want to return true only if we're in a state that immediately requires an upload.
+ if(([self.keyHierarchyState isEqualToString:SecCKKSZoneKeyStateWaitForTLKUpload] ||
+ [self.keyHierarchyState isEqualToString:SecCKKSZoneKeyStateWaitForTLKCreation])) {
+ requiresUpload = YES;
+ }
+ });
+
+ return requiresUpload;
}
// Use the following method to find the first pending operation in a weak collection
}
}
-// 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 {
+- (CKKSOutgoingQueueOperation*)processOutgoingQueue:(CKOperationGroup* _Nullable)ckoperationGroup {
return [self processOutgoingQueueAfter:nil ckoperationGroup:ckoperationGroup];
}
-- (CKKSOutgoingQueueOperation*)processOutgoingQueueAfter:(CKKSResultOperation*)after ckoperationGroup:(CKOperationGroup*)ckoperationGroup {
- if(!SecCKKSIsEnabled()) {
- ckksinfo("ckks", self, "Skipping processOutgoingQueue due to disabled CKKS");
- return nil;
- }
+- (CKKSOutgoingQueueOperation*)processOutgoingQueueAfter:(CKKSResultOperation* _Nullable)after
+ ckoperationGroup:(CKOperationGroup* _Nullable)ckoperationGroup {
+ return [self processOutgoingQueueAfter:after requiredDelay:DISPATCH_TIME_FOREVER ckoperationGroup:ckoperationGroup];
+}
+- (CKKSOutgoingQueueOperation*)processOutgoingQueueAfter:(CKKSResultOperation* _Nullable)after
+ requiredDelay:(uint64_t)requiredDelay
+ ckoperationGroup:(CKOperationGroup* _Nullable)ckoperationGroup
+{
CKKSOutgoingQueueOperation* outgoingop =
(CKKSOutgoingQueueOperation*) [self findFirstPendingOperation:self.outgoingQueueOperations
ofClass:[CKKSOutgoingQueueOperation class]];
if(outgoingop) {
- ckksinfo("ckks", self, "Skipping processOutgoingQueue due to at least one pending instance");
if(after) {
[outgoingop addDependency: after];
}
if(!outgoingop.ckoperationGroup && ckoperationGroup) {
outgoingop.ckoperationGroup = ckoperationGroup;
} else if(ckoperationGroup) {
- ckkserror("ckks", self, "Throwing away CKOperationGroup(%@) in favor of %@", ckoperationGroup, outgoingop.ckoperationGroup);
+ ckkserror("ckks", self, "Throwing away CKOperationGroup(%@) in favor of (%@)", ckoperationGroup.name, outgoingop.ckoperationGroup.name);
}
+ // Will log any pending dependencies as well
+ ckksinfo("ckksoutgoing", self, "Returning existing %@", outgoingop);
+
+ // Shouldn't be necessary, but can't hurt
+ [self.outgoingQueueOperationScheduler triggerAt:requiredDelay];
return outgoingop;
}
}
- CKKSOutgoingQueueOperation* op = [[CKKSOutgoingQueueOperation alloc] initWithCKKSKeychainView:self ckoperationGroup:ckoperationGroup];
+ CKKSOutgoingQueueOperation* op = [[CKKSOutgoingQueueOperation alloc] initWithDependencies:self.operationDependencies
+ ckks:self
+ intending:SecCKKSZoneKeyStateReady
+ errorState:SecCKKSZoneKeyStateUnhealthy
+ ckoperationGroup:ckoperationGroup];
op.name = @"outgoing-queue-operation";
[op addNullableDependency:after];
+ [op addNullableDependency:self.outgoingQueueOperationScheduler.operationDependency];
- [op addNullableDependency: self.initialScanOperation];
+ [self.outgoingQueueOperationScheduler triggerAt:requiredDelay];
+
+ [op linearDependencies:self.outgoingQueueOperations];
[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;
+ WEAKIFY(self);
CKKSResultOperation* restartIncomingQueueOperation = [CKKSResultOperation operationWithBlock:^{
- __strong __typeof(self) strongSelf = weakSelf;
+ STRONGIFY(self);
// This IQO shouldn't error if the keybag has locked again. It will simply try again later.
- [strongSelf processIncomingQueue:false];
+ [self processIncomingQueue:false];
}];
restartIncomingQueueOperation.name = @"reprocess-incoming-queue-after-unlock";
}
}
+- (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 {
- if(!SecCKKSIsEnabled()) {
- ckksinfo("ckks", self, "Skipping processIncomingQueue due to disabled CKKS");
- return nil;
- }
+ return [self processIncomingQueue:failOnClassA after:after policyConsideredAuthoritative:false];
+}
+- (CKKSIncomingQueueOperation*)processIncomingQueue:(bool)failOnClassA
+ after:(CKKSResultOperation*)after
+ policyConsideredAuthoritative:(bool)policyConsideredAuthoritative
+{
CKKSIncomingQueueOperation* incomingop = (CKKSIncomingQueueOperation*) [self findFirstPendingOperation:self.incomingQueueOperations];
if(incomingop) {
ckksinfo("ckks", self, "Skipping processIncomingQueue due to at least one pending instance");
if(after) {
- [incomingop addDependency: 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;
+ incomingop.handleMismatchedViewItems |= policyConsideredAuthoritative;
return incomingop;
}
}
- CKKSIncomingQueueOperation* op = [[CKKSIncomingQueueOperation alloc] initWithCKKSKeychainView:self errorOnClassAFailure:failOnClassA];
+ CKKSIncomingQueueOperation* op = [[CKKSIncomingQueueOperation alloc] initWithDependencies:self.operationDependencies
+ ckks:self
+ intending:SecCKKSZoneKeyStateReady
+ errorState:SecCKKSZoneKeyStateUnhealthy
+ errorOnClassAFailure:failOnClassA
+ handleMismatchedViewItems:policyConsideredAuthoritative];
op.name = @"incoming-queue-operation";
if(after != nil) {
[op addSuccessDependency: after];
}
+ if(self.resultsOfNextIncomingQueueOperationOperation) {
+ [self.resultsOfNextIncomingQueueOperationOperation addSuccessDependency:op];
+ [self scheduleOperation:self.resultsOfNextIncomingQueueOperationOperation];
+ self.resultsOfNextIncomingQueueOperationOperation = nil;
+ }
+
[self scheduleOperation: op];
return op;
}
-- (CKKSUpdateDeviceStateOperation*)updateDeviceState:(bool)rateLimit ckoperationGroup:(CKOperationGroup*)ckoperationGroup {
- if(!SecCKKSIsEnabled()) {
- ckksinfo("ckks", self, "Skipping updateDeviceState due to disabled CKKS");
- return nil;
+- (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*)[self findFirstPendingOperation:self.scanLocalItemsOperations];
+
+ if(scanOperation) {
+ [scanOperation addNullableDependency:after];
+
+ // check (again) for race condition; if the op has started we need to add another (for the dependency)
+ if([scanOperation isPending]) {
+ scanOperation.ckoperationGroup = operationGroup;
+
+ scanOperation.name = [NSString stringWithFormat:@"%@::%@", scanOperation.name, operationName];
+ return scanOperation;
+ }
}
+ scanOperation = [[CKKSScanLocalItemsOperation alloc] initWithDependencies:self.operationDependencies
+ ckks:self
+ intending:SecCKKSZoneKeyStateReady
+ errorState:SecCKKSZoneKeyStateError
+ ckoperationGroup:operationGroup];
+ scanOperation.name = operationName;
+
+ [scanOperation addNullableDependency:self.lastFixupOperation];
+ [scanOperation addNullableDependency:self.lockStateTracker.unlockDependency];
+ [scanOperation addNullableDependency:self.keyStateReadyDependency];
+ [scanOperation addNullableDependency:after];
+
+ [scanOperation linearDependencies:self.scanLocalItemsOperations];
+
+ // This might generate items for upload. Make sure that any uploads wait until the scan is complete, so we know what to upload
+ [scanOperation linearDependencies:self.outgoingQueueOperations];
+
+ [self scheduleOperation:scanOperation];
+ self.initiatedLocalScan = YES;
+ return scanOperation;
+}
+
+- (CKKSUpdateDeviceStateOperation*)updateDeviceState:(bool)rateLimit
+ waitForKeyHierarchyInitialization:(uint64_t)timeout
+ ckoperationGroup:(CKOperationGroup*)ckoperationGroup {
+ // If securityd just started, the key state might be in some transient early state. Wait a bit.
+ OctagonStateMultiStateArrivalWatcher* waitForTransient = [[OctagonStateMultiStateArrivalWatcher alloc] initNamed:@"rpc-watcher"
+ serialQueue:self.queue
+ states:CKKSKeyStateNonTransientStates()];
+ [waitForTransient timeout:timeout];
+ [self.stateMachine registerMultiStateArrivalWatcher:waitForTransient];
+
CKKSUpdateDeviceStateOperation* op = [[CKKSUpdateDeviceStateOperation alloc] initWithCKKSKeychainView:self rateLimit:rateLimit ckoperationGroup:ckoperationGroup];
op.name = @"device-state-operation";
+ [op addDependency:waitForTransient.result];
+
// 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.
- [self scheduleAccountStatusOperation:op];
+ // They also shouldn't block or be blocked by any other operation; our wait operation above will handle that
+ [self scheduleOperationWithoutDependencies:op];
return op;
}
+- (void)xpc24HrNotification
+{
+ // Called roughly once every 24hrs
+ [self.stateMachine handleFlag:CKKSFlag24hrNotification];
+}
+
// 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 {
+ dispatch_assert_queue(self.queue);
NSError* localerror = nil;
- CKKSCKAccountStateTracker* accountTracker = self.accountTracker;
+ CKKSAccountStateTracker* accountTracker = self.accountTracker;
+ CKKSAccountStatus hsa2Status = accountTracker.hsa2iCloudAccountStatus;
- // 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);
+ // We must have an HSA2 iCloud account and a CloudKit account to even create one of these
+ if(hsa2Status != CKKSAccountStatusAvailable ||
+ accountTracker.currentCKAccountInfo.accountStatus != CKAccountStatusAvailable) {
+ ckkserror("ckksdevice", self, "No iCloud account active: %@ hsa2 account:%@",
+ accountTracker.currentCKAccountInfo,
+ CKKSAccountStatusToString(hsa2Status));
localerror = [NSError errorWithDomain:@"securityd"
code:errSecInternalError
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"No active HSA2 iCloud account: %@", accountTracker.currentCKAccountInfo]}];
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);
+ NSString* ckdeviceID = accountTracker.ckdeviceID;
+ if(ckdeviceID == nil) {
+ ckkserror("ckksdevice", self, "No CK device ID available; cannot make device state entry");
+ localerror = [NSError errorWithDomain:CKKSErrorDomain
+ code:CKKSNotLoggedIn
+ userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"No CK device ID: %@", accountTracker.currentCKAccountInfo]}];
if(error) {
*error = localerror;
}
return nil;
}
- // Find out what we think the current keys are
+ CKKSDeviceStateEntry* oldcdse = [CKKSDeviceStateEntry tryFromDatabase: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];
}
// 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");
+ // But, if the platform doesn't have SOS, don't bother
+ if(OctagonPlatformSupportsSOS() && [accountTracker.accountCirclePeerIDInitialized wait:500*NSEC_PER_MSEC] != 0 && !accountTracker.accountCirclePeerID) {
+ ckkserror("ckksdevice", self, "No SOS peer ID available");
+ }
+
+ // We'd also like the Octagon status
+ if([accountTracker.octagonInformationInitialized wait:500*NSEC_PER_MSEC] != 0 && !accountTracker.octagonPeerID) {
+ ckkserror("ckksdevice", self, "No octagon 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
+ CKKSDeviceStateEntry* newcdse = [[CKKSDeviceStateEntry alloc] initForDevice:ckdeviceID
+ osVersion:SecCKKSHostOSVersion()
+ lastUnlockTime:lastUnlockDay
+ octagonPeerID:accountTracker.octagonPeerID
+ octagonStatus:accountTracker.octagonStatus
circlePeerID:accountTracker.accountCirclePeerID
- circleStatus:accountTracker.currentCircleStatus
+ circleStatus:accountTracker.currentCircleStatus.status
keyState:self.keyHierarchyState
currentTLKUUID:suggestedTLK.uuid
currentClassAUUID:suggestedClassAKey.uuid
}
- (CKKSSynchronizeOperation*) resyncWithCloud {
- if(!SecCKKSIsEnabled()) {
- ckksinfo("ckks", self, "Skipping resyncWithCloud due to disabled CKKS");
- return nil;
- }
-
CKKSSynchronizeOperation* op = [[CKKSSynchronizeOperation alloc] initWithCKKSKeychainView: self];
[self scheduleOperation: op];
return op;
}
-- (CKKSResultOperation*)fetchAndProcessCKChanges:(CKKSFetchBecause*)because {
- if(!SecCKKSIsEnabled()) {
+- (CKKSLocalSynchronizeOperation*)resyncLocal {
+ CKKSLocalSynchronizeOperation* op = [[CKKSLocalSynchronizeOperation alloc] initWithCKKSKeychainView:self];
+ [self scheduleOperation: op];
+ return op;
+}
+
+- (CKKSResultOperation*)fetchAndProcessCKChanges:(CKKSFetchBecause*)because
+{
+ if(!SecCKKSIsEnabled()) {
ckksinfo("ckks", self, "Skipping fetchAndProcessCKChanges due to disabled CKKS");
return nil;
}
- // We fetched some changes; try to process them!
+ // We fetched some changes; try to process them!
return [self processIncomingQueue:false after:[self.zoneChangeFetcher requestSuccessfulFetch:because]];
}
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.stateMachine _onqueueHandleFlag:CKKSFlagFetchRequested];
+
+ } else if(thirdLevelError.code == CKKSServerMissingRecord) {
+ // The server is concerned that there's a missing record somewhere.
+ // Issue a key hierarchy fetch and see what's happening
+ ckkserror("ckks", self, "CKKS Server extension has told us about %@ for record %@; requesting refetch and reprocess of key hierarchy", thirdLevelError, recordID);
+ [self.stateMachine _onqueueHandleFlag:CKKSFlagFetchRequested];
+
+ } 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;
// 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);
+ ckksnotice("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];
if(iqeerror) {
ckkserror("ckks", self, "Couldn't save incoming queue entry: %@", iqeerror);
}
+
+ // Delete any pending local changes; this delete wins
+ NSArray<CKKSOutgoingQueueEntry*>* siblings = [CKKSOutgoingQueueEntry allWithUUID:iqe.uuid
+ states:@[SecCKKSStateNew,
+ SecCKKSStateReencrypt,
+ SecCKKSStateError]
+ zoneID:self.zoneID
+ error:&error];
+ if(error) {
+ ckkserror("ckks", self, "Couldn't load OQE sibling for %@: %@", iqe.uuid, error);
+ }
+
+ for(CKKSOutgoingQueueEntry* oqe in siblings) {
+ NSError* deletionError = nil;
+ [oqe deleteFromDatabase:&deletionError];
+ if(deletionError) {
+ ckkserror("ckks", self, "Couldn't delete OQE sibling(%@) for %@: %@", oqe, iqe.uuid, deletionError);
+ }
+ }
}
ckksinfo("ckks", self, "CKKSMirrorEntry was deleted: %@ %@ error: %@", recordID, ckme, error);
// TODO: actually pass error back up
} 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);
+ CKKSTLKShareRecord* share = [CKKSTLKShareRecord 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);
// 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);
- ckksinfo("ckksfetch", self, "Processing record modification(%@): %@", record.recordType, record);
+ @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] 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;
+ 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;
+ }
}
}
} 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, skipping update: %@", record);
- return;
+ ckksnotice("ckksresync", self, "Already know about this item record, updating anyway: %@", record.recordID);
}
}
// If we found an old version in the database; this might be an update
if(ckme) {
- if([ckme matchesCKRecord:record]) {
+ 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");
+ ckksnotice("ckks", self, "CloudKit has told us of record we already know about for %@; skipping update", ckme.uuid);
return;
}
if(error) {
ckkserror("ckks", self, "couldn't save new CKRecord to database: %@ %@", record, error);
} else {
- ckksdebug("ckks", self, "CKKSMirrorEntry was created: %@", ckme);
+ ckksinfo("ckks", self, "CKKSMirrorEntry was created: %@", ckme);
}
NSError* iqeerror = nil;
if(iqeerror) {
ckkserror("ckks", self, "Couldn't save modified incoming queue entry: %@", iqeerror);
} else {
- ckksdebug("ckks", self, "CKKSIncomingQueueEntry was created: %@", iqe);
+ ckksinfo("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];
+ NSArray<CKKSOutgoingQueueEntry*>* siblings = [CKKSOutgoingQueueEntry allWithUUID:iqe.uuid
+ states:@[SecCKKSStateNew,
+ SecCKKSStateReencrypt,
+ SecCKKSStateError]
+ zoneID:self.zoneID
+ error:&error];
if(error) {
- ckkserror("ckks", self, "Couldn't load OutgoingQueueEntry: %@", error);
- }
- if(oqe) {
- [self _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateDeleted error:&error];
+ ckkserror("ckks", self, "Couldn't load OQE sibling for %@: %@", iqe.uuid, 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);
+ for(CKKSOutgoingQueueEntry* oqe in siblings) {
+ NSError* deletionError = nil;
+ [oqe deleteFromDatabase:&deletionError];
+ if(deletionError) {
+ ckkserror("ckks", self, "Couldn't delete OQE sibling(%@) for %@: %@", oqe, iqe.uuid, deletionError);
}
}
}
}
}
- // For now, drop into the synckeys table as a 'remote' key, then ask for a rekey operation.
CKKSKey* remotekey = [[CKKSKey alloc] initWithCKRecord: record];
- // We received this from an update. Don't use, yet.
+ // 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;
}
// We've saved a new key in the database; trigger a rekey operation.
- [self _onqueueKeyStateMachineRequestProcess];
+ [self.stateMachine _onqueueHandleFlag:CKKSFlagKeyStateProcessRequested];
+}
+
+- (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
+ CKKSTLKShareRecord* share = [[CKKSTLKShareRecord alloc] initWithCKRecord:record];
+ [share saveToDatabase:&error];
+ if(error) {
+ ckkserror("ckksshare", self, "Couldn't save new TLK share to database: %@ %@", share, error);
+ }
+
+ [self.stateMachine _onqueueHandleFlag:CKKSFlagKeyStateProcessRequested];
}
- (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) {
- NSError* ckperror = nil;
- CKKSCurrentKeyPointer* ckp = [CKKSCurrentKeyPointer tryFromDatabase:((CKKSKeyClass*) record.recordID.recordName) zoneID:self.zoneID error:&ckperror];
- if(ckperror) {
- ckkserror("ckksresync", self, "error loading ckp: %@", ckperror);
- }
- if(!ckp) {
+ if(!oldckp) {
ckkserror("ckksresync", self, "BUG: No current key pointer matching resynced CloudKit record: %@", record);
- } else if(![ckp matchesCKRecord:record]) {
- ckkserror("ckksresync", self, "BUG: Local current key pointer doesn't match resynced CloudKit record: %@ %@", ckp, 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, "Already know about this current key pointer, skipping update: %@", record);
- return;
+ ckksnotice("ckksresync", self, "Current key pointer has 'changed', but it matches our local copy: %@", record);
}
}
ckksinfo("ckkskey", self, "CKRecord was %@", record);
}
- // We've saved a new key in the database; trigger a rekey operation.
- [self _onqueueKeyStateMachineRequestProcess];
+ 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.stateMachine _onqueueHandleFlag:CKKSFlagKeyStateProcessRequested];
+ }
}
- (void)_onqueueCKRecordCurrentItemPointerChanged:(CKRecord*)record resync:(bool)resync {
- (void)_onqueueCKRecordManifestChanged:(CKRecord*)record resync:(bool)resync
{
+ dispatch_assert_queue(self.queue);
NSError* error = nil;
CKKSPendingManifest* manifest = [[CKKSPendingManifest alloc] initWithCKRecord:record];
[manifest saveToDatabase:&error];
- (void)_onqueueCKRecordManifestLeafChanged:(CKRecord*)record resync:(bool)resync
{
+ dispatch_assert_queue(self.queue);
NSError* error = nil;
CKKSManifestLeafRecord* manifestLeaf = [[CKKSManifestPendingLeafRecord alloc] initWithCKRecord:record];
[manifestLeaf saveToDatabase:&error];
}
- (void)_onqueueCKRecordDeviceStateChanged:(CKRecord*)record resync:(bool)resync {
+ dispatch_assert_queue(self.queue);
if(resync) {
NSError* dserror = nil;
CKKSDeviceStateEntry* cdse = [CKKSDeviceStateEntry tryFromDatabase:record.recordID.recordName zoneID:self.zoneID error:&dserror];
}
}
+- (bool)_onqueueResetAllInflightOQE:(NSError**)error {
+ dispatch_assert_queue(self.queue);
+ 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);
if([state isEqualToString: SecCKKSStateDeleted]) {
// Hurray, this must be a success
- SecBoolNSErrorCallback callback = self.pendingSyncCallbacks[oqe.uuid];
- if(callback) {
- callback(true, nil);
+ SecBoolNSErrorCallback syncCallback = [[CKKSViewManager manager] claimCallbackForUUID:oqe.uuid];
+ if(syncCallback) {
+ syncCallback(true, nil);
}
[oqe deleteFromDatabase: &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];
- (bool)_onqueueErrorOutgoingQueueEntry: (CKKSOutgoingQueueEntry*) oqe itemError: (NSError*) itemError error: (NSError* __autoreleasing*) error {
dispatch_assert_queue(self.queue);
- SecBoolNSErrorCallback callback = self.pendingSyncCallbacks[oqe.uuid];
+ SecBoolNSErrorCallback callback = [[CKKSViewManager manager] claimCallbackForUUID:oqe.uuid];
if(callback) {
callback(false, itemError);
}
NSError* localerror = nil;
- oqe.state = SecCKKSStateError;
- [oqe saveToDatabase: &localerror];
+ // Now, delete the OQE: it's never coming back
+ [oqe deleteFromDatabase:&localerror];
if(localerror) {
- ckkserror("ckks", self, "Couldn't set %@ as error: %@", oqe, localerror);
+ ckkserror("ckks", self, "Couldn't delete %@ (due to error %@): %@", oqe, itemError, localerror);
}
if(error && localerror) {
return localerror == nil;
}
-- (bool)_onQueueUpdateLatestManifestWithError:(NSError**)error
+- (bool)dispatchSyncWithConnection:(SecDbConnectionRef _Nonnull)dbconn
+ readWriteTxion:(BOOL)readWriteTxion
+ block:(CKKSDatabaseTransactionResult (^)(void))block
{
- dispatch_assert_queue(self.queue);
- CKKSManifest* manifest = [CKKSManifest latestTrustedManifestForZone:self.zoneName error:error];
- if (manifest) {
- self.latestManifest = manifest;
- return true;
- }
- else {
- return false;
+ 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.
+
+ SecDbTransactionType txtionType = readWriteTxion ? kSecDbExclusiveRemoteCKKSTransactionType : kSecDbNormalTransactionType;
+ bool ret = kc_transaction_type(dbconn, txtionType, &cferror, ^bool{
+ __block CKKSDatabaseTransactionResult result = CKKSDatabaseTransactionRollback;
+
+ CKKSSQLInTransaction = true;
+ if(readWriteTxion) {
+ CKKSSQLInWriteTransaction = true;
+ }
+
+ dispatch_sync(self.queue, ^{
+ result = block();
+ });
+
+ if(readWriteTxion) {
+ CKKSSQLInWriteTransaction = false;
+ }
+ CKKSSQLInTransaction = false;
+ return result == CKKSDatabaseTransactionCommit;
+ });
+
+ if(cferror) {
+ ckkserror("ckks", self, "error doing database transaction, major problems ahead: %@", cferror);
}
+ return ret;
}
-- (bool)checkTLK: (CKKSKey*) proposedTLK error: (NSError * __autoreleasing *) error {
- // Until we have Octagon Trust, accept this TLK iff we have its actual AES key in the keychain
+- (void)dispatchSyncWithSQLTransaction:(CKKSDatabaseTransactionResult (^)(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]);
- if([proposedTLK loadKeyMaterialFromKeychain:error]) {
- // Hurray!
- return true;
- } else {
- return false;
+ CFErrorRef cferror = NULL;
+ kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
+ return [self dispatchSyncWithConnection:dbt
+ readWriteTxion:YES
+ block:block];
+
+ });
+ if(cferror) {
+ ckkserror("ckks", self, "error getting database connection, major problems ahead: %@", cferror);
}
+
+ (void)transaction;
}
-- (void) dispatchAsync: (bool (^)(void)) block {
- // We need to call kc_with_dbt, which blocks. Route up through a global queue...
- __weak __typeof(self) weakSelf = self;
+- (void)dispatchSyncWithReadOnlySQLTransaction:(void (^)(void))block
+{
+ // Please don't jetsam us...
+ os_transaction_t transaction = os_transaction_create([[NSString stringWithFormat:@"com.apple.securityd.ckks.%@", self.zoneName] UTF8String]);
+
+ CFErrorRef cferror = NULL;
+
+ // Note: we are lying to kc_with_dbt here about whether we're read-and-write or read-only.
+ // This is because the SOS engine's queue are broken: SOSEngineSetNotifyPhaseBlock attempts
+ // to take the SOS engine's queue while a SecDb transaction is still ongoing. But, in
+ // SOSEngineCopyPeerConfirmedDigests, SOS takes the engine queue, then calls dsCopyManifestWithViewNameSet()
+ // which attempts to get a read-only SecDb connection.
+ //
+ // The issue manifests when many CKKS read-only transactions are in-flight, and starve out
+ // the pool of read-only connections. Then, a deadlock forms.
+ //
+ // By claiming to be a read-write connection here, we'll contend on the pool of writer threads,
+ // and shouldn't starve SOS of its read thread.
+ //
+ // But, since we pass NO to readWriteTxion, the SQLite transaction will be of type
+ // kSecDbNormalTransactionType, which won't block other readers.
+
+ kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
+ return [self dispatchSyncWithConnection:dbt
+ readWriteTxion:NO
+ block:^CKKSDatabaseTransactionResult {
+ block();
+ return CKKSDatabaseTransactionCommit;
+ }];
- dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
- [weakSelf dispatchSync:block];
});
+ if(cferror) {
+ ckkserror("ckks", self, "error getting database connection, major problems ahead: %@", cferror);
+ }
+
+ (void)transaction;
}
-// Use this if you have a potential database connection already
-- (void) dispatchSyncWithConnection: (SecDbConnectionRef) dbconn block: (bool (^)(void)) block {
- if(dbconn) {
- dispatch_sync(self.queue, ^{
- CFErrorRef cferror = NULL;
- kc_transaction_type(dbconn, kSecDbExclusiveRemoteCKKSTransactionType, &cferror, block);
+- (BOOL)insideSQLTransaction
+{
+ return CKKSSQLInTransaction;
+}
- if(cferror) {
- ckkserror("ckks", self, "error doing database transaction (sync), major problems ahead: %@", cferror);
- }
- });
+#pragma mark - CKKSZone operations
+
+- (void)beginCloudKitOperation
+{
+ [self.accountTracker registerForNotificationsOfCloudKitAccountStatusChange:self];
+}
+
+- (CKKSResultOperation*)createAccountLoggedInDependency:(NSString*)message
+{
+ WEAKIFY(self);
+ CKKSResultOperation* accountLoggedInDependency = [CKKSResultOperation named:@"account-logged-in-dependency" withBlock:^{
+ STRONGIFY(self);
+ ckksnotice("ckkszone", self, "%@", message);
+ }];
+ accountLoggedInDependency.descriptionErrorCode = CKKSResultDescriptionPendingAccountLoggedIn;
+ return accountLoggedInDependency;
+}
+
+#pragma mark - CKKSZoneUpdateReceiverProtocol
+
+- (CKKSAccountStatus)accountStatusFromCKAccountInfo:(CKAccountInfo*)info
+{
+ if(!info) {
+ return CKKSAccountStatusUnknown;
+ }
+ if(info.accountStatus == CKAccountStatusAvailable &&
+ info.hasValidCredentials) {
+ return CKKSAccountStatusAvailable;
} else {
- [self dispatchSync: block];
+ return CKKSAccountStatusNoAccount;
}
}
-- (void) dispatchSync: (bool (^)(void)) block {
- // important enough to block this thread. Must get a connection first, though!
- __weak __typeof(self) weakSelf = self;
+- (void)cloudkitAccountStateChange:(CKAccountInfo* _Nullable)oldAccountInfo to:(CKAccountInfo*)currentAccountInfo
+{
+ ckksnotice("ckkszone", self, "%@ Received notification of CloudKit account status change, moving from %@ to %@",
+ self.zoneID.zoneName,
+ oldAccountInfo,
+ currentAccountInfo);
- CFErrorRef cferror = NULL;
- kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
- __strong __typeof(weakSelf) strongSelf = weakSelf;
- if(!strongSelf) {
- ckkserror("ckks", strongSelf, "received callback for released object");
- return false;
+ // Filter for device2device encryption and cloudkit grey mode
+ CKKSAccountStatus oldStatus = [self accountStatusFromCKAccountInfo:oldAccountInfo];
+ CKKSAccountStatus currentStatus = [self accountStatusFromCKAccountInfo:currentAccountInfo];
+
+ if(oldStatus == currentStatus) {
+ ckksnotice("ckkszone", self, "Computed status of new CK account info is same as old status: %@", [CKKSAccountStateTracker stringFromAccountStatus:currentStatus]);
+ return;
+ }
+
+ switch(currentStatus) {
+ case CKKSAccountStatusAvailable: {
+ ckksnotice("ckkszone", self, "Logged into iCloud.");
+ [self handleCKLogin];
+
+ if(self.accountLoggedInDependency) {
+ [self.operationQueue addOperation:self.accountLoggedInDependency];
+ self.accountLoggedInDependency = nil;
+ };
}
+ break;
- __block bool ok = false;
- __block CFErrorRef cferror = NULL;
+ case CKKSAccountStatusNoAccount: {
+ ckksnotice("ckkszone", self, "Logging out of iCloud. Shutting down.");
- dispatch_sync(strongSelf.queue, ^{
- ok = kc_transaction_type(dbt, kSecDbExclusiveRemoteCKKSTransactionType, &cferror, block);
- });
- return ok;
+ if(!self.accountLoggedInDependency) {
+ self.accountLoggedInDependency = [self createAccountLoggedInDependency:@"CloudKit account logged in again."];
+ }
+
+ [self handleCKLogout];
+ }
+ break;
+
+ case CKKSAccountStatusUnknown: {
+ // We really don't expect to receive this as a notification, but, okay!
+ ckksnotice("ckkszone", self, "Account status has become undetermined. Pausing for %@", self.zoneID.zoneName);
+
+ if(!self.accountLoggedInDependency) {
+ self.accountLoggedInDependency = [self createAccountLoggedInDependency:@"CloudKit account logged in again."];
+ }
+
+ [self handleCKLogout];
+ }
+ break;
+ }
+}
+
+- (void)handleCKLogin
+{
+ ckksnotice("ckks", self, "received a notification of CK login");
+ if(!SecCKKSIsEnabled()) {
+ ckksnotice("ckks", self, "Skipping CloudKit initialization due to disabled CKKS");
+ return;
+ }
+
+ dispatch_sync(self.queue, ^{
+ ckksinfo("ckkszone", self, "received a notification of CK login");
+
+ // Change our condition variables to reflect that we think we're logged in
+ self.accountStatus = CKKSAccountStatusAvailable;
+ self.loggedOut = [[CKKSCondition alloc] initToChain:self.loggedOut];
+ [self.loggedIn fulfill];
});
- if(cferror) {
- ckkserror("ckks", self, "error getting database connection (sync), major problems ahead: %@", cferror);
+
+ [self.stateMachine handleFlag:CKKSFlagCloudKitLoggedIn];
+
+ [self.accountStateKnown fulfill];
+}
+
+- (void)handleCKLogout
+{
+ dispatch_sync(self.queue, ^{
+ ckksinfo("ckkszone", self, "received a notification of CK logout");
+
+ self.accountStatus = CKKSAccountStatusNoAccount;
+ self.loggedIn = [[CKKSCondition alloc] initToChain:self.loggedIn];
+ [self.loggedOut fulfill];
+ });
+
+ [self.stateMachine handleFlag:CKKSFlagCloudKitLoggedOut];
+
+ [self.accountStateKnown fulfill];
+}
+
+#pragma mark - Trust operations
+
+- (void)beginTrustedOperation:(NSArray<id<CKKSPeerProvider>>*)peerProviders
+ suggestTLKUpload:(CKKSNearFutureScheduler*)suggestTLKUpload
+ requestPolicyCheck:(CKKSNearFutureScheduler*)requestPolicyCheck
+{
+ for(id<CKKSPeerProvider> peerProvider in peerProviders) {
+ [peerProvider registerForPeerChangeUpdates:self];
}
+
+ [self.launch addEvent:@"beginTrusted"];
+
+ dispatch_sync(self.queue, ^{
+ ckksnotice("ckkstrust", self, "Beginning trusted operation");
+ self.operationDependencies.peerProviders = peerProviders;
+
+ CKKSAccountStatus oldTrustStatus = self.trustStatus;
+
+ self.suggestTLKUpload = suggestTLKUpload;
+ self.requestPolicyCheck = requestPolicyCheck;
+
+ self.trustStatus = CKKSAccountStatusAvailable;
+ [self.stateMachine _onqueueHandleFlag:CKKSFlagBeginTrustedOperation];
+
+ if(oldTrustStatus == CKKSAccountStatusNoAccount) {
+ ckksnotice("ckkstrust", self, "Moving from an untrusted status; we need to process incoming queue and scan for any new items");
+
+ [self.stateMachine _onqueueHandleFlag:CKKSFlagProcessIncomingQueue];
+ [self.stateMachine _onqueueHandleFlag:CKKSFlagScanLocalItems];
+ }
+ });
}
-- (void)dispatchSyncWithAccountQueue:(bool (^)(void))block
+- (void)endTrustedOperation
{
- [SOSAccount performOnAccountQueue:^{
- [CKKSManifest performWithAccountInfo:^{
- [self dispatchSync:^bool{
- __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();
- }];
- return result;
- }];
- }];
- }];
+ [self.launch addEvent:@"endTrusted"];
+
+ dispatch_sync(self.queue, ^{
+ ckksnotice("ckkstrust", self, "Ending trusted operation");
+
+ self.operationDependencies.peerProviders = @[];
+
+ self.suggestTLKUpload = nil;
+
+ self.trustStatus = CKKSAccountStatusNoAccount;
+ [self.stateMachine _onqueueHandleFlag:CKKSFlagEndTrustedOperation];
+ });
+}
+
+- (BOOL)itemSyncingEnabled
+{
+ if(!self.operationDependencies.syncingPolicy) {
+ ckksnotice("ckks", self, "No syncing policy loaded; item syncing is disabled");
+ return NO;
+ } else {
+ return [self.operationDependencies.syncingPolicy isSyncingEnabledForView:self.zoneName];
+ }
+}
+
+- (void)setCurrentSyncingPolicy:(TPSyncingPolicy*)syncingPolicy policyIsFresh:(BOOL)policyIsFresh
+{
+ dispatch_sync(self.queue, ^{
+ BOOL oldEnabled = [self itemSyncingEnabled];
+
+ self.operationDependencies.syncingPolicy = syncingPolicy;
+
+ BOOL enabled = [self itemSyncingEnabled];
+ if(enabled != oldEnabled) {
+ ckksnotice("ckks", self, "Syncing for this view is now %@ (policy: %@)", enabled ? @"enabled" : @"paused", self.operationDependencies.syncingPolicy);
+ }
+
+ if(enabled) {
+ CKKSResultOperation* incomingOp = [self processIncomingQueue:false after:nil policyConsideredAuthoritative:policyIsFresh];
+ [self processOutgoingQueueAfter:incomingOp ckoperationGroup:nil];
+ }
+ });
}
-#pragma mark - CKKSZoneUpdateReceiver
+- (void)receivedItemForWrongView
+{
+ [self.requestPolicyCheck trigger];
+}
-- (void)notifyZoneChange: (CKRecordZoneNotification*) notification {
- ckksinfo("ckks", self, "hurray, got a zone change for %@ %@", self, notification);
+#pragma mark - CKKSChangeFetcherClient
- [self fetchAndProcessCKChanges:CKKSFetchBecauseAPNS];
+- (BOOL)zoneIsReadyForFetching
+{
+ __block BOOL ready = NO;
+
+ [self dispatchSyncWithReadOnlySQLTransaction:^{
+ ready = (bool)[self _onQueueZoneIsReadyForFetching];
+ }];
+
+ return ready;
}
-// Must be on the queue when this is called
-- (void)handleCKLogin {
+- (BOOL)_onQueueZoneIsReadyForFetching
+{
dispatch_assert_queue(self.queue);
+ if(self.accountStatus != CKKSAccountStatusAvailable) {
+ ckksnotice("ckksfetch", self, "Not participating in fetch: not logged in");
+ return NO;
+ }
- if(!self.setupStarted) {
- [self _onqueueInitializeZone];
- } else {
- ckksinfo("ckks", self, "ignoring login as setup has already started");
+ CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.operationDependencies.zoneID.zoneName];
+
+ if(!ckse.ckzonecreated) {
+ ckksnotice("ckksfetch", self, "Not participating in fetch: zone not created yet");
+ return NO;
}
+ return YES;
}
-- (void)handleCKLogout {
- NSBlockOperation* logout = [NSBlockOperation blockOperationWithBlock: ^{
- [self dispatchSync:^bool {
- ckksnotice("ckks", self, "received a notification of CK logout for %@", self.zoneName);
- NSError* error = nil;
+- (CKKSCloudKitFetchRequest*)participateInFetch
+{
+ __block CKKSCloudKitFetchRequest* request = [[CKKSCloudKitFetchRequest alloc] init];
- [self _onqueueResetLocalData: &error];
+ [self dispatchSyncWithReadOnlySQLTransaction:^{
+ if (![self _onQueueZoneIsReadyForFetching]) {
+ ckksnotice("ckksfetch", self, "skipping fetch since zones are not ready");
+ return;
+ }
- if(error) {
- ckkserror("ckks", self, "error while resetting local data: %@", error);
+ request.participateInFetch = true;
+ [self.launch addEvent:@"fetch"];
+
+ 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;
+ request.resync = true;
+ } else {
+ CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.zoneName];
+ if(!ckse) {
+ ckkserror("ckksfetch", self, "couldn't fetch zone change token for %@", self.zoneName);
+ return;
}
- return true;
- }];
+ request.changeToken = ckse.changeToken;
+ }
}];
- logout.name = @"cloudkit-logout";
- [self scheduleAccountStatusOperation: logout];
+ if (request.changeToken == nil) {
+ self.launch.firstLaunch = true;
+ }
+
+ return request;
}
-#pragma mark - CKKSChangeFetcherErrorOracle
+- (void)changesFetched:(NSArray<CKRecord*>*)changedRecords
+ deletedRecordIDs:(NSArray<CKKSCloudKitDeletion*>*)deletedRecords
+ newChangeToken:(CKServerChangeToken*)newChangeToken
+ moreComing:(BOOL)moreComing
+ resync:(BOOL)resync
+{
+ [self.launch addEvent:@"changes-fetched"];
-- (bool) isFatalCKFetchError: (NSError*) error {
- __weak __typeof(self) weakSelf = self;
+ if(changedRecords.count == 0 && deletedRecords.count == 0 && !moreComing && !resync) {
+ // Early-exit, so we don't pick up the account keys or kick off an IncomingQueue operation for no changes
+ [self dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
+ ckksinfo("ckksfetch", self, "No record changes in this fetch");
- // Again, note that this handles exactly one zone. Mutli-zone errors are not supported.
- 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(NSError* partialError in partialErrors.allValues) {
- if([partialError.domain isEqualToString:CKErrorDomain] && (partialError.code == CKErrorChangeTokenExpired)) {
- isChangeTokenExpiredError = true;
+ NSError* error = nil;
+ CKKSZoneStateEntry* state = [CKKSZoneStateEntry state:self.zoneName];
+ state.lastFetchTime = [NSDate date]; // The last fetch happened right now!
+ state.changeToken = newChangeToken;
+ state.moreRecordsInCloudKit = moreComing;
+ [state saveToDatabase:&error];
+ if(error) {
+ ckkserror("ckksfetch", self, "Couldn't save new server change token: %@", error);
}
- }
+ return CKKSDatabaseTransactionCommit;
+ }];
+ return;
}
- if(isChangeTokenExpiredError) {
- ckkserror("ckks", self, "Received notice that our change token is out of date. Resetting local data...");
- [self cancelAllOperations];
- 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;
+ [self dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
+ 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) {
+ // If we're performing a resync, we need to keep track of everything that's actively in
+ // CloudKit during the fetch, (so that we can find anything that's on-disk and not in CloudKit).
+ // Please note that if, during a resync, the fetch errors, we won't be notified. If a record is in
+ // the first refetch but not the second, it'll be added to our set, and the second resync will not
+ // delete the record (which is a consistency violation, but only with actively changing records).
+ // A third resync should correctly delete that record.
+
+ if(self.resyncRecordsSeen == nil) {
+ self.resyncRecordsSeen = [NSMutableSet set];
+ }
+ for(CKRecord* r in changedRecords) {
+ [self.resyncRecordsSeen addObject:r.recordID.recordName];
}
- if(resetOp.error) {
- ckksnotice("ckks", strongSelf, "CloudKit-inspired local reset of %@ ended with error: %@", strongSelf.zoneID, error);
+ // Is there More Coming? If not, self.resyncRecordsSeen contains everything in CloudKit. Inspect for anything extra!
+ if(moreComing) {
+ ckksnotice("ckksresync", self, "In a resync, but there's More Coming. Waiting to scan for extra items.");
+
} else {
- ckksnotice("ckksreset", strongSelf, "re-initializing zone %@", strongSelf.zoneID);
- [strongSelf initializeZone];
+ // 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) {
+ if([self.resyncRecordsSeen containsObject:uuid]) {
+ 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];
+ }
+ }
+
+ // Now that we've inspected resyncRecordsSeen, reset it for the next time through
+ self.resyncRecordsSeen = nil;
}
- }];
+ }
- [resetHandler addDependency:resetOp];
- [self scheduleOperation:resetHandler];
- return true;
- }
+ CKKSZoneStateEntry* state = [CKKSZoneStateEntry state:self.zoneName];
+ state.lastFetchTime = [NSDate date]; // The last fetch happened right now!
+ state.changeToken = newChangeToken;
+ state.moreRecordsInCloudKit = moreComing;
+ [state saveToDatabase:&error];
+ if(error) {
+ ckkserror("ckksfetch", self, "Couldn't save new server change token: %@", error);
+ }
+
+ if(!moreComing) {
+ // Might as well kick off a IQO!
+ [self processIncomingQueue:false];
+ ckksnotice("ckksfetch", self, "Beginning incoming processing for %@", self.zoneID);
+ }
+
+ ckksnotice("ckksfetch", self, "Finished processing changes for %@", self.zoneID);
- 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)) {
+ return CKKSDatabaseTransactionCommit;
+ }];
+}
+
+- (bool)ckErrorOrPartialError:(NSError *)error isError:(CKErrorCode)errorCode
+{
+ if((error.code == errorCode) && [error.domain isEqualToString:CKErrorDomain]) {
+ return true;
+ } else if((error.code == CKErrorPartialFailure) && [error.domain isEqualToString:CKErrorDomain]) {
NSDictionary* partialErrors = error.userInfo[CKPartialErrorsByItemIDKey];
- for(NSError* partialError in partialErrors.allValues) {
- if([partialError.domain isEqualToString:CKErrorDomain] && ((partialError.code == CKErrorUserDeletedZone) || (partialError.code == CKErrorZoneNotFound))) {
- isDeletedZoneError = true;
- }
+
+ NSError* partialError = partialErrors[self.zoneID];
+ if ((partialError.code == errorCode) && [partialError.domain isEqualToString:CKErrorDomain]) {
+ return true;
}
}
+ return false;
+}
- if(isDeletedZoneError) {
- ckkserror("ckks", self, "Received notice that our zone does not exist. Resetting all data.");
- [self cancelAllOperations];
- CKKSResultOperation* resetOp = [self resetCloudKitZone];
- CKKSResultOperation* resetHandler = [CKKSResultOperation named:@"reset-handler" withBlock:^{
- __strong __typeof(self) strongSelf = weakSelf;
- if(!strongSelf) {
- ckkserror("ckks", strongSelf, "received callback for released object");
- return;
- }
+- (bool)shouldRetryAfterFetchError:(NSError*)error {
- if(resetOp.error) {
- ckksnotice("ckks", strongSelf, "CloudKit-inspired zone reset of %@ ended with error: %@", strongSelf.zoneID, resetOp.error);
- } else {
- ckksnotice("ckksreset", strongSelf, "re-initializing zone %@", strongSelf.zoneID);
- [strongSelf initializeZone];
- }
- }];
+ bool isChangeTokenExpiredError = [self ckErrorOrPartialError:error isError:CKErrorChangeTokenExpired];
+ if(isChangeTokenExpiredError) {
+ ckkserror("ckks", self, "Received notice that our change token is out of date (for %@). Resetting local data...", self.zoneID);
- [resetHandler addDependency:resetOp];
- [self scheduleOperation:resetHandler];
+ [self.stateMachine handleFlag:CKKSFlagChangeTokenExpired];
return true;
}
+ bool isDeletedZoneError = [self ckErrorOrPartialError:error isError:CKErrorZoneNotFound];
+ if(isDeletedZoneError) {
+ ckkserror("ckks", self, "Received notice that our zone(%@) does not exist. Resetting local data.", self.zoneID);
+
+ [self.stateMachine handleFlag:CKKSFlagCloudKitZoneMissing];
+ 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 true;
+ return false;
}
- return false;
+ return true;
+}
+
+#pragma mark CKKSPeerUpdateListener
+
+- (void)selfPeerChanged:(id<CKKSPeerProvider>)provider
+{
+ // 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:(id<CKKSPeerProvider>)provider
+{
+ // 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.stateMachine handleFlag:CKKSFlagTrustedPeersSetChanged];
}
#pragma mark - Test Support
- (bool) outgoingQueueEmpty: (NSError * __autoreleasing *) error {
__block bool ret = false;
- [self dispatchSync: ^bool{
+ [self dispatchSyncWithReadOnlySQLTransaction:^{
NSArray* queueEntries = [CKKSOutgoingQueueEntry all: error];
ret = queueEntries && ([queueEntries count] == 0);
- return true;
}];
return ret;
}
-- (CKKSResultOperation*)waitForFetchAndIncomingQueueProcessing {
- if(!SecCKKSIsEnabled()) {
- ckksinfo("ckks", self, "Due to disabled CKKS, returning fast from waitForFetchAndIncomingQueueProcessing");
- return nil;
- }
-
- CKKSResultOperation* op = [self fetchAndProcessCKChanges:CKKSFetchBecauseTesting];
- [op waitUntilFinished];
- return op;
+- (void)waitForFetchAndIncomingQueueProcessing
+{
+ [[self.zoneChangeFetcher inflightFetch] waitUntilFinished];
+ [self waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
}
- (void)waitForKeyHierarchyReadiness {
}
}
+#pragma mark - NSOperation assistance
+
+- (void)scheduleOperation:(NSOperation*)op
+{
+ if(self.halted) {
+ ckkserror("ckkszone", self, "attempted to schedule an operation on a halted zone, ignoring");
+ return;
+ }
+
+ [op addNullableDependency:self.accountLoggedInDependency];
+ [self.operationQueue addOperation: op];
+}
+
+// to be used rarely, if at all
+- (bool)scheduleOperationWithoutDependencies:(NSOperation*)op
+{
+ if(self.halted) {
+ ckkserror("ckkszone", self, "attempted to schedule an non-dependent operation on a halted zone, ignoring");
+ return false;
+ }
+
+ [self.operationQueue addOperation: op];
+ return true;
+}
+
+- (void)waitUntilAllOperationsAreFinished
+{
+ [self.operationQueue waitUntilAllOperationsAreFinished];
+}
+
+- (void)waitForOperationsOfClass:(Class)operationClass
+{
+ NSArray* operations = [self.operationQueue.operations copy];
+ for(NSOperation* op in operations) {
+ if([op isKindOfClass:operationClass]) {
+ [op 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];
+ }
+
+ @synchronized(self.scanLocalItemsOperations) {
+ for(NSOperation* op in self.scanLocalItemsOperations) {
+ [op cancel];
+ }
+ [self.scanLocalItemsOperations removeAllObjects];
+ }
+}
+
- (void)cancelAllOperations {
- [self.zoneSetupOperation cancel];
- [self.keyStateMachineOperation cancel];
[self.keyStateReadyDependency cancel];
[self.zoneChangeFetcher cancel];
+ [self.notifyViewChangedScheduler cancel];
- [super cancelAllOperations];
+ [self cancelPendingOperations];
+ [self.operationQueue cancelAllOperations];
+}
- [self dispatchSync:^bool{
- [self _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateCancelled withError: nil];
- return true;
- }];
+- (void)halt {
+ [self.stateMachine haltOperation];
+
+ // Synchronously set the 'halted' bit
+ dispatch_sync(self.queue, ^{
+ self.halted = true;
+ });
+
+ // Bring all operations down, too
+ [self cancelAllOperations];
+
+ // And now, wait for all operations that are running
+ for(NSOperation* op in self.operationQueue.operations) {
+ if(op.isExecuting) {
+ [op waitUntilFinished];
+ }
+ }
+
+ // Don't send any more notifications, either
+ _notifierClass = nil;
}
- (NSDictionary*)status {
#define stringify(obj) CKKSNilToNSNull([obj description])
#define boolstr(obj) (!!(obj) ? @"yes" : @"no")
- __block NSDictionary* ret = nil;
+ __block NSMutableDictionary* ret = nil;
__block NSError* error = nil;
- CKKSManifest* manifest = [CKKSManifest latestTrustedManifestForZone:self.zoneName error:&error];
- [self dispatchSync: ^bool {
- NSString* uuidTLK = [CKKSKey currentKeyForClass:SecCKKSKeyClassTLK zoneID:self.zoneID error:&error].uuid;
- NSString* uuidClassA = [CKKSKey currentKeyForClass:SecCKKSKeyClassA zoneID:self.zoneID error:&error].uuid;
- NSString* uuidClassC = [CKKSKey currentKeyForClass:SecCKKSKeyClassC zoneID:self.zoneID error:&error].uuid;
-
- NSString* manifestGeneration = manifest ? [NSString stringWithFormat:@"%lu", (unsigned long)manifest.generationCount] : nil;
+ ret = [[self fastStatus] mutableCopy];
+
+ [self dispatchSyncWithReadOnlySQLTransaction:^{
+ CKKSCurrentKeySet* keyset = [CKKSCurrentKeySet loadForZone:self.zoneID];
+ if(keyset.error) {
+ error = keyset.error;
+ }
if(error) {
ckkserror("ckks", self, "error during status: %@", error);
[mutDeviceStates addObject: [obj description]];
}];
- 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),
- @"setup": boolstr(self.setupComplete),
- @"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),
+ NSArray* tlkShares = [CKKSTLKShareRecord 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 countsByState:self.zoneID error:&error]),
- @"iqe": CKKSNilToNSNull([CKKSIncomingQueueEntry countsByState:self.zoneID error:&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(uuidTLK),
- @"currentClassA": CKKSNilToNSNull(uuidClassA),
- @"currentClassC": CKKSNilToNSNull(uuidClassC),
- @"currentManifestGen": CKKSNilToNSNull(manifestGeneration),
-
- @"zoneSetupOperation": stringify(self.zoneSetupOperation),
- @"viewSetupOperation": stringify(self.viewSetupOperation),
- @"keyStateOperation": stringify(self.keyStateMachineOperation),
- @"lastIncomingQueueOperation": stringify(self.lastIncomingQueueOperation),
- @"lastNewTLKOperation": stringify(self.lastNewTLKOperation),
- @"lastOutgoingQueueOperation": stringify(self.lastOutgoingQueueOperation),
- @"lastRecordZoneChangesOperation": stringify(self.lastRecordZoneChangesOperation),
- @"lastProcessReceivedKeysOperation": stringify(self.lastProcessReceivedKeysOperation),
- @"lastReencryptOutgoingItemsOperation":stringify(self.lastReencryptOutgoingItemsOperation),
- @"lastScanLocalItemsOperation": stringify(self.lastScanLocalItemsOperation),
- };
- return false;
+ @"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),
+ @"itemsyncing": self.itemSyncingEnabled ? @"enabled" : @"paused",
+ }];
}];
return ret;
}
+- (NSDictionary*)fastStatus {
+
+ __block NSDictionary* ret = nil;
+
+ [self dispatchSyncWithReadOnlySQLTransaction:^{
+ CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.zoneName];
+
+ 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",
+ @"accounttracker": stringify(self.accountTracker),
+ @"fetcher": stringify(self.zoneChangeFetcher),
+ @"zoneCreated": boolstr(ckse.ckzonecreated),
+ @"zoneSubscribed": boolstr(ckse.ckzonesubscribed),
+ @"keystate": CKKSNilToNSNull(self.keyHierarchyState),
+ @"statusError": [NSNull null],
+ @"launchSequence": CKKSNilToNSNull([self.launch eventsByTime]),
+
+ @"lastIncomingQueueOperation": stringify(self.lastIncomingQueueOperation),
+ @"lastNewTLKOperation": stringify(self.lastNewTLKOperation),
+ @"lastOutgoingQueueOperation": stringify(self.lastOutgoingQueueOperation),
+ @"lastProcessReceivedKeysOperation": stringify(self.lastProcessReceivedKeysOperation),
+ @"lastReencryptOutgoingItemsOperation":stringify(self.lastReencryptOutgoingItemsOperation),
+ };
+ }];
+ return ret;
+}
#endif /* OCTAGON */
@end