]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSFetchAllRecordZoneChangesOperation.m
Security-58286.1.32.tar.gz
[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 #include <securityd/SecItemServer.h>
37
38
39 @interface CKKSFetchAllRecordZoneChangesOperation()
40 @property CKDatabaseOperation<CKKSFetchRecordZoneChangesOperation>* fetchRecordZoneChangesOperation;
41 @property CKOperationGroup* ckoperationGroup;
42 @end
43
44 @implementation CKKSFetchAllRecordZoneChangesOperation
45
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.
48
49 - (instancetype)init {
50 return nil;
51 }
52
53 - (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks ckoperationGroup:(CKOperationGroup*)ckoperationGroup {
54
55 if(self = [super init]) {
56 _ckks = ckks;
57 _ckoperationGroup = ckoperationGroup;
58 self.zoneID = ckks.zoneID;
59
60 self.resync = false;
61
62 self.modifications = [[NSMutableDictionary alloc] init];
63 self.deletions = [[NSMutableDictionary alloc] init];
64
65 // Can't fetch unless the zone is created.
66 [self addNullableDependency:ckks.viewSetupOperation];
67 }
68 return self;
69 }
70
71 - (void)_onqueueRecordsChanged:(NSArray*)records
72 {
73 for (CKRecord* record in records) {
74 [self.ckks _onqueueCKRecordChanged:record resync:self.resync];
75 }
76 }
77
78 - (void)_updateLatestTrustedManifest
79 {
80 CKKSKeychainView* ckks = self.ckks;
81 NSError* error = nil;
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];
87 }
88 else {
89 // if this is an older generation, just get rid of it
90 [manifest deleteFromDatabase:&error];
91 }
92 }
93
94 if (![ckks _onQueueUpdateLatestManifestWithError:&error]) {
95 self.error = error;
96 ckkserror("ckksfetch", ckks, "failed to get latest manifest");
97 }
98 }
99
100 - (void)_onqueueProcessRecordDeletions
101 {
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);
105
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];
110 }];
111 }
112
113 - (void)_onqueueScanForExtraneousLocalItems
114 {
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;
118 if(self.resync) {
119 ckksnotice("ckksresync", ckks, "Comparing local UUIDs against the CloudKit list");
120 NSMutableArray<NSString*>* uuids = [[CKKSMirrorEntry allUUIDs: &error] mutableCopy];
121
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);
125 } else {
126 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase: uuid zoneID:ckks.zoneID error: &error];
127 if(error != nil) {
128 ckkserror("ckksresync", ckks, "Couldn't read an item from the database, but it used to be there: %@ %@", uuid, error);
129 self.error = error;
130 continue;
131 }
132
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];
135 }
136 }
137 }
138 }
139
140 - (void)groupStart {
141 __weak __typeof(self) weakSelf = self;
142
143
144 CKKSKeychainView* ckks = self.ckks;
145 if(!ckks) {
146 ckkserror("ckksresync", ckks, "no CKKS object");
147 return;
148 }
149
150 [ckks dispatchSync: ^bool{
151 ckks.lastRecordZoneChangesOperation = self;
152
153 NSError* error = nil;
154 NSQualityOfService qos = NSQualityOfServiceUtility;
155
156 CKFetchRecordZoneChangesOptions* options = [[CKFetchRecordZoneChangesOptions alloc] init];
157 if(self.resync) {
158 ckksnotice("ckksresync", ckks, "Beginning resync fetch!");
159
160 options.previousServerChangeToken = nil;
161
162 // currently, resyncs are user initiated (or the key hierarchy is upset, which is implicitly user initiated)
163 qos = NSQualityOfServiceUserInitiated;
164 } else {
165 // This is the normal case: fetch only the delta since the last fetch
166 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state: ckks.zoneName];
167 if(error || !ckse) {
168 ckkserror("ckksfetch", ckks, "couldn't fetch zone status for %@: %@", ckks.zoneName, error);
169 self.error = error;
170 return false;
171 }
172
173 ckksnotice("ckksfetch", ckks, "Beginning fetch(%@) starting at change token %@", ckks.zoneName, ckse.changeToken);
174
175 options.previousServerChangeToken = ckse.changeToken;
176
177 if(ckse.changeToken == nil) {
178 // First sync is special.
179 qos = NSQualityOfServiceUserInitiated;
180 }
181 }
182
183 self.fetchRecordZoneChangesOperation = [[ckks.fetchRecordZoneChangesOperationClass alloc] initWithRecordZoneIDs: @[ckks.zoneID] optionsByRecordZoneID:@{ckks.zoneID: options}];
184
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);
189
190 self.fetchRecordZoneChangesOperation.recordChangedBlock = ^(CKRecord *record) {
191 __strong __typeof(weakSelf) strongSelf = weakSelf;
192 __strong __typeof(strongSelf.ckks) strongCKKS = strongSelf.ckks;
193 if(!strongSelf) {
194 ckkserror("ckksfetch", strongCKKS, "received callback for released object");
195 return;
196 }
197
198 ckksinfo("ckksfetch", strongCKKS, "CloudKit notification: record changed(%@): %@", [record recordType], record);
199
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];
203 };
204
205 self.fetchRecordZoneChangesOperation.recordWithIDWasDeletedBlock = ^(CKRecordID *recordID, NSString *recordType) {
206 __strong __typeof(weakSelf) strongSelf = weakSelf;
207 __strong __typeof(strongSelf.ckks) strongCKKS = strongSelf.ckks;
208 if(!strongSelf) {
209 ckkserror("ckksfetch", strongCKKS, "received callback for released object");
210 return;
211 }
212
213 ckksinfo("ckksfetch", strongCKKS, "CloudKit notification: deleted record(%@): %@", recordType, recordID);
214
215 // Add to the deletions, and remove any pending modifications
216 [strongSelf.modifications removeObjectForKey: recordID];
217 [strongSelf.deletions setObject: recordType forKey: recordID];
218 };
219
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;
224 if(!strongSelf) {
225 ckkserror("ckksfetch", strongCKKS, "received callback for released object");
226 return;
227 }
228
229 ckksinfo("ckksfetch", strongCKKS, "Received a new server change token: %@ %@", serverChangeToken, clientChangeTokenData);
230 strongSelf.serverChangeToken = serverChangeToken;
231 };
232
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";
236
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;
240
241 if(!strongSelf) {
242 ckkserror("ckksfetch", blockCKKS, "received callback for released object");
243 return;
244 }
245
246 if(!blockCKKS) {
247 ckkserror("ckksfetch", blockCKKS, "no CKKS object");
248 return;
249 }
250
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,
254 recordZoneError);
255
256 // Completion! Mark these down.
257 if(recordZoneError) {
258 strongSelf.error = recordZoneError;
259 }
260 strongSelf.serverChangeToken = serverChangeToken;
261
262 if(recordZoneError != nil) {
263 // An error occurred. All our fetches are useless. Skip to the end.
264 } else {
265 // Commit these changes!
266 __block NSError* error = nil;
267
268 NSMutableDictionary<NSString*, NSMutableArray*>* changedRecordsDict = [[NSMutableDictionary alloc] init];
269
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
276 // 5. Item records
277
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;
284 };
285
286 [changedRecordsByType addObject:record];
287 }];
288
289 if ([CKKSManifest shouldSyncManifests]) {
290 if (!strongSelf.resync) {
291 [strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordManifestLeafType]];
292 [strongSelf _onqueueRecordsChanged:changedRecordsDict[SecCKRecordManifestType]];
293 }
294
295 [strongSelf _updateLatestTrustedManifest];
296 }
297
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]];
303
304 [strongSelf _onqueueProcessRecordDeletions];
305 [strongSelf _onqueueScanForExtraneousLocalItems];
306
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;
312 }
313 [state saveToDatabase:&error];
314 if(error) {
315 ckkserror("ckksfetch", blockCKKS, "Couldn't save new server change token: %@", error);
316 strongSelf.error = error;
317 }
318
319 if(error) {
320 ckkserror("ckksfetch", blockCKKS, "horrible error occurred: %@", error);
321 strongSelf.error = error;
322 return false;
323 }
324
325 return true;
326 }];
327 }
328 };
329
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;
335 if(!strongSelf) {
336 ckkserror("ckksfetch", strongCKKS, "received callback for released object");
337 return;
338 }
339
340 ckksnotice("ckksfetch", strongCKKS, "Record zone changes fetch complete: error=%@", operationError);
341 if(operationError) {
342 strongSelf.error = operationError;
343 }
344
345 // Trigger the fake 'we're done' operation.
346 [strongSelf runBeforeGroupFinished: recordZoneChangesCompletedOperation];
347 };
348
349 [self dependOnBeforeGroupFinished: recordZoneChangesCompletedOperation];
350 [self dependOnBeforeGroupFinished: self.fetchRecordZoneChangesOperation];
351 [ckks.database addOperation: self.fetchRecordZoneChangesOperation];
352 return true;
353 }];
354 }
355
356 - (void)cancel {
357 [self.fetchRecordZoneChangesOperation cancel];
358 [super cancel];
359 }
360
361 @end
362
363 #endif