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 #import "CKKSPowerCollection.h"
37 #include <securityd/SecItemServer.h>
40 @interface CKKSFetchAllRecordZoneChangesOperation()
41 @property CKDatabaseOperation<CKKSFetchRecordZoneChangesOperation>* fetchRecordZoneChangesOperation;
42 @property CKOperationGroup* ckoperationGroup;
43 @property (assign) NSUInteger fetchedItems;
46 @implementation CKKSFetchAllRecordZoneChangesOperation
48 // Sets up an operation to fetch all changes from the server, and collect them until we know if the fetch completes.
49 // As a bonus, you can depend on this operation without worry about NSOperation completion block dependency issues.
51 - (instancetype)init {
55 - (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks
56 fetchReasons:(NSSet<CKKSFetchBecause*>*)fetchReasons
57 ckoperationGroup:(CKOperationGroup*)ckoperationGroup {
59 if(self = [super init]) {
61 _ckoperationGroup = ckoperationGroup;
62 _fetchReasons = fetchReasons;
63 _zoneID = ckks.zoneID;
67 _modifications = [[NSMutableDictionary alloc] init];
68 _deletions = [[NSMutableDictionary alloc] init];
70 // Can't fetch unless the zone is created.
71 [self addNullableDependency:ckks.zoneSetupOperation];
76 - (void)_onqueueRecordsChanged:(NSArray*)records
78 for (CKRecord* record in records) {
79 [self.ckks _onqueueCKRecordChanged:record resync:self.resync];
83 - (void)_updateLatestTrustedManifest
85 CKKSKeychainView* ckks = self.ckks;
87 NSArray* pendingManifests = [CKKSPendingManifest all:&error];
88 NSUInteger greatestKnownManifestGeneration = [CKKSManifest greatestKnownGenerationCount];
89 for (CKKSPendingManifest* manifest in pendingManifests) {
90 if (manifest.generationCount >= greatestKnownManifestGeneration) {
91 [manifest commitToDatabaseWithError:&error];
94 // if this is an older generation, just get rid of it
95 [manifest deleteFromDatabase:&error];
99 if (![ckks _onqueueUpdateLatestManifestWithError:&error]) {
101 ckkserror("ckksfetch", ckks, "failed to get latest manifest");
105 - (void)_onqueueProcessRecordDeletions
107 CKKSKeychainView* ckks = self.ckks;
108 [self.deletions enumerateKeysAndObjectsUsingBlock:^(CKRecordID * _Nonnull recordID, NSString * _Nonnull recordType, BOOL * _Nonnull stop) {
109 ckksinfo("ckksfetch", ckks, "Processing record deletion(%@): %@", recordType, recordID);
111 // <rdar://problem/32475600> CKKS: Check Current Item pointers in the Manifest
112 // TODO: check that these deletions match a manifest upload
113 // Delegate these back up into the CKKS object for processing
114 [ckks _onqueueCKRecordDeleted:recordID recordType:recordType resync:self.resync];
118 - (void)_onqueueScanForExtraneousLocalItems
120 // TODO: must scan through all CKMirrorEntries and determine if any exist that CloudKit didn't tell us about
121 CKKSKeychainView* ckks = self.ckks;
122 NSError* error = nil;
124 ckksnotice("ckksresync", ckks, "Comparing local UUIDs against the CloudKit list");
125 NSMutableArray<NSString*>* uuids = [[CKKSMirrorEntry allUUIDs:ckks.zoneID error:&error] mutableCopy];
127 for(NSString* uuid in uuids) {
128 if([self.modifications objectForKey: [[CKRecordID alloc] initWithRecordName: uuid zoneID: ckks.zoneID]]) {
129 ckksnotice("ckksresync", ckks, "UUID %@ is still in CloudKit; carry on.", uuid);
131 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase:uuid zoneID:ckks.zoneID error: &error];
133 ckkserror("ckksresync", ckks, "Couldn't read an item from the database, but it used to be there: %@ %@", uuid, error);
138 ckkserror("ckksresync", ckks, "Couldn't read ckme(%@) from database; continuing", uuid);
142 ckkserror("ckksresync", ckks, "BUG: Local item %@ not found in CloudKit, deleting", uuid);
143 [ckks _onqueueCKRecordDeleted:ckme.item.storedCKRecord.recordID recordType:ckme.item.storedCKRecord.recordType resync:self.resync];
150 __weak __typeof(self) weakSelf = self;
152 CKKSKeychainView* ckks = self.ckks;
154 ckkserror("ckksresync", ckks, "no CKKS object");
158 [ckks dispatchSync: ^bool{
159 ckks.lastRecordZoneChangesOperation = self;
161 NSError* error = nil;
162 NSQualityOfService qos = NSQualityOfServiceUtility;
164 CKFetchRecordZoneChangesOptions* options = [[CKFetchRecordZoneChangesOptions alloc] init];
166 ckksnotice("ckksresync", ckks, "Beginning resync fetch!");
168 options.previousServerChangeToken = nil;
170 // currently, resyncs are user initiated (or the key hierarchy is upset, which is implicitly user initiated)
171 qos = NSQualityOfServiceUserInitiated;
173 // This is the normal case: fetch only the delta since the last fetch
174 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state: ckks.zoneName];
176 ckkserror("ckksfetch", ckks, "couldn't fetch zone status for %@: %@", ckks.zoneName, error);
181 // If this is the first sync, or an API fetch, use QoS userInitated
182 if(ckse.changeToken == nil || [self.fetchReasons containsObject:CKKSFetchBecauseAPIFetchRequest]) {
183 qos = NSQualityOfServiceUserInitiated;
186 options.previousServerChangeToken = ckse.changeToken;
189 ckksnotice("ckksfetch", ckks, "Beginning fetch(%@) starting at change token %@ with QoS %d", ckks.zoneName, options.previousServerChangeToken, (int)qos);
191 self.fetchRecordZoneChangesOperation = [[ckks.fetchRecordZoneChangesOperationClass alloc] initWithRecordZoneIDs: @[ckks.zoneID] optionsByRecordZoneID:@{ckks.zoneID: options}];
193 self.fetchRecordZoneChangesOperation.fetchAllChanges = YES;
194 self.fetchRecordZoneChangesOperation.qualityOfService = qos;
195 self.fetchRecordZoneChangesOperation.group = self.ckoperationGroup;
196 ckksnotice("ckksfetch", ckks, "Operation group is %@", self.ckoperationGroup);
198 self.fetchRecordZoneChangesOperation.recordChangedBlock = ^(CKRecord *record) {
199 __strong __typeof(weakSelf) strongSelf = weakSelf;
200 __strong __typeof(strongSelf.ckks) strongCKKS = strongSelf.ckks;
202 ckkserror("ckksfetch", strongCKKS, "received callback for released object");
206 ckksinfo("ckksfetch", strongCKKS, "CloudKit notification: record changed(%@): %@", [record recordType], record);
208 // Add this to the modifications, and remove it from the deletions
209 [strongSelf.modifications setObject: record forKey: record.recordID];
210 [strongSelf.deletions removeObjectForKey: record.recordID];
211 strongSelf.fetchedItems++;
214 self.fetchRecordZoneChangesOperation.recordWithIDWasDeletedBlock = ^(CKRecordID *recordID, NSString *recordType) {
215 __strong __typeof(weakSelf) strongSelf = weakSelf;
216 __strong __typeof(strongSelf.ckks) strongCKKS = strongSelf.ckks;
218 ckkserror("ckksfetch", strongCKKS, "received callback for released object");
222 ckksinfo("ckksfetch", strongCKKS, "CloudKit notification: deleted record(%@): %@", recordType, recordID);
224 // Add to the deletions, and remove any pending modifications
225 [strongSelf.modifications removeObjectForKey: recordID];
226 [strongSelf.deletions setObject: recordType forKey: recordID];
227 strongSelf.fetchedItems++;
230 // This class only supports fetching from a single zone, so we can ignore recordZoneID
231 self.fetchRecordZoneChangesOperation.recordZoneChangeTokensUpdatedBlock = ^(CKRecordZoneID *recordZoneID, CKServerChangeToken *serverChangeToken, NSData *clientChangeTokenData) {
232 __strong __typeof(weakSelf) strongSelf = weakSelf;
233 __strong __typeof(strongSelf.ckks) strongCKKS = strongSelf.ckks;
235 ckkserror("ckksfetch", strongCKKS, "received callback for released object");
239 ckksinfo("ckksfetch", strongCKKS, "Received a new server change token: %@ %@", serverChangeToken, clientChangeTokenData);
240 strongSelf.serverChangeToken = serverChangeToken;
243 // Completion blocks don't count for dependencies. Use this intermediate operation hack instead.
244 NSBlockOperation* recordZoneChangesCompletedOperation = [[NSBlockOperation alloc] init];
245 recordZoneChangesCompletedOperation.name = @"record-zone-changes-completed";
247 self.fetchRecordZoneChangesOperation.recordZoneFetchCompletionBlock = ^(CKRecordZoneID *recordZoneID, CKServerChangeToken *serverChangeToken, NSData *clientChangeTokenData, BOOL moreComing, NSError * recordZoneError) {
248 __strong __typeof(weakSelf) strongSelf = weakSelf;
249 __strong __typeof(strongSelf.ckks) blockCKKS = strongSelf.ckks;
252 ckkserror("ckksfetch", blockCKKS, "received callback for released object");
257 ckkserror("ckksfetch", blockCKKS, "no CKKS object");
261 ckksnotice("ckksfetch", blockCKKS, "Record zone fetch complete: changeToken=%@ clientChangeTokenData=%@ changed=%lu deleted=%lu error=%@", serverChangeToken, clientChangeTokenData,
262 (unsigned long)strongSelf.modifications.count,
263 (unsigned long)strongSelf.deletions.count,
266 // Completion! Mark these down.
267 if(recordZoneError) {
268 strongSelf.error = recordZoneError;
270 strongSelf.serverChangeToken = serverChangeToken;
272 if(recordZoneError != nil) {
273 // An error occurred. All our fetches are useless. Skip to the end.
275 // Commit these changes!
276 __block NSError* error = nil;
278 NSMutableDictionary<NSString*, NSMutableArray*>* changedRecordsDict = [[NSMutableDictionary alloc] init];
280 [blockCKKS dispatchSyncWithAccountKeys:^bool{
281 // let's process records in a specific order by type
282 // 1. Manifest leaf records, without which the manifest master records are meaningless
283 // 2. Manifest master records, which will be used to validate incoming items
284 // 3. Intermediate key records
285 // 4. Current key records
288 [strongSelf.modifications enumerateKeysAndObjectsUsingBlock:^(CKRecordID* _Nonnull recordID, CKRecord* _Nonnull record, BOOL* stop) {
289 ckksinfo("ckksfetch", blockCKKS, "Sorting record modification %@: %@", recordID, record);
290 NSMutableArray* changedRecordsByType = changedRecordsDict[record.recordType];
291 if(!changedRecordsByType) {
292 changedRecordsByType = [[NSMutableArray alloc] init];
293 changedRecordsDict[record.recordType] = changedRecordsByType;
296 [changedRecordsByType addObject:record];
299 if ([CKKSManifest shouldSyncManifests]) {
300 if (!strongSelf.resync) {
301 [strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordManifestLeafType]];
302 [strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordManifestType]];
305 [strongSelf _updateLatestTrustedManifest];
308 [strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordIntermediateKeyType]];
309 [strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordCurrentKeyType]];
310 [strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordItemType]];
311 [strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordCurrentItemType]];
312 [strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordDeviceStateType]];
313 [strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordTLKShareType]];
315 [strongSelf _onqueueProcessRecordDeletions];
316 [strongSelf _onqueueScanForExtraneousLocalItems];
318 CKKSZoneStateEntry* state = [CKKSZoneStateEntry state: blockCKKS.zoneName];
319 state.lastFetchTime = [NSDate date]; // The last fetch happened right now!
320 if(strongSelf.serverChangeToken) {
321 ckksdebug("ckksfetch", blockCKKS, "Zone change fetch complete: saving new server change token: %@", strongSelf.serverChangeToken);
322 state.changeToken = strongSelf.serverChangeToken;
324 [state saveToDatabase:&error];
326 ckkserror("ckksfetch", blockCKKS, "Couldn't save new server change token: %@", error);
327 strongSelf.error = error;
331 ckkserror("ckksfetch", blockCKKS, "horrible error occurred: %@", error);
332 strongSelf.error = error;
336 ckksnotice("ckksfetch", blockCKKS, "Finished processing fetch for %@", recordZoneID);
343 // Called with overall operation success. As I understand it, this block will be called for every operation.
344 // In the case of, e.g., network failure, the recordZoneFetchCompletionBlock will not be called, but this one will.
345 self.fetchRecordZoneChangesOperation.fetchRecordZoneChangesCompletionBlock = ^(NSError * _Nullable operationError) {
346 __strong __typeof(weakSelf) strongSelf = weakSelf;
347 __strong __typeof(strongSelf.ckks) strongCKKS = strongSelf.ckks;
349 ckkserror("ckksfetch", strongCKKS, "received callback for released object");
353 ckksnotice("ckksfetch", strongCKKS, "Record zone changes fetch complete: error=%@", operationError);
355 strongSelf.error = operationError;
358 [CKKSPowerCollection CKKSPowerEvent:kCKKSPowerEventFetchAllChanges zone:ckks.zoneName count:strongSelf.fetchedItems];
361 // Trigger the fake 'we're done' operation.
362 [strongSelf runBeforeGroupFinished: recordZoneChangesCompletedOperation];
365 [self dependOnBeforeGroupFinished: recordZoneChangesCompletedOperation];
366 [self dependOnBeforeGroupFinished: self.fetchRecordZoneChangesOperation];
367 [ckks.database addOperation: self.fetchRecordZoneChangesOperation];
373 [self.fetchRecordZoneChangesOperation cancel];