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