CKKSOutgoingQueueOperation.m [plain text]
/*
* Copyright (c) 2016 Apple Inc. All Rights Reserved.
*
* @APPLE_LICENSE_HEADER_START@
*
* This file contains Original Code and/or Modifications of Original Code
* as defined in and that are subject to the Apple Public Source License
* Version 2.0 (the 'License'). You may not use this file except in
* compliance with the License. Please obtain a copy of the License at
* http://www.opensource.apple.com/apsl/ and read it before using this
* file.
*
* The Original Code and all software distributed under the License are
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
* Please see the License for the specific language governing rights and
* limitations under the License.
*
* @APPLE_LICENSE_HEADER_END@
*/
#import "CKKSKeychainView.h"
#import "CKKSCurrentKeyPointer.h"
#import "CKKSOutgoingQueueOperation.h"
#import "CKKSIncomingQueueEntry.h"
#import "CKKSItemEncrypter.h"
#import "CKKSOutgoingQueueEntry.h"
#import "CKKSReencryptOutgoingItemsOperation.h"
#import "CKKSManifest.h"
#import "CKKSAnalyticsLogger.h"
#include <securityd/SecItemServer.h>
#include <securityd/SecItemDb.h>
#include <Security/SecItemPriv.h>
#include <utilities/SecADWrapper.h>
#import "CKKSPowerCollection.h"
#if OCTAGON
@interface CKKSOutgoingQueueOperation()
@property CKModifyRecordsOperation* modifyRecordsOperation;
@end
@implementation CKKSOutgoingQueueOperation
- (instancetype)init {
if(self = [super init]) {
}
return nil;
}
- (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks ckoperationGroup:(CKOperationGroup*)ckoperationGroup {
if(self = [super init]) {
_ckks = ckks;
_ckoperationGroup = ckoperationGroup;
[self addNullableDependency:ckks.viewSetupOperation];
[self addNullableDependency:ckks.holdOutgoingQueueOperation];
// Depend on all previous CKKSOutgoingQueueOperations
[self linearDependencies:ckks.outgoingQueueOperations];
// We also depend on the view being setup and the key hierarchy being reasonable
[self addNullableDependency:ckks.viewSetupOperation];
[self addNullableDependency:ckks.keyStateReadyDependency];
}
return self;
}
- (void) groupStart {
// Synchronous, on some thread. Get back on the CKKS queue for thread-safety.
__weak __typeof(self) weakSelf = self;
CKKSKeychainView* ckks = self.ckks;
if(!ckks) {
ckkserror("ckksoutgoing", ckks, "no ckks object");
return;
}
[ckks dispatchSyncWithAccountKeys: ^bool{
ckks.lastOutgoingQueueOperation = self;
if(self.cancelled) {
ckksnotice("ckksoutgoing", ckks, "CKKSOutgoingQueueOperation cancelled, quitting");
return false;
}
NSError* error = nil;
// We only actually care about queue items in the 'new' state
NSArray<CKKSOutgoingQueueEntry*> * queueEntries = [CKKSOutgoingQueueEntry fetch:SecCKKSOutgoingQueueItemsAtOnce state: SecCKKSStateNew zoneID:ckks.zoneID error:&error];
if(error != nil) {
ckkserror("ckksoutgoing", ckks, "Error fetching outgoing queue records: self.error = error;
return false;
}
ckksinfo("ckksoutgoing", ckks, "processing outgoing queue:
NSMutableDictionary<CKRecordID*, CKRecord*>* recordsToSave = [[NSMutableDictionary alloc] init];
NSMutableSet<CKRecordID*>* oqesModified = [[NSMutableSet alloc] init];
NSMutableArray<CKRecordID *>* recordIDsToDelete = [[NSMutableArray alloc] init];
CKKSCurrentKeyPointer* currentClassAKeyPointer = [CKKSCurrentKeyPointer fromDatabase: SecCKKSKeyClassA zoneID:ckks.zoneID error: &error];
CKKSCurrentKeyPointer* currentClassCKeyPointer = [CKKSCurrentKeyPointer fromDatabase: SecCKKSKeyClassC zoneID:ckks.zoneID error: &error];
NSMutableDictionary<CKKSKeyClass*, CKKSCurrentKeyPointer*>* currentKeysToSave = [[NSMutableDictionary alloc] init];
bool needsReencrypt = false;
if(error != nil) {
ckkserror("ckksoutgoing", ckks, "Couldn't load current class keys: return false;
}
for(CKKSOutgoingQueueEntry* oqe in queueEntries) {
if(self.cancelled) {
secdebug("ckksoutgoing", "CKKSOutgoingQueueOperation cancelled, quitting");
return false;
}
CKKSOutgoingQueueEntry* inflight = [CKKSOutgoingQueueEntry tryFromDatabase: oqe.uuid state:SecCKKSStateInFlight zoneID:ckks.zoneID error: &error];
if(!error && inflight) {
// There is an inflight request with this UUID. Leave this request in-queue until CloudKit returns and we resolve the inflight request.
continue;
}
// If this item is not a delete, check the encryption status of this item.
if(![oqe.action isEqualToString: SecCKKSActionDelete]) {
// Check if this item is encrypted under a current key
if([oqe.item.parentKeyUUID isEqualToString: currentClassAKeyPointer.currentKeyUUID]) {
// Excellent.
currentKeysToSave[SecCKKSKeyClassA] = currentClassAKeyPointer;
} else if([oqe.item.parentKeyUUID isEqualToString: currentClassCKeyPointer.currentKeyUUID]) {
// Works for us!
currentKeysToSave[SecCKKSKeyClassC] = currentClassCKeyPointer;
} else {
// This item is encrypted under an old key. Set it up for reencryption and move on.
ckksnotice("ckksoutgoing", ckks, "Item's encryption key ( [ckks _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateReencrypt error:&error];
if(error) {
ckkserror("ckksoutgoing", ckks, "couldn't save oqe to database: self.error = error;
error = nil;
}
needsReencrypt = true;
continue;
}
}
if([oqe.action isEqualToString: SecCKKSActionAdd]) {
CKRecord* record = [oqe.item CKRecordWithZoneID: ckks.zoneID];
recordsToSave[record.recordID] = record;
[oqesModified addObject: record.recordID];
[ckks _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateInFlight error:&error];
if(error) {
ckkserror("ckksoutgoing", ckks, "couldn't save state for CKKSOutgoingQueueEntry: self.error = error;
}
} else if ([oqe.action isEqualToString: SecCKKSActionDelete]) {
[recordIDsToDelete addObject: [[CKRecordID alloc] initWithRecordName: oqe.item.uuid zoneID: ckks.zoneID]];
[ckks _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateInFlight error:&error];
if(error) {
ckkserror("ckksoutgoing", ckks, "couldn't save state for CKKSOutgoingQueueEntry: }
} else if ([oqe.action isEqualToString: SecCKKSActionModify]) {
// Load the existing item from the ckmirror.
CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase: oqe.item.uuid zoneID:ckks.zoneID error:&error];
if(!ckme) {
// This is a problem: we have an update to an item that doesn't exist.
// Either: an Add operation we launched failed due to a CloudKit error (conflict?) and this is a follow-on update
// Or: ?
ckkserror("ckksoutgoing", ckks, "update to a record that doesn't exist? // treat as an add.
CKRecord* record = [oqe.item CKRecordWithZoneID: ckks.zoneID];
recordsToSave[record.recordID] = record;
[oqesModified addObject: record.recordID];
[ckks _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateInFlight error:&error];
if(error) {
ckkserror("ckksoutgoing", ckks, "couldn't save state for CKKSOutgoingQueueEntry: self.error = error;
}
} else {
if(![oqe.item.storedCKRecord.recordChangeTag isEqual: ckme.item.storedCKRecord.recordChangeTag]) {
// The mirror entry has updated since this item was created. If we proceed, we might end up with
// a badly-authenticated record.
ckksnotice("ckksoutgoing", ckks, "Record ( [ckks _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateReencrypt error:&error];
if(error) {
ckkserror("ckksoutgoing", ckks, "couldn't save oqe to database: self.error = error;
error = nil;
}
needsReencrypt = true;
continue;
}
// Grab the old ckrecord and update it
CKRecord* record = [oqe.item updateCKRecord: ckme.item.storedCKRecord zoneID: ckks.zoneID];
recordsToSave[record.recordID] = record;
[oqesModified addObject: record.recordID];
[ckks _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateInFlight error:&error];
if(error) {
ckkserror("ckksoutgoing", ckks, "couldn't save state for CKKSOutgoingQueueEntry: }
}
}
}
if(needsReencrypt) {
ckksnotice("ckksoutgoing", ckks, "An item needs reencryption!");
CKKSReencryptOutgoingItemsOperation* op = [[CKKSReencryptOutgoingItemsOperation alloc] initWithCKKSKeychainView:ckks ckoperationGroup:self.ckoperationGroup];
[ckks scheduleOperation: op];
}
if([recordsToSave count] == 0 && [recordIDsToDelete count] == 0) {
// Nothing to do! exit.
ckksnotice("ckksoutgoing", ckks, "Nothing in outgoing queue to process");
if(self.ckoperationGroup) {
ckksnotice("ckksoutgoing", ckks, "End of operation group: }
return true;
}
self.itemsProcessed = recordsToSave.count;
NSBlockOperation* modifyComplete = [[NSBlockOperation alloc] init];
modifyComplete.name = @"modifyRecordsComplete";
[self dependOnBeforeGroupFinished: modifyComplete];
if ([CKKSManifest shouldSyncManifests]) {
if (ckks.egoManifest) {
[ckks.egoManifest updateWithNewOrChangedRecords:recordsToSave.allValues deletedRecordIDs:recordIDsToDelete];
for(CKRecord* record in [ckks.egoManifest allCKRecordsWithZoneID:ckks.zoneID]) {
recordsToSave[record.recordID] = record;
}
NSError* saveError = nil;
if (![ckks.egoManifest saveToDatabase:&saveError]) {
self.error = saveError;
ckkserror("ckksoutgoing", ckks, "could not save ego manifest with error: }
}
else {
ckkserror("ckksoutgoing", ckks, "could not get current ego manifest to update");
}
}
void (^modifyRecordsCompletionBlock)(NSArray<CKRecord*>*, NSArray<CKRecordID*>*, NSError*) = ^(NSArray<CKRecord *> *savedRecords, NSArray<CKRecordID *> *deletedRecordIDs, NSError *ckerror) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
__strong __typeof(strongSelf.ckks) strongCKKS = strongSelf.ckks;
if(!strongSelf || !strongCKKS) {
ckkserror("ckksoutgoing", strongCKKS, "received callback for released object");
return;
}
[strongCKKS dispatchSync: ^bool{
if(ckerror) {
ckkserror("ckksoutgoing", strongCKKS, "error processing outgoing queue:
// Tell CKKS about any out-of-date records
[strongCKKS _onqueueCKWriteFailed:ckerror attemptedRecordsChanged:recordsToSave];
// Check if these are due to key records being out of date. We'll see a CKErrorBatchRequestFailed, with a bunch of errors inside
if([ckerror.domain isEqualToString:CKErrorDomain] && (ckerror.code == CKErrorPartialFailure)) {
NSMutableDictionary<CKRecordID*, NSError*>* failedRecords = ckerror.userInfo[CKPartialErrorsByItemIDKey];
ckksnotice("ckksoutgoing", strongCKKS, "failed records for(CKRecordID* recordID in failedRecords.allKeys) {
NSError* recordError = failedRecords[recordID];
if(recordError.code == CKErrorServerRecordChanged) {
if([recordID.recordName isEqualToString: SecCKKSKeyClassA] ||
[recordID.recordName isEqualToString: SecCKKSKeyClassC]) {
// The current key pointers have updated without our knowledge, so CloudKit failed this operation. Mark all records as 'needs reencryption' and kick that off.
[strongSelf _onqueueModifyAllRecordsAsReencrypt: failedRecords.allKeys];
// Note that _onqueueCKWriteFailed is responsible for kicking the key state machine, so we don't need to do it here.
// This will wait for the key hierarchy to become 'ready'
CKKSReencryptOutgoingItemsOperation* op = [[CKKSReencryptOutgoingItemsOperation alloc] initWithCKKSKeychainView:strongCKKS ckoperationGroup:strongSelf.ckoperationGroup];
[strongCKKS scheduleOperation: op];
// Quit the loop so we only do this once
break;
} else {
// CKErrorServerRecordChanged on an item update means that we've been overwritten.
if([oqesModified containsObject:recordID]) {
[strongSelf _onqueueModifyRecordAsError:recordID recordError:recordError];
}
}
} else if(recordError.code == CKErrorBatchRequestFailed) {
// Also fine. This record only didn't succeed because something else failed.
// OQEs should be placed back into the 'new' state, unless they've been overwritten by a new OQE. Other records should be ignored.
if([oqesModified containsObject:recordID]) {
NSError* error = nil;
CKKSOutgoingQueueEntry* inflightOQE = [CKKSOutgoingQueueEntry tryFromDatabase:recordID.recordName state:SecCKKSStateInFlight zoneID:recordID.zoneID error:&error];
CKKSOutgoingQueueEntry* newOQE = [CKKSOutgoingQueueEntry tryFromDatabase:recordID.recordName state:SecCKKSStateNew zoneID:recordID.zoneID error:&error];
if(error) {
ckkserror("ckksoutgoing", strongCKKS, "Couldn't try to fetch an overwriting OQE: }
if(newOQE) {
ckksnotice("ckksoutgoing", strongCKKS, "New modification has come in behind failed change for [strongCKKS _onqueueChangeOutgoingQueueEntry:inflightOQE toState:SecCKKSStateDeleted error:&error];
if(error) {
ckkserror("ckksoutgoing", strongCKKS, "Couldn't delete in-flight OQE: }
} else {
[strongCKKS _onqueueChangeOutgoingQueueEntry:inflightOQE toState:SecCKKSStateNew error:&error];
}
}
} else if ([recordID.recordName hasPrefix:@"Manifest:-:"] || [recordID.recordName hasPrefix:@"ManifestLeafRecord:-:"]) {
[[CKKSAnalyticsLogger logger] logSoftFailureForEventNamed:@"ManifestUpload" withAttributes:@{CKKSManifestZoneKey : strongCKKS.zoneID.zoneName, CKKSManifestSignerIDKey : strongCKKS.egoManifest.signerID, CKKSManifestGenCountKey : @(strongCKKS.egoManifest.generationCount)}];
} else {
// Some unknown error occurred on this record. If it's an OQE, move it to the error state.
ckkserror("ckksoutgoing", strongCKKS, "Unknown error on row: if([oqesModified containsObject:recordID]) {
[strongSelf _onqueueModifyRecordAsError:recordID recordError:recordError];
}
}
}
}
strongSelf.error = error;
return true;
}
ckksnotice("ckksoutgoing", strongCKKS, "Completed processing outgoing queue");
NSError* error = NULL;
CKKSPowerCollection *plstats = [[CKKSPowerCollection alloc] init];
for(CKRecord* record in savedRecords) {
// Save the item records
if([record.recordType isEqualToString: SecCKRecordItemType]) {
CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry fromDatabase: record.recordID.recordName state: SecCKKSStateInFlight zoneID:strongCKKS.zoneID error:&error];
[strongCKKS _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateDeleted error:&error];
if(error) {
ckkserror("ckksoutgoing", strongCKKS, "Couldn't update strongSelf.error = error;
}
error = nil;
CKKSMirrorEntry* ckme = [[CKKSMirrorEntry alloc] initWithCKRecord: record];
[ckme saveToDatabase: &error];
if(error) {
ckkserror("ckksoutgoing", strongCKKS, "Couldn't save strongSelf.error = error;
}
[plstats storedOQE:oqe];
// And the CKCurrentKeyRecords (do we need to do this? Will the server update the change tag on a save which saves nothing?)
} else if([record.recordType isEqualToString: SecCKRecordCurrentKeyType]) {
CKKSCurrentKeyPointer* currentkey = [[CKKSCurrentKeyPointer alloc] initWithCKRecord: record];
[currentkey saveToDatabase: &error];
if(error) {
ckkserror("ckksoutgoing", strongCKKS, "Couldn't save strongSelf.error = error;
}
} else if ([record.recordType isEqualToString:SecCKRecordDeviceStateType]) {
CKKSDeviceStateEntry* newcdse = [[CKKSDeviceStateEntry alloc] initWithCKRecord:record];
[newcdse saveToDatabase:&error];
if(error) {
ckkserror("ckksoutgoing", strongCKKS, "Couldn't save strongSelf.error = error;
}
} else if ([record.recordType isEqualToString:SecCKRecordManifestType]) {
[[CKKSAnalyticsLogger logger] logSuccessForEventNamed:@"ManifestUpload"];
} else if (![record.recordType isEqualToString:SecCKRecordManifestLeafType]) {
ckkserror("ckksoutgoing", strongCKKS, "unknown record type in results: }
}
// Delete the deleted record IDs
for(CKRecordID* ckrecordID in deletedRecordIDs) {
NSError* error = nil;
CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry fromDatabase: ckrecordID.recordName state: SecCKKSStateInFlight zoneID:strongCKKS.zoneID error:&error];
[strongCKKS _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateDeleted error:&error];
if(error) {
ckkserror("ckksoutgoing", strongCKKS, "Couldn't delete strongSelf.error = error;
}
error = nil;
CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase: ckrecordID.recordName zoneID:strongCKKS.zoneID error:&error];
[ckme deleteFromDatabase: &error];
if(error) {
ckkserror("ckksoutgoing", strongCKKS, "Couldn't delete strongSelf.error = error;
}
[plstats deletedOQE:oqe];
}
[plstats commit];
if(strongSelf.error) {
ckkserror("ckksoutgoing", strongCKKS, "Operation failed; rolling back: return false;
}
return true;
}];
[strongSelf.operationQueue addOperation: modifyComplete];
// Kick off another queue process. We expect it to exit instantly, but who knows!
[strongCKKS processOutgoingQueue:strongSelf.ckoperationGroup];
};
ckksinfo("ckksoutgoing", ckks, "Current keys to update: for(CKKSCurrentKeyPointer* keypointer in currentKeysToSave.allValues) {
CKRecord* record = [keypointer CKRecordWithZoneID: ckks.zoneID];
recordsToSave[record.recordID] = record;
}
// Piggyback on this operation to update our device state
NSError* cdseError = nil;
CKKSDeviceStateEntry* cdse = [ckks _onqueueCurrentDeviceStateEntry:&cdseError];
CKRecord* cdseRecord = [cdse CKRecordWithZoneID: ckks.zoneID];
if(cdseError) {
ckkserror("ckksoutgoing", ckks, "Can't make current device state: } else if(!cdseRecord) {
ckkserror("ckksoutgoing", ckks, "Can't make current device state cloudkit record, but no reason why");
} else {
// Add the CDSE to the outgoing records
// TODO: maybe only do this every few hours?
ckksnotice("ckksoutgoing", ckks, "Updating device state: recordsToSave[cdseRecord.recordID] = cdseRecord;
}
ckksinfo("ckksoutgoing", ckks, "Saving records
self.modifyRecordsOperation = [[CKModifyRecordsOperation alloc] initWithRecordsToSave:recordsToSave.allValues recordIDsToDelete:recordIDsToDelete];
self.modifyRecordsOperation.atomic = TRUE;
self.modifyRecordsOperation.timeoutIntervalForRequest = 2;
self.modifyRecordsOperation.qualityOfService = NSQualityOfServiceUtility;
self.modifyRecordsOperation.savePolicy = CKRecordSaveIfServerRecordUnchanged;
self.modifyRecordsOperation.group = self.ckoperationGroup;
ckksnotice("ckksoutgoing", ckks, "Operation group is
self.modifyRecordsOperation.perRecordCompletionBlock = ^(CKRecord *record, NSError * _Nullable error) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
__strong __typeof(strongSelf.ckks) blockCKKS = strongSelf.ckks;
if(!error) {
ckksnotice("ckksoutgoing", blockCKKS, "Record upload successful for } else {
ckkserror("ckksoutgoing", blockCKKS, "error on row: }
};
self.modifyRecordsOperation.modifyRecordsCompletionBlock = modifyRecordsCompletionBlock;
[self dependOnBeforeGroupFinished: self.modifyRecordsOperation];
[ckks.database addOperation: self.modifyRecordsOperation];
return true;
}];
}
- (void)_onqueueModifyRecordAsError:(CKRecordID*)recordID recordError:(NSError*)itemerror {
CKKSKeychainView* ckks = self.ckks;
if(!ckks) {
ckkserror("ckksoutgoing", ckks, "no CKKS object");
return;
}
dispatch_assert_queue(ckks.queue);
NSError* error = nil;
uint64_t count = 0;
// At this stage, cloudkit doesn't give us record types
if([recordID.recordName isEqualToString: SecCKKSKeyClassA] ||
[recordID.recordName isEqualToString: SecCKKSKeyClassC] ||
[recordID.recordName hasPrefix:@"Manifest:-:"] ||
[recordID.recordName hasPrefix:@"ManifestLeafRecord:-:"]) {
// Nothing to do here. We need a whole key refetch and synchronize.
} else {
CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry fromDatabase:recordID.recordName state: SecCKKSStateInFlight zoneID:ckks.zoneID error:&error];
[ckks _onqueueErrorOutgoingQueueEntry: oqe itemError: itemerror error:&error];
if(error) {
ckkserror("ckksoutgoing", ckks, "Couldn't set OQE self.error = error;
}
count ++;
}
}
- (void)_onqueueModifyAllRecordsAsReencrypt: (NSArray<CKRecordID*>*) recordIDs {
CKKSKeychainView* ckks = self.ckks;
if(!ckks) {
ckkserror("ckksoutgoing", ckks, "no CKKS object");
return;
}
dispatch_assert_queue(ckks.queue);
NSError* error = nil;
uint64_t count = 0;
for(CKRecordID* recordID in recordIDs) {
// At this stage, cloudkit doesn't give us record types
if([recordID.recordName isEqualToString: SecCKKSKeyClassA] ||
[recordID.recordName isEqualToString: SecCKKSKeyClassC]) {
// Nothing to do here. We need a whole key refetch and synchronize.
} else {
CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry fromDatabase:recordID.recordName state: SecCKKSStateInFlight zoneID:ckks.zoneID error:&error];
[ckks _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateReencrypt error:&error];
if(error) {
ckkserror("ckksoutgoing", ckks, "Couldn't set OQE self.error = error;
}
count ++;
}
}
SecADAddValueForScalarKey((__bridge CFStringRef) SecCKKSAggdItemReencryption, count);
}
@end;
#endif