CKKSFetchAllRecordZoneChangesOperation.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@
*/
#import <Foundation/Foundation.h>
#if OCTAGON
#import "keychain/ckks/CloudKitDependencies.h"
#import "keychain/ckks/CKKS.h"
#import "keychain/ckks/CKKSKeychainView.h"
#import "keychain/ckks/CKKSZoneStateEntry.h"
#import "keychain/ckks/CKKSFetchAllRecordZoneChangesOperation.h"
#import "keychain/ckks/CKKSMirrorEntry.h"
#import "keychain/ckks/CKKSManifest.h"
#import "keychain/ckks/CKKSManifestLeafRecord.h"
#include <securityd/SecItemServer.h>
@interface CKKSFetchAllRecordZoneChangesOperation()
@property CKDatabaseOperation<CKKSFetchRecordZoneChangesOperation>* fetchRecordZoneChangesOperation;
@property CKOperationGroup* ckoperationGroup;
@end
@implementation CKKSFetchAllRecordZoneChangesOperation
// Sets up an operation to fetch all changes from the server, and collect them until we know if the fetch completes.
// As a bonus, you can depend on this operation without worry about NSOperation completion block dependency issues.
- (instancetype)init {
return nil;
}
- (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks ckoperationGroup:(CKOperationGroup*)ckoperationGroup {
if(self = [super init]) {
_ckks = ckks;
_ckoperationGroup = ckoperationGroup;
self.zoneID = ckks.zoneID;
self.resync = false;
self.modifications = [[NSMutableDictionary alloc] init];
self.deletions = [[NSMutableDictionary alloc] init];
// Can't fetch unless the zone is created.
[self addNullableDependency:ckks.viewSetupOperation];
}
return self;
}
- (void)_onqueueRecordsChanged:(NSArray*)records
{
for (CKRecord* record in records) {
[self.ckks _onqueueCKRecordChanged:record resync:self.resync];
}
}
- (void)_updateLatestTrustedManifest
{
CKKSKeychainView* ckks = self.ckks;
NSError* error = nil;
NSArray* pendingManifests = [CKKSPendingManifest all:&error];
NSUInteger greatestKnownManifestGeneration = [CKKSManifest greatestKnownGenerationCount];
for (CKKSPendingManifest* manifest in pendingManifests) {
if (manifest.generationCount >= greatestKnownManifestGeneration) {
[manifest commitToDatabaseWithError:&error];
}
else {
// if this is an older generation, just get rid of it
[manifest deleteFromDatabase:&error];
}
}
if (![ckks _onQueueUpdateLatestManifestWithError:&error]) {
self.error = error;
ckkserror("ckksfetch", ckks, "failed to get latest manifest");
}
}
- (void)_onqueueProcessRecordDeletions
{
CKKSKeychainView* ckks = self.ckks;
[self.deletions enumerateKeysAndObjectsUsingBlock:^(CKRecordID * _Nonnull recordID, NSString * _Nonnull recordType, BOOL * _Nonnull stop) {
ckksinfo("ckksfetch", ckks, "Processing record deletion(
// <rdar://problem/32475600> CKKS: Check Current Item pointers in the Manifest
// TODO: check that these deletions match a manifest upload
// Delegate these back up into the CKKS object for processing
[ckks _onqueueCKRecordDeleted:recordID recordType:recordType resync:self.resync];
}];
}
- (void)_onqueueScanForExtraneousLocalItems
{
// TODO: must scan through all CKMirrorEntries and determine if any exist that CloudKit didn't tell us about
CKKSKeychainView* ckks = self.ckks;
NSError* error = nil;
if(self.resync) {
ckksnotice("ckksresync", ckks, "Comparing local UUIDs against the CloudKit list");
NSMutableArray<NSString*>* uuids = [[CKKSMirrorEntry allUUIDs: &error] mutableCopy];
for(NSString* uuid in uuids) {
if([self.modifications objectForKey: [[CKRecordID alloc] initWithRecordName: uuid zoneID: ckks.zoneID]]) {
ckksdebug("ckksresync", ckks, "UUID } else {
CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase: uuid zoneID:ckks.zoneID error: &error];
if(error != nil) {
ckkserror("ckksresync", ckks, "Couldn't read an item from the database, but it used to be there: self.error = error;
continue;
}
ckkserror("ckksresync", ckks, "BUG: Local item [ckks _onqueueCKRecordDeleted:ckme.item.storedCKRecord.recordID recordType:ckme.item.storedCKRecord.recordType resync:self.resync];
}
}
}
}
- (void)groupStart {
__weak __typeof(self) weakSelf = self;
CKKSKeychainView* ckks = self.ckks;
if(!ckks) {
ckkserror("ckksresync", ckks, "no CKKS object");
return;
}
[ckks dispatchSync: ^bool{
ckks.lastRecordZoneChangesOperation = self;
NSError* error = nil;
NSQualityOfService qos = NSQualityOfServiceUtility;
CKFetchRecordZoneChangesOptions* options = [[CKFetchRecordZoneChangesOptions alloc] init];
if(self.resync) {
ckksnotice("ckksresync", ckks, "Beginning resync fetch!");
options.previousServerChangeToken = nil;
// currently, resyncs are user initiated (or the key hierarchy is upset, which is implicitly user initiated)
qos = NSQualityOfServiceUserInitiated;
} else {
// This is the normal case: fetch only the delta since the last fetch
CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state: ckks.zoneName];
if(error || !ckse) {
ckkserror("ckksfetch", ckks, "couldn't fetch zone status for self.error = error;
return false;
}
ckksnotice("ckksfetch", ckks, "Beginning fetch(
options.previousServerChangeToken = ckse.changeToken;
if(ckse.changeToken == nil) {
// First sync is special.
qos = NSQualityOfServiceUserInitiated;
}
}
self.fetchRecordZoneChangesOperation = [[ckks.fetchRecordZoneChangesOperationClass alloc] initWithRecordZoneIDs: @[ckks.zoneID] optionsByRecordZoneID:@{ckks.zoneID: options}];
self.fetchRecordZoneChangesOperation.fetchAllChanges = YES;
self.fetchRecordZoneChangesOperation.qualityOfService = qos;
self.fetchRecordZoneChangesOperation.group = self.ckoperationGroup;
ckksnotice("ckksfetch", ckks, "Operation group is
self.fetchRecordZoneChangesOperation.recordChangedBlock = ^(CKRecord *record) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
__strong __typeof(strongSelf.ckks) strongCKKS = strongSelf.ckks;
if(!strongSelf) {
ckkserror("ckksfetch", strongCKKS, "received callback for released object");
return;
}
ckksinfo("ckksfetch", strongCKKS, "CloudKit notification: record changed(
// Add this to the modifications, and remove it from the deletions
[strongSelf.modifications setObject: record forKey: record.recordID];
[strongSelf.deletions removeObjectForKey: record.recordID];
};
self.fetchRecordZoneChangesOperation.recordWithIDWasDeletedBlock = ^(CKRecordID *recordID, NSString *recordType) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
__strong __typeof(strongSelf.ckks) strongCKKS = strongSelf.ckks;
if(!strongSelf) {
ckkserror("ckksfetch", strongCKKS, "received callback for released object");
return;
}
ckksinfo("ckksfetch", strongCKKS, "CloudKit notification: deleted record(
// Add to the deletions, and remove any pending modifications
[strongSelf.modifications removeObjectForKey: recordID];
[strongSelf.deletions setObject: recordType forKey: recordID];
};
// This class only supports fetching from a single zone, so we can ignore recordZoneID
self.fetchRecordZoneChangesOperation.recordZoneChangeTokensUpdatedBlock = ^(CKRecordZoneID *recordZoneID, CKServerChangeToken *serverChangeToken, NSData *clientChangeTokenData) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
__strong __typeof(strongSelf.ckks) strongCKKS = strongSelf.ckks;
if(!strongSelf) {
ckkserror("ckksfetch", strongCKKS, "received callback for released object");
return;
}
ckksinfo("ckksfetch", strongCKKS, "Received a new server change token: strongSelf.serverChangeToken = serverChangeToken;
};
// Completion blocks don't count for dependencies. Use this intermediate operation hack instead.
NSBlockOperation* recordZoneChangesCompletedOperation = [[NSBlockOperation alloc] init];
recordZoneChangesCompletedOperation.name = @"record-zone-changes-completed";
self.fetchRecordZoneChangesOperation.recordZoneFetchCompletionBlock = ^(CKRecordZoneID *recordZoneID, CKServerChangeToken *serverChangeToken, NSData *clientChangeTokenData, BOOL moreComing, NSError * recordZoneError) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
__strong __typeof(strongSelf.ckks) blockCKKS = strongSelf.ckks;
if(!strongSelf) {
ckkserror("ckksfetch", blockCKKS, "received callback for released object");
return;
}
if(!blockCKKS) {
ckkserror("ckksfetch", blockCKKS, "no CKKS object");
return;
}
ckksnotice("ckksfetch", blockCKKS, "Record zone fetch complete: changeToken= (unsigned long)strongSelf.deletions.count,
(unsigned long)strongSelf.deletions.count,
recordZoneError);
// Completion! Mark these down.
if(recordZoneError) {
strongSelf.error = recordZoneError;
}
strongSelf.serverChangeToken = serverChangeToken;
if(recordZoneError != nil) {
// An error occurred. All our fetches are useless. Skip to the end.
} else {
// Commit these changes!
__block NSError* error = nil;
NSMutableDictionary<NSString*, NSMutableArray*>* changedRecordsDict = [[NSMutableDictionary alloc] init];
[blockCKKS dispatchSyncWithAccountKeys:^bool{
// let's process records in a specific order by type
// 1. Manifest leaf records, without which the manifest master records are meaningless
// 2. Manifest master records, which will be used to validate incoming items
// 3. Intermediate key records
// 4. Current key records
// 5. Item records
[strongSelf.modifications enumerateKeysAndObjectsUsingBlock:^(CKRecordID* _Nonnull recordID, CKRecord* _Nonnull record, BOOL* stop) {
ckksinfo("ckksfetch", blockCKKS, "Sorting record modification NSMutableArray* changedRecordsByType = changedRecordsDict[record.recordType];
if(!changedRecordsByType) {
changedRecordsByType = [[NSMutableArray alloc] init];
changedRecordsDict[record.recordType] = changedRecordsByType;
};
[changedRecordsByType addObject:record];
}];
if ([CKKSManifest shouldSyncManifests]) {
if (!strongSelf.resync) {
[strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordManifestLeafType]];
[strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordManifestType]];
}
[strongSelf _updateLatestTrustedManifest];
}
[strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordIntermediateKeyType]];
[strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordCurrentKeyType]];
[strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordItemType]];
[strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordCurrentItemType]];
[strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordDeviceStateType]];
[strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordTLKShareType]];
[strongSelf _onqueueProcessRecordDeletions];
[strongSelf _onqueueScanForExtraneousLocalItems];
CKKSZoneStateEntry* state = [CKKSZoneStateEntry state: blockCKKS.zoneName];
state.lastFetchTime = [NSDate date]; // The last fetch happened right now!
if(strongSelf.serverChangeToken) {
ckksdebug("ckksfetch", blockCKKS, "Zone change fetch complete: saving new server change token: state.changeToken = strongSelf.serverChangeToken;
}
[state saveToDatabase:&error];
if(error) {
ckkserror("ckksfetch", blockCKKS, "Couldn't save new server change token: strongSelf.error = error;
}
if(error) {
ckkserror("ckksfetch", blockCKKS, "horrible error occurred: strongSelf.error = error;
return false;
}
return true;
}];
}
};
// Called with overall operation success. As I understand it, this block will be called for every operation.
// In the case of, e.g., network failure, the recordZoneFetchCompletionBlock will not be called, but this one will.
self.fetchRecordZoneChangesOperation.fetchRecordZoneChangesCompletionBlock = ^(NSError * _Nullable operationError) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
__strong __typeof(strongSelf.ckks) strongCKKS = strongSelf.ckks;
if(!strongSelf) {
ckkserror("ckksfetch", strongCKKS, "received callback for released object");
return;
}
ckksnotice("ckksfetch", strongCKKS, "Record zone changes fetch complete: error= if(operationError) {
strongSelf.error = operationError;
}
// Trigger the fake 'we're done' operation.
[strongSelf runBeforeGroupFinished: recordZoneChangesCompletedOperation];
};
[self dependOnBeforeGroupFinished: recordZoneChangesCompletedOperation];
[self dependOnBeforeGroupFinished: self.fetchRecordZoneChangesOperation];
[ckks.database addOperation: self.fetchRecordZoneChangesOperation];
return true;
}];
}
- (void)cancel {
[self.fetchRecordZoneChangesOperation cancel];
[super cancel];
}
@end
#endif