CKKSHealTLKSharesOperation.m [plain text]
/*
* Copyright (c) 2017 Apple Inc. All Rights Reserved.
*
* @APPLE_LICENSE_HEADER_START@
*
* This file contains Original Code and/or Modifications of Original Code
* as defined in and that are subject to the Apple Public Source License
* Version 2.0 (the 'License'). You may not use this file except in
* compliance with the License. Please obtain a copy of the License at
* http://www.opensource.apple.com/apsl/ and read it before using this
* file.
*
* The Original Code and all software distributed under the License are
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
* Please see the License for the specific language governing rights and
* limitations under the License.
*
* @APPLE_LICENSE_HEADER_END@
*/
#if OCTAGON
#import <CloudKit/CloudKit.h>
#import <CloudKit/CloudKit_Private.h>
#import "keychain/ckks/CKKSKeychainView.h"
#import "keychain/ckks/CKKSCurrentKeyPointer.h"
#import "keychain/ckks/CKKSKey.h"
#import "keychain/ckks/CKKSHealTLKSharesOperation.h"
#import "keychain/ckks/CKKSGroupOperation.h"
#import "keychain/ckks/CKKSTLKShareRecord.h"
#import "keychain/ot/ObjCImprovements.h"
#import "CKKSPowerCollection.h"
@interface CKKSHealTLKSharesOperation ()
@property NSBlockOperation* cloudkitModifyOperationFinished;
@end
@implementation CKKSHealTLKSharesOperation
@synthesize intendedState = _intendedState;
@synthesize nextState = _nextState;
- (instancetype)init {
return nil;
}
- (instancetype)initWithOperationDependencies:(CKKSOperationDependencies*)operationDependencies
ckks:(CKKSKeychainView*)ckks
{
if(self = [super init]) {
_ckks = ckks;
_deps = operationDependencies;
_nextState = SecCKKSZoneKeyStateHealTLKSharesFailed;
_intendedState = SecCKKSZoneKeyStateBecomeReady;
}
return self;
}
- (void)groupStart {
/*
* We've been invoked because something is wonky with the tlk shares.
*
* Attempt to figure out what it is, and what we can do about it.
*/
WEAKIFY(self);
if(self.cancelled) {
ckksnotice("ckksshare", self.deps.zoneID, "CKKSHealTLKSharesOperation cancelled, quitting");
return;
}
NSArray<CKKSPeerProviderState*>* trustStates = [self.deps currentTrustStates];
NSError* error = nil;
__block CKKSCurrentKeySet* keyset = nil;
[self.deps.databaseProvider dispatchSyncWithReadOnlySQLTransaction:^{
keyset = [CKKSCurrentKeySet loadForZone:self.deps.zoneID];
}];
if(keyset.error) {
self.nextState = SecCKKSZoneKeyStateUnhealthy;
self.error = keyset.error;
ckkserror("ckksshare", self.deps.zoneID, "couldn't load current keys: can't fix TLK shares");
return;
} else {
ckksnotice("ckksshare", self.deps.zoneID, "Key set is }
[CKKSPowerCollection CKKSPowerEvent:kCKKSPowerEventTLKShareProcessing zone:self.deps.zoneID.zoneName];
// Okay! Perform the checks.
if(![keyset.tlk loadKeyMaterialFromKeychain:&error] || error) {
// Well, that's no good. We can't share a TLK we don't have.
if([self.deps.lockStateTracker isLockedError: error]) {
ckkserror("ckksshare", self.deps.zoneID, "Keychain is locked: can't fix shares yet: self.nextState = SecCKKSZoneKeyStateBecomeReady;
} else {
ckkserror("ckksshare", self.deps.zoneID, "couldn't load current tlk from keychain: self.nextState = SecCKKSZoneKeyStateUnhealthy;
}
return;
}
NSSet<CKKSTLKShareRecord*>* newShares = [CKKSHealTLKSharesOperation createMissingKeyShares:keyset
trustStates:trustStates
error:&error];
if(error) {
ckkserror("ckksshare", self.deps.zoneID, "Unable to create shares: self.nextState = SecCKKSZoneKeyStateUnhealthy;
return;
}
if(newShares.count == 0u) {
ckksnotice("ckksshare", self.deps.zoneID, "Don't believe we need to change any TLKShares, stopping");
self.nextState = self.intendedState;
return;
}
keyset.pendingTLKShares = [newShares allObjects];
// Let's double-check: if we upload these TLKShares, will the world be right?
BOOL newSharesSufficient = [CKKSHealTLKSharesOperation areNewSharesSufficient:keyset
trustStates:trustStates
error:&error];
if(!newSharesSufficient || error) {
ckksnotice("ckksshare", self.deps.zoneID, "New shares won't resolve the share issue; erroring to avoid infinite loops");
self.nextState = SecCKKSZoneKeyStateError;
return;
}
// Fire up our CloudKit operation!
NSMutableArray<CKRecord *>* recordsToSave = [[NSMutableArray alloc] init];
NSMutableArray<CKRecordID *>* recordIDsToDelete = [[NSMutableArray alloc] init];
NSMutableDictionary<CKRecordID*, CKRecord*>* attemptedRecords = [[NSMutableDictionary alloc] init];
ckksnotice("ckksshare", self.deps.zoneID, "Uploading for(CKKSTLKShareRecord* share in newShares) {
ckksnotice("ckksshare", self.deps.zoneID, "Uploading TLKShare to
CKRecord* record = [share CKRecordWithZoneID:self.deps.zoneID];
[recordsToSave addObject: record];
attemptedRecords[record.recordID] = record;
}
// Use the spare operation trick to wait for the CKModifyRecordsOperation to complete
self.cloudkitModifyOperationFinished = [NSBlockOperation named:@"heal-tlkshares-modify-operation-finished" withBlock:^{}];
[self dependOnBeforeGroupFinished: self.cloudkitModifyOperationFinished];
// Get the CloudKit operation ready...
CKModifyRecordsOperation* modifyRecordsOp = [[CKModifyRecordsOperation alloc] initWithRecordsToSave:recordsToSave
recordIDsToDelete:recordIDsToDelete];
modifyRecordsOp.atomic = YES;
modifyRecordsOp.longLived = NO;
// very important: get the TLKShares off-device ASAP
modifyRecordsOp.configuration.automaticallyRetryNetworkFailures = NO;
modifyRecordsOp.configuration.discretionaryNetworkBehavior = CKOperationDiscretionaryNetworkBehaviorNonDiscretionary;
modifyRecordsOp.configuration.isCloudKitSupportOperation = YES;
modifyRecordsOp.group = self.deps.ckoperationGroup;
ckksnotice("ckksshare", self.deps.zoneID, "Operation group is
modifyRecordsOp.perRecordCompletionBlock = ^(CKRecord *record, NSError * _Nullable error) {
STRONGIFY(self);
// These should all fail or succeed as one. Do the hard work in the records completion block.
if(!error) {
ckksnotice("ckksshare", self.deps.zoneID, "Successfully completed upload for record } else {
ckkserror("ckksshare", self.deps.zoneID, "error on row: }
};
modifyRecordsOp.modifyRecordsCompletionBlock = ^(NSArray<CKRecord *> *savedRecords, NSArray<CKRecordID *> *deletedRecordIDs, NSError *error) {
STRONGIFY(self);
CKKSKeychainView* strongCKKS = self.ckks;
[self.deps.databaseProvider dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
if(error == nil) {
// Success. Persist the records to the CKKS database
ckksnotice("ckksshare", self.deps.zoneID, "Completed TLK Share heal operation with success");
NSError* localerror = nil;
// Save the new CKRecords to the database
for(CKRecord* record in savedRecords) {
CKKSTLKShareRecord* savedShare = [[CKKSTLKShareRecord alloc] initWithCKRecord:record];
bool saved = [savedShare saveToDatabase:&localerror];
if(!saved || localerror != nil) {
// erroring means we were unable to save the new TLKShare records to the database. This will cause us to try to reupload them. Fail.
// No recovery from this, really...
ckkserror("ckksshare", self.deps.zoneID, "Couldn't save new TLKShare record to database: self.error = localerror;
self.nextState = SecCKKSZoneKeyStateError;
return CKKSDatabaseTransactionCommit;
} else {
ckksnotice("ckksshare", self.deps.zoneID, "Successfully completed upload for }
}
// Successfully sharing TLKs means we're now in ready!
self.nextState = SecCKKSZoneKeyStateBecomeReady;
} else {
ckkserror("ckksshare", self.deps.zoneID, "Completed TLK Share heal operation with error: [strongCKKS _onqueueCKWriteFailed:error attemptedRecordsChanged:attemptedRecords];
// Send the key state machine into tlksharesfailed
self.nextState = SecCKKSZoneKeyStateHealTLKSharesFailed;
}
return CKKSDatabaseTransactionCommit;
}];
// Notify that we're done
[self.operationQueue addOperation: self.cloudkitModifyOperationFinished];
};
[self.deps.ckdatabase addOperation:modifyRecordsOp];
}
- (void)cancel {
[self.cloudkitModifyOperationFinished cancel];
[super cancel];
}
+ (BOOL)areNewSharesSufficient:(CKKSCurrentKeySet*)keyset
trustStates:(NSArray<CKKSPeerProviderState*>*)trustStates
error:(NSError* __autoreleasing*)error
{
for(CKKSPeerProviderState* trustState in trustStates) {
NSError* localError = nil;
NSSet<id<CKKSPeer>>* peersMissingShares = [trustState findPeersMissingTLKSharesFor:keyset
error:&localError];
if(peersMissingShares == nil || localError) {
if(trustState.essential) {
if(error) {
*error = localError;
}
return NO;
} else {
ckksnotice("ckksshare", keyset.tlk, "Failed to find peers for nonessential system: // Not a hard failure.
}
}
if(peersMissingShares.count > 0) {
ckksnotice("ckksshare", keyset.tlk, "New share set is missing shares for peers: return NO;
}
}
return YES;
}
+ (NSSet<CKKSTLKShareRecord*>* _Nullable)createMissingKeyShares:(CKKSCurrentKeySet*)keyset
trustStates:(NSArray<CKKSPeerProviderState*>*)trustStates
error:(NSError* __autoreleasing*)error
{
NSError* localerror = nil;
NSSet<CKKSTLKShareRecord*>* newShares = nil;
// If any one of our trust states succeed, this function doesn't have an error
for(CKKSPeerProviderState* trustState in trustStates) {
NSError* stateError = nil;
NSSet<CKKSTLKShareRecord*>* newTrustShares = [self createMissingKeyShares:keyset
peers:trustState
error:&stateError];
if(newTrustShares && !stateError) {
newShares = newShares ? [newShares setByAddingObjectsFromSet:newTrustShares] : newTrustShares;
} else {
ckksnotice("ckksshare", keyset.tlk, "Unable to create shares for trust set if(localerror == nil) {
localerror = stateError;
}
}
}
// Only report an error if none of the trust states were able to succeed
if(newShares) {
return newShares;
} else {
if(error && localerror) {
*error = localerror;
}
return nil;
}
}
+ (NSSet<CKKSTLKShareRecord*>*)createMissingKeyShares:(CKKSCurrentKeySet*)keyset
peers:(CKKSPeerProviderState*)trustState
error:(NSError* __autoreleasing*)error
{
NSError* localerror = nil;
if(![keyset.tlk ensureKeyLoaded:&localerror]) {
ckkserror("ckksshare", keyset.tlk, "TLK not loaded; cannot make shares for peers: if(error) {
*error = localerror;
}
return nil;
}
NSSet<id<CKKSPeer>>* remainingPeers = [trustState findPeersMissingTLKSharesFor:keyset
error:&localerror];
if(!remainingPeers) {
ckkserror("ckksshare", keyset.tlk, "Unable to find peers missing TLKShares: if(error) {
*error = localerror;
}
return nil;
}
NSMutableSet<CKKSTLKShareRecord*>* newShares = [NSMutableSet set];
for(id<CKKSPeer> peer in remainingPeers) {
if(!peer.publicEncryptionKey) {
ckksnotice("ckksshare", keyset.tlk, "No need to make TLK for continue;
}
// Create a share for this peer.
ckksnotice("ckksshare", keyset.tlk, "Creating share of CKKSTLKShareRecord* newShare = [CKKSTLKShareRecord share:keyset.tlk
as:trustState.currentSelfPeers.currentSelf
to:peer
epoch:-1
poisoned:0
error:&localerror];
if(localerror) {
ckkserror("ckksshare", keyset.tlk, "Couldn't create new share for if(error) {
*error = localerror;
}
return nil;
}
[newShares addObject: newShare];
}
return newShares;
}
@end;
#endif