]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSFetchAllRecordZoneChangesOperation.m
a2eb44d1c2c8cc811b559d8564ac13999b82a0ea
[apple/security.git] / keychain / ckks / CKKSFetchAllRecordZoneChangesOperation.m
1 /*
2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
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
11 * file.
12 *
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.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 #import <Foundation/Foundation.h>
25
26 #if OCTAGON
27
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>
38
39
40 @interface CKKSFetchAllRecordZoneChangesOperation()
41 @property CKDatabaseOperation<CKKSFetchRecordZoneChangesOperation>* fetchRecordZoneChangesOperation;
42 @property CKOperationGroup* ckoperationGroup;
43 @property (assign) NSUInteger fetchedItems;
44 @end
45
46 @implementation CKKSFetchAllRecordZoneChangesOperation
47
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.
50
51 - (instancetype)init {
52 return nil;
53 }
54
55 - (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks
56 fetchReasons:(NSSet<CKKSFetchBecause*>*)fetchReasons
57 ckoperationGroup:(CKOperationGroup*)ckoperationGroup {
58
59 if(self = [super init]) {
60 _ckks = ckks;
61 _ckoperationGroup = ckoperationGroup;
62 _fetchReasons = fetchReasons;
63 _zoneID = ckks.zoneID;
64
65 _resync = false;
66
67 _modifications = [[NSMutableDictionary alloc] init];
68 _deletions = [[NSMutableDictionary alloc] init];
69
70 // Can't fetch unless the zone is created.
71 [self addNullableDependency:ckks.zoneSetupOperation];
72 }
73 return self;
74 }
75
76 - (void)_onqueueRecordsChanged:(NSArray*)records
77 {
78 for (CKRecord* record in records) {
79 [self.ckks _onqueueCKRecordChanged:record resync:self.resync];
80 }
81 }
82
83 - (void)_updateLatestTrustedManifest
84 {
85 CKKSKeychainView* ckks = self.ckks;
86 NSError* error = nil;
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];
92 }
93 else {
94 // if this is an older generation, just get rid of it
95 [manifest deleteFromDatabase:&error];
96 }
97 }
98
99 if (![ckks _onqueueUpdateLatestManifestWithError:&error]) {
100 self.error = error;
101 ckkserror("ckksfetch", ckks, "failed to get latest manifest");
102 }
103 }
104
105 - (void)_onqueueProcessRecordDeletions
106 {
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);
110
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];
115 }];
116 }
117
118 - (void)_onqueueScanForExtraneousLocalItems
119 {
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;
123 if(self.resync) {
124 ckksnotice("ckksresync", ckks, "Comparing local UUIDs against the CloudKit list");
125 NSMutableArray<NSString*>* uuids = [[CKKSMirrorEntry allUUIDs:ckks.zoneID error:&error] mutableCopy];
126
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);
130 } else {
131 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase:uuid zoneID:ckks.zoneID error: &error];
132 if(error != nil) {
133 ckkserror("ckksresync", ckks, "Couldn't read an item from the database, but it used to be there: %@ %@", uuid, error);
134 self.error = error;
135 continue;
136 }
137 if(!ckme) {
138 ckkserror("ckksresync", ckks, "Couldn't read ckme(%@) from database; continuing", uuid);
139 continue;
140 }
141
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];
144 }
145 }
146 }
147 }
148
149 - (void)groupStart {
150 __weak __typeof(self) weakSelf = self;
151
152 CKKSKeychainView* ckks = self.ckks;
153 if(!ckks) {
154 ckkserror("ckksresync", ckks, "no CKKS object");
155 return;
156 }
157
158 [ckks dispatchSync: ^bool{
159 ckks.lastRecordZoneChangesOperation = self;
160
161 NSError* error = nil;
162 NSQualityOfService qos = NSQualityOfServiceUtility;
163
164 CKFetchRecordZoneChangesOptions* options = [[CKFetchRecordZoneChangesOptions alloc] init];
165 if(self.resync) {
166 ckksnotice("ckksresync", ckks, "Beginning resync fetch!");
167
168 options.previousServerChangeToken = nil;
169
170 // currently, resyncs are user initiated (or the key hierarchy is upset, which is implicitly user initiated)
171 qos = NSQualityOfServiceUserInitiated;
172 } else {
173 // This is the normal case: fetch only the delta since the last fetch
174 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state: ckks.zoneName];
175 if(error || !ckse) {
176 ckkserror("ckksfetch", ckks, "couldn't fetch zone status for %@: %@", ckks.zoneName, error);
177 self.error = error;
178 return false;
179 }
180
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;
184 }
185
186 options.previousServerChangeToken = ckse.changeToken;
187 }
188
189 ckksnotice("ckksfetch", ckks, "Beginning fetch(%@) starting at change token %@ with QoS %d", ckks.zoneName, options.previousServerChangeToken, (int)qos);
190
191 self.fetchRecordZoneChangesOperation = [[ckks.fetchRecordZoneChangesOperationClass alloc] initWithRecordZoneIDs: @[ckks.zoneID] optionsByRecordZoneID:@{ckks.zoneID: options}];
192
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);
197
198 self.fetchRecordZoneChangesOperation.recordChangedBlock = ^(CKRecord *record) {
199 __strong __typeof(weakSelf) strongSelf = weakSelf;
200 __strong __typeof(strongSelf.ckks) strongCKKS = strongSelf.ckks;
201 if(!strongSelf) {
202 ckkserror("ckksfetch", strongCKKS, "received callback for released object");
203 return;
204 }
205
206 ckksinfo("ckksfetch", strongCKKS, "CloudKit notification: record changed(%@): %@", [record recordType], record);
207
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++;
212 };
213
214 self.fetchRecordZoneChangesOperation.recordWithIDWasDeletedBlock = ^(CKRecordID *recordID, NSString *recordType) {
215 __strong __typeof(weakSelf) strongSelf = weakSelf;
216 __strong __typeof(strongSelf.ckks) strongCKKS = strongSelf.ckks;
217 if(!strongSelf) {
218 ckkserror("ckksfetch", strongCKKS, "received callback for released object");
219 return;
220 }
221
222 ckksinfo("ckksfetch", strongCKKS, "CloudKit notification: deleted record(%@): %@", recordType, recordID);
223
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++;
228 };
229
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;
234 if(!strongSelf) {
235 ckkserror("ckksfetch", strongCKKS, "received callback for released object");
236 return;
237 }
238
239 ckksinfo("ckksfetch", strongCKKS, "Received a new server change token: %@ %@", serverChangeToken, clientChangeTokenData);
240 strongSelf.serverChangeToken = serverChangeToken;
241 };
242
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";
246
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;
250
251 if(!strongSelf) {
252 ckkserror("ckksfetch", blockCKKS, "received callback for released object");
253 return;
254 }
255
256 if(!blockCKKS) {
257 ckkserror("ckksfetch", blockCKKS, "no CKKS object");
258 return;
259 }
260
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,
264 recordZoneError);
265
266 // Completion! Mark these down.
267 if(recordZoneError) {
268 strongSelf.error = recordZoneError;
269 }
270 strongSelf.serverChangeToken = serverChangeToken;
271
272 if(recordZoneError != nil) {
273 // An error occurred. All our fetches are useless. Skip to the end.
274 } else {
275 // Commit these changes!
276 __block NSError* error = nil;
277
278 NSMutableDictionary<NSString*, NSMutableArray*>* changedRecordsDict = [[NSMutableDictionary alloc] init];
279
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
286 // 5. Item records
287
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;
294 };
295
296 [changedRecordsByType addObject:record];
297 }];
298
299 if ([CKKSManifest shouldSyncManifests]) {
300 if (!strongSelf.resync) {
301 [strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordManifestLeafType]];
302 [strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordManifestType]];
303 }
304
305 [strongSelf _updateLatestTrustedManifest];
306 }
307
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]];
314
315 [strongSelf _onqueueProcessRecordDeletions];
316 [strongSelf _onqueueScanForExtraneousLocalItems];
317
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;
323 }
324 [state saveToDatabase:&error];
325 if(error) {
326 ckkserror("ckksfetch", blockCKKS, "Couldn't save new server change token: %@", error);
327 strongSelf.error = error;
328 }
329
330 if(error) {
331 ckkserror("ckksfetch", blockCKKS, "horrible error occurred: %@", error);
332 strongSelf.error = error;
333 return false;
334 }
335
336 ckksnotice("ckksfetch", blockCKKS, "Finished processing fetch for %@", recordZoneID);
337
338 return true;
339 }];
340 }
341 };
342
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;
348 if(!strongSelf) {
349 ckkserror("ckksfetch", strongCKKS, "received callback for released object");
350 return;
351 }
352
353 ckksnotice("ckksfetch", strongCKKS, "Record zone changes fetch complete: error=%@", operationError);
354 if(operationError) {
355 strongSelf.error = operationError;
356 }
357
358 [CKKSPowerCollection CKKSPowerEvent:kCKKSPowerEventFetchAllChanges zone:ckks.zoneName count:strongSelf.fetchedItems];
359
360
361 // Trigger the fake 'we're done' operation.
362 [strongSelf runBeforeGroupFinished: recordZoneChangesCompletedOperation];
363 };
364
365 [self dependOnBeforeGroupFinished: recordZoneChangesCompletedOperation];
366 [self dependOnBeforeGroupFinished: self.fetchRecordZoneChangesOperation];
367 [ckks.database addOperation: self.fetchRecordZoneChangesOperation];
368 return true;
369 }];
370 }
371
372 - (void)cancel {
373 [self.fetchRecordZoneChangesOperation cancel];
374 [super cancel];
375 }
376
377 @end
378
379 #endif