]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSFetchAllRecordZoneChangesOperation.m
Security-59306.140.5.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 <CloudKit/CloudKit.h>
29 #import <CloudKit/CloudKit_Private.h>
30
31 #import "keychain/categories/NSError+UsefulConstructors.h"
32 #import "keychain/ckks/CloudKitDependencies.h"
33 #import "keychain/ckks/CloudKitCategories.h"
34 #import "keychain/ckks/CKKS.h"
35 #import "keychain/ckks/CKKSKeychainView.h"
36 #import "keychain/ckks/CKKSReachabilityTracker.h"
37 #import "keychain/ot/ObjCImprovements.h"
38 #import "keychain/analytics/SecEventMetric.h"
39 #import "keychain/analytics/SecMetrics.h"
40 #import "NSError+UsefulConstructors.h"
41 #import "CKKSPowerCollection.h"
42 #include "keychain/securityd/SecItemServer.h"
43
44 @implementation CKKSCloudKitFetchRequest
45 @end
46
47 @implementation CKKSCloudKitDeletion
48 - (instancetype)initWithRecordID:(CKRecordID*)recordID recordType:(NSString*)recordType
49 {
50 if((self = [super init])) {
51 _recordID = recordID;
52 _recordType = recordType;
53 }
54 return self;
55 }
56 @end
57
58
59 @interface CKKSFetchAllRecordZoneChangesOperation()
60 @property CKDatabaseOperation<CKKSFetchRecordZoneChangesOperation>* fetchRecordZoneChangesOperation;
61 @property NSMutableDictionary<CKRecordZoneID*, CKFetchRecordZoneChangesConfiguration*>* allClientOptions;
62
63 @property NSMutableDictionary<CKRecordZoneID*, id<CKKSChangeFetcherClient>>* clientMap;
64
65 @property CKOperationGroup* ckoperationGroup;
66 @property (assign) NSUInteger fetchedItems;
67 @property bool forceResync;
68
69 @property bool moreComing;
70
71 @property size_t totalModifications;
72 @property size_t totalDeletions;
73
74 // A zoneID is in this set if we're attempting to resync them
75 @property NSMutableSet<CKRecordZoneID*>* resyncingZones;
76
77 @property CKKSResultOperation* fetchCompletedOperation;
78 @end
79
80 @implementation CKKSFetchAllRecordZoneChangesOperation
81
82 // Sets up an operation to fetch all changes from the server, and collect them until we know if the fetch completes.
83 // As a bonus, you can depend on this operation without worry about NSOperation completion block dependency issues.
84
85 - (instancetype)init {
86 return nil;
87 }
88
89 - (instancetype)initWithContainer:(CKContainer*)container
90 fetchClass:(Class<CKKSFetchRecordZoneChangesOperation>)fetchRecordZoneChangesOperationClass
91 clients:(NSArray<id<CKKSChangeFetcherClient>>*)clients
92 fetchReasons:(NSSet<CKKSFetchBecause*>*)fetchReasons
93 apnsPushes:(NSSet<CKRecordZoneNotification*>*)apnsPushes
94 forceResync:(bool)forceResync
95 ckoperationGroup:(CKOperationGroup*)ckoperationGroup
96 {
97 if(self = [super init]) {
98 _container = container;
99 _fetchRecordZoneChangesOperationClass = fetchRecordZoneChangesOperationClass;
100
101 _clientMap = [NSMutableDictionary dictionary];
102 for(id<CKKSChangeFetcherClient> client in clients) {
103 _clientMap[client.zoneID] = client;
104 }
105
106 _ckoperationGroup = ckoperationGroup;
107 _forceResync = forceResync;
108 _fetchReasons = fetchReasons;
109 _apnsPushes = apnsPushes;
110
111 _modifications = [[NSMutableDictionary alloc] init];
112 _deletions = [[NSMutableDictionary alloc] init];
113 _changeTokens = [[NSMutableDictionary alloc] init];
114
115 _resyncingZones = [NSMutableSet set];
116
117 _totalModifications = 0;
118 _totalDeletions = 0;
119
120 _fetchCompletedOperation = [CKKSResultOperation named:@"record-zone-changes-completed" withBlock:^{}];
121
122 _moreComing = false;
123 }
124 return self;
125 }
126
127 - (void)queryClientsForChangeTokens
128 {
129 // Ask all our clients for their change tags
130
131 // Unused until [<rdar://problem/38725728> Changes to discretionary-ness (explicit or derived from QoS) should be "live"] has happened and we can determine network
132 // discretionaryness.
133 //bool nilChangeTag = false;
134
135 for(CKRecordZoneID* clientZoneID in self.clientMap) {
136 id<CKKSChangeFetcherClient> client = self.clientMap[clientZoneID];
137
138 CKKSCloudKitFetchRequest* clientPreference = [client participateInFetch];
139 if(clientPreference.participateInFetch) {
140 [self.fetchedZoneIDs addObject:client.zoneID];
141
142 CKFetchRecordZoneChangesConfiguration* options = [[CKFetchRecordZoneChangesConfiguration alloc] init];
143
144 if(!self.forceResync) {
145 if (self.changeTokens[clientZoneID]) {
146 options.previousServerChangeToken = self.changeTokens[clientZoneID];
147 secnotice("ckksfetch", "Using cached change token for %@: %@", clientZoneID, self.changeTokens[clientZoneID]);
148 } else {
149 options.previousServerChangeToken = clientPreference.changeToken;
150 }
151 }
152
153 if(clientPreference.resync || self.forceResync) {
154 [self.resyncingZones addObject:clientZoneID];
155 }
156
157 self.allClientOptions[client.zoneID] = options;
158 }
159 }
160 }
161
162 - (void)groupStart {
163 self.allClientOptions = [NSMutableDictionary dictionary];
164 self.fetchedZoneIDs = [NSMutableArray array];
165
166 [self queryClientsForChangeTokens];
167
168 if(self.fetchedZoneIDs.count == 0) {
169 // No clients actually want to fetch right now, so quit
170 self.error = [NSError errorWithDomain:CKKSErrorDomain code:CKKSNoFetchesRequested description:@"No clients want a fetch right now"];
171 secnotice("ckksfetch", "Cancelling fetch: %@", self.error);
172 return;
173 }
174
175 [self performFetch];
176 }
177
178 - (void)performFetch
179 {
180 WEAKIFY(self);
181
182 // Compute the network discretionary approach this fetch will take.
183 // For now, everything is nondiscretionary, because we can't afford to block a nondiscretionary request later.
184 // Once [<rdar://problem/38725728> Changes to discretionary-ness (explicit or derived from QoS) should be "live"] happens, we can come back through and make things
185 // discretionary, but boost them later.
186 //
187 // Rules:
188 // If there's a nil change tag, go to nondiscretionary. This is likely a zone bringup (which happens during iCloud sign-in) or a resync (which happens due to user input)
189 // If the fetch reasons include an API fetch, an initial start or a key hierarchy fetch, become nondiscretionary as well.
190
191 CKOperationDiscretionaryNetworkBehavior networkBehavior = CKOperationDiscretionaryNetworkBehaviorNonDiscretionary;
192 //if(nilChangeTag ||
193 // [self.fetchReasons containsObject:CKKSFetchBecauseAPIFetchRequest] ||
194 // [self.fetchReasons containsObject:CKKSFetchBecauseInitialStart] ||
195 // [self.fetchReasons containsObject:CKKSFetchBecauseKeyHierarchy]) {
196 // networkBehavior = CKOperationDiscretionaryNetworkBehaviorNonDiscretionary;
197 //}
198
199 secnotice("ckksfetch", "Beginning fetch with discretionary network (%d): %@", (int)networkBehavior, self.allClientOptions);
200 self.fetchRecordZoneChangesOperation = [[self.fetchRecordZoneChangesOperationClass alloc] initWithRecordZoneIDs:self.fetchedZoneIDs
201 configurationsByRecordZoneID:self.allClientOptions];
202
203 self.fetchRecordZoneChangesOperation.fetchAllChanges = NO;
204 self.fetchRecordZoneChangesOperation.configuration.discretionaryNetworkBehavior = networkBehavior;
205 self.fetchRecordZoneChangesOperation.configuration.isCloudKitSupportOperation = YES;
206 self.fetchRecordZoneChangesOperation.group = self.ckoperationGroup;
207 secnotice("ckksfetch", "Operation group is %@", self.ckoperationGroup);
208
209 self.fetchRecordZoneChangesOperation.recordChangedBlock = ^(CKRecord *record) {
210 STRONGIFY(self);
211 secinfo("ckksfetch", "CloudKit notification: record changed(%@): %@", [record recordType], record);
212
213 // Add this to the modifications, and remove it from the deletions
214 self.modifications[record.recordID] = record;
215 [self.deletions removeObjectForKey:record.recordID];
216 self.fetchedItems++;
217 };
218
219 self.fetchRecordZoneChangesOperation.recordWithIDWasDeletedBlock = ^(CKRecordID *recordID, NSString *recordType) {
220 STRONGIFY(self);
221 secinfo("ckksfetch", "CloudKit notification: deleted record(%@): %@", recordType, recordID);
222
223 // Add to the deletions, and remove any pending modifications
224 [self.modifications removeObjectForKey: recordID];
225 self.deletions[recordID] = [[CKKSCloudKitDeletion alloc] initWithRecordID:recordID recordType:recordType];
226 self.fetchedItems++;
227 };
228
229 self.fetchRecordZoneChangesOperation.recordZoneChangeTokensUpdatedBlock = ^(CKRecordZoneID *recordZoneID, CKServerChangeToken *serverChangeToken, NSData *clientChangeTokenData) {
230 STRONGIFY(self);
231
232 secinfo("ckksfetch", "Received a new server change token (via block) for %@: %@ %@", recordZoneID, serverChangeToken, clientChangeTokenData);
233 self.changeTokens[recordZoneID] = serverChangeToken;
234 };
235
236 self.fetchRecordZoneChangesOperation.recordZoneFetchCompletionBlock = ^(CKRecordZoneID *recordZoneID, CKServerChangeToken *serverChangeToken, NSData *clientChangeTokenData, BOOL moreComing, NSError * recordZoneError) {
237 STRONGIFY(self);
238
239 secnotice("ckksfetch", "Received a new server change token for %@: %@ %@", recordZoneID, serverChangeToken, clientChangeTokenData);
240 self.changeTokens[recordZoneID] = serverChangeToken;
241 self.allClientOptions[recordZoneID].previousServerChangeToken = serverChangeToken;
242
243 self.moreComing |= moreComing;
244 if(moreComing) {
245 secnotice("ckksfetch", "more changes pending for %@, will start a new fetch at change token %@", recordZoneID, self.changeTokens[recordZoneID]);
246 }
247
248 ckksnotice("ckksfetch", recordZoneID, "Record zone fetch complete: changeToken=%@ clientChangeTokenData=%@ moreComing=%@ error=%@", serverChangeToken, clientChangeTokenData,
249 moreComing ? @"YES" : @"NO",
250 recordZoneError);
251
252 [self sendChangesToClient:recordZoneID moreComing:moreComing];
253 };
254
255 // Called with overall operation success. As I understand it, this block will be called for every operation.
256 // In the case of, e.g., network failure, the recordZoneFetchCompletionBlock will not be called, but this one will.
257 self.fetchRecordZoneChangesOperation.fetchRecordZoneChangesCompletionBlock = ^(NSError * _Nullable operationError) {
258 STRONGIFY(self);
259 if(!self) {
260 secerror("ckksfetch: received callback for released object");
261 return;
262 }
263
264 // Count record changes per zone
265 NSMutableDictionary<CKRecordZoneID*,NSNumber*>* recordChangesPerZone = [NSMutableDictionary dictionary];
266 self.totalModifications += self.modifications.count;
267 self.totalDeletions += self.deletions.count;
268
269 // All of these should have been delivered by recordZoneFetchCompletionBlock; throw them away
270 [self.modifications removeAllObjects];
271 [self.deletions removeAllObjects];
272
273 // If we were told that there were moreChanges coming for any zone, we'd like to fetch again.
274 // This is true if we recieve no error or a network timeout. Any other error should cause a failure.
275 if(self.moreComing && (operationError == nil || [CKKSReachabilityTracker isNetworkFailureError:operationError])) {
276 secnotice("ckksfetch", "Must issue another fetch (with potential error %@)", operationError);
277 self.moreComing = false;
278 [self performFetch];
279 return;
280 }
281
282 if(operationError) {
283 self.error = operationError;
284 }
285
286 secnotice("ckksfetch", "Record zone changes fetch complete: error=%@", operationError);
287
288 [CKKSPowerCollection CKKSPowerEvent:kCKKSPowerEventFetchAllChanges
289 count:self.fetchedItems];
290
291
292 for(CKRecordID* recordID in self.modifications) {
293 NSNumber* last = recordChangesPerZone[recordID.zoneID];
294 recordChangesPerZone[recordID.zoneID] = [NSNumber numberWithUnsignedLong:1+(last ? [last unsignedLongValue] : 0)];
295 }
296 for(CKRecordID* recordID in self.deletions) {
297 NSNumber* last = recordChangesPerZone[recordID.zoneID];
298 recordChangesPerZone[recordID.zoneID] = [NSNumber numberWithUnsignedLong:1+(last ? [last unsignedLongValue] : 0)];
299 }
300
301 for(CKRecordZoneNotification* rz in self.apnsPushes) {
302 if(rz.ckksPushTracingEnabled) {
303 secnotice("ckksfetch", "Submitting post-fetch CKEventMetric due to notification %@", rz);
304
305 // Schedule submitting this metric on another operation, so hopefully CK will have marked this fetch as done by the time that fires?
306 CKEventMetric *metric = [[CKEventMetric alloc] initWithEventName:@"APNSPushMetrics"];
307 metric.isPushTriggerFired = true;
308 metric[@"push_token_uuid"] = rz.ckksPushTracingUUID;
309 metric[@"push_received_date"] = rz.ckksPushReceivedDate;
310 metric[@"push_event_name"] = @"CKKS Push";
311
312 metric[@"fetch_error"] = operationError ? @1 : @0;
313 metric[@"fetch_error_domain"] = operationError.domain;
314 metric[@"fetch_error_code"] = [NSNumber numberWithLong:operationError.code];
315
316 metric[@"total_modifications"] = @(self.totalModifications);
317 metric[@"total_deletions"] = @(self.totalDeletions);
318 for(CKRecordZoneID* zoneID in recordChangesPerZone) {
319 metric[zoneID.zoneName] = recordChangesPerZone[zoneID];
320 }
321
322 SecEventMetric *metric2 = [[SecEventMetric alloc] initWithEventName:@"APNSPushMetrics"];
323 metric2[@"push_token_uuid"] = rz.ckksPushTracingUUID;
324 metric2[@"push_received_date"] = rz.ckksPushReceivedDate;
325 metric2[@"push_event_name"] = @"CKKS Push-webtunnel";
326
327 metric2[@"fetch_error"] = operationError;
328
329 metric2[@"total_modifications"] = @(self.totalModifications);
330 metric2[@"total_deletions"] = @(self.totalDeletions);
331 for(CKRecordZoneID* zoneID in recordChangesPerZone) {
332 metric2[zoneID.zoneName] = recordChangesPerZone[zoneID];
333 }
334
335 // Okay, we now have this metric. But, it's unclear if calling associateWithCompletedOperation in this block will work. So, do something silly with operation scheduling.
336 // Grab pointers to these things
337 CKContainer* container = self.container;
338 CKDatabaseOperation<CKKSFetchRecordZoneChangesOperation>* rzcOperation = self.fetchRecordZoneChangesOperation;
339
340 CKKSResultOperation* launchMetricOp = [CKKSResultOperation named:@"submit-metric" withBlock:^{
341 if(![metric associateWithCompletedOperation:rzcOperation]) {
342 secerror("ckksfetch: Couldn't associate metric with operation: %@ %@", metric, rzcOperation);
343 }
344 [container submitEventMetric:metric];
345 [[SecMetrics managerObject] submitEvent:metric2];
346 secnotice("ckksfetch", "Metric submitted: %@", metric);
347 }];
348 [launchMetricOp addSuccessDependency:self.fetchCompletedOperation];
349
350 [self.operationQueue addOperation:launchMetricOp];
351 }
352 }
353
354 // Trigger the fake 'we're done' operation.
355 [self runBeforeGroupFinished: self.fetchCompletedOperation];
356
357 // Drop strong pointer to clients
358 [self.clientMap removeAllObjects];
359 };
360
361 [self dependOnBeforeGroupFinished:self.fetchCompletedOperation];
362 [self dependOnBeforeGroupFinished:self.fetchRecordZoneChangesOperation];
363 [self.container.privateCloudDatabase addOperation:self.fetchRecordZoneChangesOperation];
364 }
365
366 - (void)sendChangesToClient:(CKRecordZoneID*)recordZoneID moreComing:(BOOL)moreComing
367 {
368 id<CKKSChangeFetcherClient> client = self.clientMap[recordZoneID];
369 if(!client) {
370 secerror("ckksfetch: no client registered for %@, so why did we get any data?", recordZoneID);
371 return;
372 }
373
374 // First, filter the modifications and deletions for this zone
375 NSMutableArray<CKRecord*>* zoneModifications = [NSMutableArray array];
376 NSMutableArray<CKKSCloudKitDeletion*>* zoneDeletions = [NSMutableArray array];
377
378 [self.modifications enumerateKeysAndObjectsUsingBlock:^(CKRecordID* _Nonnull recordID,
379 CKRecord* _Nonnull record,
380 BOOL* stop) {
381 if([recordID.zoneID isEqual:recordZoneID]) {
382 ckksinfo("ckksfetch", recordZoneID, "Sorting record modification %@: %@", recordID, record);
383 [zoneModifications addObject:record];
384 }
385 }];
386
387 [self.deletions enumerateKeysAndObjectsUsingBlock:^(CKRecordID* _Nonnull recordID,
388 CKKSCloudKitDeletion* _Nonnull deletion,
389 BOOL* _Nonnull stop) {
390 if([recordID.zoneID isEqual:recordZoneID]) {
391 ckksinfo("ckksfetch", recordZoneID, "Sorting record deletion %@: %@", recordID, deletion);
392 [zoneDeletions addObject:deletion];
393 }
394 }];
395
396 BOOL resync = [self.resyncingZones containsObject:recordZoneID];
397
398 ckksnotice("ckksfetch", recordZoneID, "Delivering fetched changes: changed=%lu deleted=%lu moreComing=%lu resync=%u",
399 (unsigned long)zoneModifications.count, (unsigned long)zoneDeletions.count, (unsigned long)moreComing, resync);
400
401 // Tell the client about these changes!
402 [client changesFetched:zoneModifications
403 deletedRecordIDs:zoneDeletions
404 newChangeToken:self.changeTokens[recordZoneID]
405 moreComing:moreComing
406 resync:resync];
407
408 if(resync && !moreComing) {
409 ckksnotice("ckksfetch", recordZoneID, "No more changes for zone; turning off resync bit");
410 [self.resyncingZones removeObject:recordZoneID];
411 }
412 }
413
414 - (void)cancel {
415 [self.fetchRecordZoneChangesOperation cancel];
416 [super cancel];
417 }
418
419 @end
420
421 #endif