2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
21 * @APPLE_LICENSE_HEADER_END@
24 #import <Foundation/Foundation.h>
28 #import "keychain/ckks/CloudKitDependencies.h"
29 #import "keychain/ckks/CKKS.h"
30 #import "keychain/ckks/CKKSKeychainView.h"
31 #import "keychain/ckks/CKKSZoneStateEntry.h"
32 #import "keychain/ckks/CKKSFetchAllRecordZoneChangesOperation.h"
33 #import "keychain/ckks/CKKSMirrorEntry.h"
34 #import "keychain/ckks/CKKSManifest.h"
35 #import "keychain/ckks/CKKSManifestLeafRecord.h"
36 #include <securityd/SecItemServer.h>
39 @interface CKKSFetchAllRecordZoneChangesOperation()
40 @property CKDatabaseOperation<CKKSFetchRecordZoneChangesOperation>* fetchRecordZoneChangesOperation;
41 @property CKOperationGroup* ckoperationGroup;
44 @implementation CKKSFetchAllRecordZoneChangesOperation
46 // Sets up an operation to fetch all changes from the server, and collect them until we know if the fetch completes.
47 // As a bonus, you can depend on this operation without worry about NSOperation completion block dependency issues.
49 - (instancetype)init {
53 - (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks ckoperationGroup:(CKOperationGroup*)ckoperationGroup {
55 if(self = [super init]) {
57 _ckoperationGroup = ckoperationGroup;
58 self.zoneID = ckks.zoneID;
62 self.modifications = [[NSMutableDictionary alloc] init];
63 self.deletions = [[NSMutableDictionary alloc] init];
65 // Can't fetch unless the zone is created.
66 [self addNullableDependency:ckks.viewSetupOperation];
71 - (void)_onqueueRecordsChanged:(NSArray*)records
73 for (CKRecord* record in records) {
74 [self.ckks _onqueueCKRecordChanged:record resync:self.resync];
78 - (void)_updateLatestTrustedManifest
80 CKKSKeychainView* ckks = self.ckks;
82 NSArray* pendingManifests = [CKKSPendingManifest all:&error];
83 NSUInteger greatestKnownManifestGeneration = [CKKSManifest greatestKnownGenerationCount];
84 for (CKKSPendingManifest* manifest in pendingManifests) {
85 if (manifest.generationCount >= greatestKnownManifestGeneration) {
86 [manifest commitToDatabaseWithError:&error];
89 // if this is an older generation, just get rid of it
90 [manifest deleteFromDatabase:&error];
94 if (![ckks _onQueueUpdateLatestManifestWithError:&error]) {
96 ckkserror("ckksfetch", ckks, "failed to get latest manifest");
100 - (void)_onqueueProcessRecordDeletions
102 CKKSKeychainView* ckks = self.ckks;
103 [self.deletions enumerateKeysAndObjectsUsingBlock:^(CKRecordID * _Nonnull recordID, NSString * _Nonnull recordType, BOOL * _Nonnull stop) {
104 ckksinfo("ckksfetch", ckks, "Processing record deletion(%@): %@", recordType, recordID);
106 // <rdar://problem/32475600> CKKS: Check Current Item pointers in the Manifest
107 // TODO: check that these deletions match a manifest upload
108 // Delegate these back up into the CKKS object for processing
109 [ckks _onqueueCKRecordDeleted:recordID recordType:recordType resync:self.resync];
113 - (void)_onqueueScanForExtraneousLocalItems
115 // TODO: must scan through all CKMirrorEntries and determine if any exist that CloudKit didn't tell us about
116 CKKSKeychainView* ckks = self.ckks;
117 NSError* error = nil;
119 ckksnotice("ckksresync", ckks, "Comparing local UUIDs against the CloudKit list");
120 NSMutableArray<NSString*>* uuids = [[CKKSMirrorEntry allUUIDs: &error] mutableCopy];
122 for(NSString* uuid in uuids) {
123 if([self.modifications objectForKey: [[CKRecordID alloc] initWithRecordName: uuid zoneID: ckks.zoneID]]) {
124 ckksdebug("ckksresync", ckks, "UUID %@ is still in CloudKit; carry on.", uuid);
126 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase: uuid zoneID:ckks.zoneID error: &error];
128 ckkserror("ckksresync", ckks, "Couldn't read an item from the database, but it used to be there: %@ %@", uuid, error);
133 ckkserror("ckksresync", ckks, "BUG: Local item %@ not found in CloudKit, deleting", uuid);
134 [ckks _onqueueCKRecordDeleted:ckme.item.storedCKRecord.recordID recordType:ckme.item.storedCKRecord.recordType resync:self.resync];
141 __weak __typeof(self) weakSelf = self;
144 CKKSKeychainView* ckks = self.ckks;
146 ckkserror("ckksresync", ckks, "no CKKS object");
150 [ckks dispatchSync: ^bool{
151 ckks.lastRecordZoneChangesOperation = self;
153 NSError* error = nil;
154 NSQualityOfService qos = NSQualityOfServiceUtility;
156 CKFetchRecordZoneChangesOptions* options = [[CKFetchRecordZoneChangesOptions alloc] init];
158 ckksnotice("ckksresync", ckks, "Beginning resync fetch!");
160 options.previousServerChangeToken = nil;
162 // currently, resyncs are user initiated (or the key hierarchy is upset, which is implicitly user initiated)
163 qos = NSQualityOfServiceUserInitiated;
165 // This is the normal case: fetch only the delta since the last fetch
166 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state: ckks.zoneName];
168 ckkserror("ckksfetch", ckks, "couldn't fetch zone status for %@: %@", ckks.zoneName, error);
173 ckksnotice("ckksfetch", ckks, "Beginning fetch(%@) starting at change token %@", ckks.zoneName, ckse.changeToken);
175 options.previousServerChangeToken = ckse.changeToken;
177 if(ckse.changeToken == nil) {
178 // First sync is special.
179 qos = NSQualityOfServiceUserInitiated;
183 self.fetchRecordZoneChangesOperation = [[ckks.fetchRecordZoneChangesOperationClass alloc] initWithRecordZoneIDs: @[ckks.zoneID] optionsByRecordZoneID:@{ckks.zoneID: options}];
185 self.fetchRecordZoneChangesOperation.fetchAllChanges = YES;
186 self.fetchRecordZoneChangesOperation.qualityOfService = qos;
187 self.fetchRecordZoneChangesOperation.group = self.ckoperationGroup;
188 ckksnotice("ckksfetch", ckks, "Operation group is %@", self.ckoperationGroup);
190 self.fetchRecordZoneChangesOperation.recordChangedBlock = ^(CKRecord *record) {
191 __strong __typeof(weakSelf) strongSelf = weakSelf;
192 __strong __typeof(strongSelf.ckks) strongCKKS = strongSelf.ckks;
194 ckkserror("ckksfetch", strongCKKS, "received callback for released object");
198 ckksinfo("ckksfetch", strongCKKS, "CloudKit notification: record changed(%@): %@", [record recordType], record);
200 // Add this to the modifications, and remove it from the deletions
201 [strongSelf.modifications setObject: record forKey: record.recordID];
202 [strongSelf.deletions removeObjectForKey: record.recordID];
205 self.fetchRecordZoneChangesOperation.recordWithIDWasDeletedBlock = ^(CKRecordID *recordID, NSString *recordType) {
206 __strong __typeof(weakSelf) strongSelf = weakSelf;
207 __strong __typeof(strongSelf.ckks) strongCKKS = strongSelf.ckks;
209 ckkserror("ckksfetch", strongCKKS, "received callback for released object");
213 ckksinfo("ckksfetch", strongCKKS, "CloudKit notification: deleted record(%@): %@", recordType, recordID);
215 // Add to the deletions, and remove any pending modifications
216 [strongSelf.modifications removeObjectForKey: recordID];
217 [strongSelf.deletions setObject: recordType forKey: recordID];
220 // This class only supports fetching from a single zone, so we can ignore recordZoneID
221 self.fetchRecordZoneChangesOperation.recordZoneChangeTokensUpdatedBlock = ^(CKRecordZoneID *recordZoneID, CKServerChangeToken *serverChangeToken, NSData *clientChangeTokenData) {
222 __strong __typeof(weakSelf) strongSelf = weakSelf;
223 __strong __typeof(strongSelf.ckks) strongCKKS = strongSelf.ckks;
225 ckkserror("ckksfetch", strongCKKS, "received callback for released object");
229 ckksinfo("ckksfetch", strongCKKS, "Received a new server change token: %@ %@", serverChangeToken, clientChangeTokenData);
230 strongSelf.serverChangeToken = serverChangeToken;
233 // Completion blocks don't count for dependencies. Use this intermediate operation hack instead.
234 NSBlockOperation* recordZoneChangesCompletedOperation = [[NSBlockOperation alloc] init];
235 recordZoneChangesCompletedOperation.name = @"record-zone-changes-completed";
237 self.fetchRecordZoneChangesOperation.recordZoneFetchCompletionBlock = ^(CKRecordZoneID *recordZoneID, CKServerChangeToken *serverChangeToken, NSData *clientChangeTokenData, BOOL moreComing, NSError * recordZoneError) {
238 __strong __typeof(weakSelf) strongSelf = weakSelf;
239 __strong __typeof(strongSelf.ckks) blockCKKS = strongSelf.ckks;
242 ckkserror("ckksfetch", blockCKKS, "received callback for released object");
247 ckkserror("ckksfetch", blockCKKS, "no CKKS object");
251 ckksnotice("ckksfetch", blockCKKS, "Record zone fetch complete: changeToken=%@ clientChangeTokenData=%@ changed=%lu deleted=%lu error=%@", serverChangeToken, clientChangeTokenData,
252 (unsigned long)strongSelf.deletions.count,
253 (unsigned long)strongSelf.deletions.count,
256 // Completion! Mark these down.
257 if(recordZoneError) {
258 strongSelf.error = recordZoneError;
260 strongSelf.serverChangeToken = serverChangeToken;
262 if(recordZoneError != nil) {
263 // An error occurred. All our fetches are useless. Skip to the end.
265 // Commit these changes!
266 __block NSError* error = nil;
268 NSMutableDictionary<NSString*, NSMutableArray*>* changedRecordsDict = [[NSMutableDictionary alloc] init];
270 [blockCKKS dispatchSyncWithAccountQueue:^bool{
271 // let's process records in a specific order by type
272 // 1. Manifest leaf records, without which the manifest master records are meaningless
273 // 2. Manifest master records, which will be used to validate incoming items
274 // 3. Intermediate key records
275 // 4. Current key records
278 [strongSelf.modifications enumerateKeysAndObjectsUsingBlock:^(CKRecordID* _Nonnull recordID, CKRecord* _Nonnull record, BOOL* stop) {
279 ckksinfo("ckksfetch", blockCKKS, "Sorting record modification %@: %@", recordID, record);
280 NSMutableArray* changedRecordsByType = changedRecordsDict[record.recordType];
281 if(!changedRecordsByType) {
282 changedRecordsByType = [[NSMutableArray alloc] init];
283 changedRecordsDict[record.recordType] = changedRecordsByType;
286 [changedRecordsByType addObject:record];
289 if ([CKKSManifest shouldSyncManifests]) {
290 if (!strongSelf.resync) {
291 [strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordManifestLeafType]];
292 [strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordManifestType]];
295 [strongSelf _updateLatestTrustedManifest];
298 [strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordIntermediateKeyType]];
299 [strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordCurrentKeyType]];
300 [strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordItemType]];
301 [strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordCurrentItemType]];
302 [strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordDeviceStateType]];
304 [strongSelf _onqueueProcessRecordDeletions];
305 [strongSelf _onqueueScanForExtraneousLocalItems];
307 CKKSZoneStateEntry* state = [CKKSZoneStateEntry state: blockCKKS.zoneName];
308 state.lastFetchTime = [NSDate date]; // The last fetch happened right now!
309 if(strongSelf.serverChangeToken) {
310 ckksdebug("ckksfetch", blockCKKS, "Zone change fetch complete: saving new server change token: %@", strongSelf.serverChangeToken);
311 state.changeToken = strongSelf.serverChangeToken;
313 [state saveToDatabase:&error];
315 ckkserror("ckksfetch", blockCKKS, "Couldn't save new server change token: %@", error);
316 strongSelf.error = error;
320 ckkserror("ckksfetch", blockCKKS, "horrible error occurred: %@", error);
321 strongSelf.error = error;
330 // Called with overall operation success. As I understand it, this block will be called for every operation.
331 // In the case of, e.g., network failure, the recordZoneFetchCompletionBlock will not be called, but this one will.
332 self.fetchRecordZoneChangesOperation.fetchRecordZoneChangesCompletionBlock = ^(NSError * _Nullable operationError) {
333 __strong __typeof(weakSelf) strongSelf = weakSelf;
334 __strong __typeof(strongSelf.ckks) strongCKKS = strongSelf.ckks;
336 ckkserror("ckksfetch", strongCKKS, "received callback for released object");
340 ckksnotice("ckksfetch", strongCKKS, "Record zone changes fetch complete: error=%@", operationError);
342 strongSelf.error = operationError;
345 // Trigger the fake 'we're done' operation.
346 [strongSelf runBeforeGroupFinished: recordZoneChangesCompletedOperation];
349 [self dependOnBeforeGroupFinished: recordZoneChangesCompletedOperation];
350 [self dependOnBeforeGroupFinished: self.fetchRecordZoneChangesOperation];
351 [ckks.database addOperation: self.fetchRecordZoneChangesOperation];
357 [self.fetchRecordZoneChangesOperation cancel];