]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSZoneChangeFetcher.m
8b9fe2f50c1df7517dec9c36a6f2d5105447af63
[apple/security.git] / keychain / ckks / CKKSZoneChangeFetcher.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 <dispatch/dispatch.h>
29
30 #import "keychain/ckks/CKKSZoneChangeFetcher.h"
31 #import "keychain/ckks/CKKSFetchAllRecordZoneChangesOperation.h"
32 #import "keychain/ckks/CKKSKeychainView.h"
33 #import "keychain/ckks/CKKSNearFutureScheduler.h"
34 #import "keychain/ckks/CloudKitCategories.h"
35 #import "keychain/ckks/CKKSReachabilityTracker.h"
36 #import "keychain/categories/NSError+UsefulConstructors.h"
37 #import "keychain/analytics/SecEventMetric.h"
38 #import "keychain/analytics/SecMetrics.h"
39 #import "keychain/ot/ObjCImprovements.h"
40
41 CKKSFetchBecause* const CKKSFetchBecauseAPNS = (CKKSFetchBecause*) @"apns";
42 CKKSFetchBecause* const CKKSFetchBecauseAPIFetchRequest = (CKKSFetchBecause*) @"api";
43 CKKSFetchBecause* const CKKSFetchBecauseCurrentItemFetchRequest = (CKKSFetchBecause*) @"currentitemcheck";
44 CKKSFetchBecause* const CKKSFetchBecauseInitialStart = (CKKSFetchBecause*) @"initialfetch";
45 CKKSFetchBecause* const CKKSFetchBecauseSecuritydRestart = (CKKSFetchBecause*) @"restart";
46 CKKSFetchBecause* const CKKSFetchBecausePreviousFetchFailed = (CKKSFetchBecause*) @"fetchfailed";
47 CKKSFetchBecause* const CKKSFetchBecauseNetwork = (CKKSFetchBecause*) @"network";
48 CKKSFetchBecause* const CKKSFetchBecauseKeyHierarchy = (CKKSFetchBecause*) @"keyhierarchy";
49 CKKSFetchBecause* const CKKSFetchBecauseTesting = (CKKSFetchBecause*) @"testing";
50 CKKSFetchBecause* const CKKSFetchBecauseResync = (CKKSFetchBecause*) @"resync";
51 CKKSFetchBecause* const CKKSFetchBecauseMoreComing = (CKKSFetchBecause*) @"more-coming";
52
53 #pragma mark - CKKSZoneChangeFetchDependencyOperation
54 @interface CKKSZoneChangeFetchDependencyOperation : CKKSResultOperation
55 @property CKKSZoneChangeFetcher* owner;
56 @property NSMutableArray<CKKSZoneChangeFetchDependencyOperation*>* chainDependents;
57 - (void)chainDependency:(CKKSZoneChangeFetchDependencyOperation*)newDependency;
58 @end
59
60 @implementation CKKSZoneChangeFetchDependencyOperation
61 - (instancetype)init {
62 if((self = [super init])) {
63 _chainDependents = [NSMutableArray array];
64 }
65 return self;
66 }
67
68 - (NSError* _Nullable)descriptionError {
69 return [NSError errorWithDomain:CKKSResultDescriptionErrorDomain
70 code:CKKSResultDescriptionPendingSuccessfulFetch
71 description:@"Fetch failed"
72 underlying:self.owner.lastCKFetchError];
73 }
74
75 - (void)chainDependency:(CKKSZoneChangeFetchDependencyOperation*)newDependency {
76 [self addSuccessDependency:newDependency];
77
78 // There's no need to build a chain more than two links long. Move all our children up to depend on the new dependency.
79 for(CKKSZoneChangeFetchDependencyOperation* op in self.chainDependents) {
80 [newDependency.chainDependents addObject:op];
81 [op addSuccessDependency:newDependency];
82 [op removeDependency:self];
83 }
84 [self.chainDependents removeAllObjects];
85 }
86 @end
87
88 #pragma mark - CKKSZoneChangeFetcher
89
90 @interface CKKSZoneChangeFetcher ()
91 @property NSString* name;
92 @property NSOperationQueue* operationQueue;
93 @property dispatch_queue_t queue;
94
95 @property NSError* lastCKFetchError;
96
97 @property NSMapTable<CKRecordZoneID*, id<CKKSChangeFetcherClient>>* clientMap;
98
99 @property CKKSFetchAllRecordZoneChangesOperation* currentFetch;
100 @property CKKSResultOperation* currentProcessResult;
101
102 @property NSMutableSet<CKKSFetchBecause*>* currentFetchReasons;
103 @property NSMutableSet<CKRecordZoneNotification*>* apnsPushes;
104 @property bool newRequests; // true if there's someone pending on successfulFetchDependency
105 @property CKKSZoneChangeFetchDependencyOperation* successfulFetchDependency;
106
107 @property CKKSResultOperation* holdOperation;
108 @end
109
110 @implementation CKKSZoneChangeFetcher
111
112 - (instancetype)initWithContainer:(CKContainer*)container
113 fetchClass:(Class<CKKSFetchRecordZoneChangesOperation>)fetchRecordZoneChangesOperationClass
114 reachabilityTracker:(CKKSReachabilityTracker *)reachabilityTracker
115 {
116 if((self = [super init])) {
117 _container = container;
118 _fetchRecordZoneChangesOperationClass = fetchRecordZoneChangesOperationClass;
119 _reachabilityTracker = reachabilityTracker;
120
121 _currentFetchReasons = [[NSMutableSet alloc] init];
122 _apnsPushes = [[NSMutableSet alloc] init];
123
124 _clientMap = [NSMapTable strongToWeakObjectsMapTable];
125
126 _name = @"zone-change-fetcher";
127 _queue = dispatch_queue_create([_name UTF8String], DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
128 _operationQueue = [[NSOperationQueue alloc] init];
129 _successfulFetchDependency = [self createSuccesfulFetchDependency];
130
131 _newRequests = false;
132
133 // If we're testing, for the initial delay, use 0.2 second. Otherwise, 2s.
134 dispatch_time_t initialDelay = (SecCKKSReduceRateLimiting() ? 200 * NSEC_PER_MSEC : 2 * NSEC_PER_SEC);
135
136 // If we're testing, for the continuing delay, use 2 second. Otherwise, 30s.
137 dispatch_time_t continuingDelay = (SecCKKSReduceRateLimiting() ? 2 * NSEC_PER_SEC : 30 * NSEC_PER_SEC);
138
139 WEAKIFY(self);
140 _fetchScheduler = [[CKKSNearFutureScheduler alloc] initWithName:@"zone-change-fetch-scheduler"
141 initialDelay:initialDelay
142 continuingDelay:continuingDelay
143 keepProcessAlive:false
144 dependencyDescriptionCode:CKKSResultDescriptionPendingZoneChangeFetchScheduling
145 block:^{
146 STRONGIFY(self);
147 [self maybeCreateNewFetch];
148 }];
149 }
150 return self;
151 }
152
153 - (NSString*)description {
154 NSDate* nextFetchAt = self.fetchScheduler.nextFireTime;
155 if(nextFetchAt) {
156 NSDateFormatter* dateFormatter = [[NSDateFormatter alloc] init];
157 [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
158 return [NSString stringWithFormat: @"<CKKSZoneChangeFetcher(%@): next fetch at %@", self.name, [dateFormatter stringFromDate: nextFetchAt]];
159 } else {
160 return [NSString stringWithFormat: @"<CKKSZoneChangeFetcher(%@): no pending fetches", self.name];
161 }
162 }
163
164 - (void)registerClient:(id<CKKSChangeFetcherClient>)client
165 {
166 @synchronized(self.clientMap) {
167 [self.clientMap setObject:client forKey:client.zoneID];
168 }
169 }
170
171
172 - (CKKSResultOperation*)requestSuccessfulFetch:(CKKSFetchBecause*)why {
173 return [self requestSuccessfulFetchForManyReasons:[NSSet setWithObject:why]];
174 }
175
176 - (CKKSResultOperation*)requestSuccessfulFetchForManyReasons:(NSSet<CKKSFetchBecause*>*)why
177 {
178 return [self requestSuccessfulFetchForManyReasons:why apns:nil];
179 }
180
181 - (CKKSResultOperation*)requestSuccessfulFetchDueToAPNS:(CKRecordZoneNotification*)notification
182 {
183 return [self requestSuccessfulFetchForManyReasons:[NSSet setWithObject:CKKSFetchBecauseAPNS] apns:notification];
184 }
185
186 - (CKKSResultOperation*)requestSuccessfulFetchForManyReasons:(NSSet<CKKSFetchBecause*>*)why apns:(CKRecordZoneNotification*)notification
187 {
188 __block CKKSResultOperation* dependency = nil;
189 dispatch_sync(self.queue, ^{
190 dependency = self.successfulFetchDependency;
191 self.newRequests = true;
192 [self.currentFetchReasons unionSet:why];
193 if(notification) {
194 [self.apnsPushes addObject:notification];
195
196 if(notification.ckksPushTracingEnabled) {
197 // Report that we saw this notification before doing anything else
198 secnotice("ckksfetch", "Submitting initial CKEventMetric due to notification %@", notification);
199
200 CKEventMetric *metric = [[CKEventMetric alloc] initWithEventName:@"APNSPushMetrics"];
201 metric.isPushTriggerFired = true;
202 metric[@"push_token_uuid"] = notification.ckksPushTracingUUID;
203 metric[@"push_received_date"] = notification.ckksPushReceivedDate;
204 metric[@"push_event_name"] = @"CKKS APNS Push Received";
205
206 [self.container submitEventMetric:metric];
207
208 SecEventMetric *metric2 = [[SecEventMetric alloc] initWithEventName:@"APNSPushMetrics"];
209 metric2[@"push_token_uuid"] = notification.ckksPushTracingUUID;
210 metric2[@"push_received_date"] = notification.ckksPushReceivedDate;
211 metric2[@"push_event_name"] = @"CKKS APNS Push Received-webtunnel";
212
213 [[SecMetrics managerObject] submitEvent:metric2];
214
215 }
216
217 }
218
219 [self.fetchScheduler trigger];
220 });
221
222 return dependency;
223 }
224
225 -(void)maybeCreateNewFetchOnQueue {
226 dispatch_assert_queue(self.queue);
227 if(self.newRequests &&
228 (self.currentFetch == nil || [self.currentFetch isFinished]) &&
229 (self.currentProcessResult == nil || [self.currentProcessResult isFinished])) {
230 [self _onqueueCreateNewFetch];
231 }
232 }
233
234 -(void)maybeCreateNewFetch {
235 dispatch_sync(self.queue, ^{
236 [self maybeCreateNewFetchOnQueue];
237 });
238 }
239
240 -(void)_onqueueCreateNewFetch {
241 dispatch_assert_queue(self.queue);
242
243 WEAKIFY(self);
244
245 CKKSZoneChangeFetchDependencyOperation* dependency = self.successfulFetchDependency;
246 NSMutableSet<CKKSFetchBecause*>* lastFetchReasons = self.currentFetchReasons;
247 self.currentFetchReasons = [[NSMutableSet alloc] init];
248
249 NSString *reasonsString = [[lastFetchReasons sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"description" ascending:YES]]] componentsJoinedByString:@","];
250
251 secnotice("ckksfetcher", "Starting a new fetch, reasons: %@", reasonsString);
252
253 NSMutableSet<CKRecordZoneNotification*>* lastAPNSPushes = self.apnsPushes;
254 self.apnsPushes = [[NSMutableSet alloc] init];
255
256 CKOperationGroup* operationGroup = [CKOperationGroup CKKSGroupWithName: reasonsString];
257
258 NSMutableArray<id<CKKSChangeFetcherClient>>* clients = [NSMutableArray array];
259 @synchronized(self.clientMap) {
260 for(id<CKKSChangeFetcherClient> client in [self.clientMap objectEnumerator]) {
261 if(client != nil) {
262 [clients addObject:client];
263 }
264 }
265 }
266
267 if(clients.count == 0u) {
268 secnotice("ckksfetcher", "No clients");
269 // Nothing to do, really.
270 }
271
272 CKKSFetchAllRecordZoneChangesOperation* fetchAllChanges = [[CKKSFetchAllRecordZoneChangesOperation alloc] initWithContainer:self.container
273 fetchClass:self.fetchRecordZoneChangesOperationClass
274 clients:clients
275 fetchReasons:lastFetchReasons
276 apnsPushes:lastAPNSPushes
277 forceResync:false
278 ckoperationGroup:operationGroup];
279
280 if ([lastFetchReasons containsObject:CKKSFetchBecauseNetwork]) {
281 secnotice("ckksfetcher", "blocking fetch on network reachability");
282 [fetchAllChanges addNullableDependency: self.reachabilityTracker.reachabilityDependency]; // wait on network, if its unavailable
283 }
284 [fetchAllChanges addNullableDependency: self.holdOperation];
285
286 self.currentProcessResult = [CKKSResultOperation operationWithBlock: ^{
287 STRONGIFY(self);
288 if(!self) {
289 secerror("ckksfetcher: Received a null self pointer; strange.");
290 return;
291 }
292
293 bool attemptAnotherFetch = false;
294 if(fetchAllChanges.error != nil) {
295 secerror("ckksfetcher: Interrogating clients about fetch error: %@", fetchAllChanges.error);
296
297 // Check in with clients: should we keep fetching for them?
298 @synchronized(self.clientMap) {
299 for(CKRecordZoneID* zoneID in fetchAllChanges.fetchedZoneIDs) {
300 id<CKKSChangeFetcherClient> client = [self.clientMap objectForKey:zoneID];
301 if(client) {
302 attemptAnotherFetch |= [client shouldRetryAfterFetchError:fetchAllChanges.error];
303 }
304 }
305 }
306 }
307
308 dispatch_sync(self.queue, ^{
309 self.lastCKFetchError = fetchAllChanges.error;
310
311 if(!fetchAllChanges.error) {
312 if (attemptAnotherFetch) {
313 [dependency chainDependency:self.successfulFetchDependency];
314 [self.operationQueue addOperation: dependency];
315
316 [self.currentFetchReasons unionSet:lastFetchReasons];
317 [self.apnsPushes unionSet:lastAPNSPushes];
318
319 self.newRequests = true;
320 [self.fetchScheduler trigger];
321 } else {
322 // success! notify the listeners.
323 [self.operationQueue addOperation: dependency];
324 self.currentFetch = nil;
325
326 // Did new people show up and want another fetch?
327 if(self.newRequests) {
328 [self.fetchScheduler trigger];
329 }
330 }
331 } else {
332 // The operation errored. Chain the dependency on the current one...
333 [dependency chainDependency:self.successfulFetchDependency];
334 [self.operationQueue addOperation: dependency];
335
336 if(!attemptAnotherFetch) {
337 secerror("ckksfetcher: All clients thought %@ is a fatal error. Not restarting fetch.", fetchAllChanges.error);
338 return;
339 }
340
341 // And in a bit, try the fetch again.
342 NSTimeInterval delay = CKRetryAfterSecondsForError(fetchAllChanges.error);
343 if (delay) {
344 secnotice("ckksfetcher", "Fetch failed with rate-limiting error, restarting in %.1f seconds: %@", delay, fetchAllChanges.error);
345 [self.fetchScheduler waitUntil:NSEC_PER_SEC * delay];
346 } else {
347 secnotice("ckksfetcher", "Fetch failed with error, restarting soon: %@", fetchAllChanges.error);
348 }
349
350 // Add the failed fetch reasons to the new fetch reasons
351 [self.currentFetchReasons unionSet:lastFetchReasons];
352 [self.apnsPushes unionSet:lastAPNSPushes];
353
354 // If its a network error, make next try depend on network availability
355 if ([self.reachabilityTracker isNetworkError:fetchAllChanges.error]) {
356 [self.currentFetchReasons addObject:CKKSFetchBecauseNetwork];
357 } else {
358 [self.currentFetchReasons addObject:CKKSFetchBecausePreviousFetchFailed];
359 }
360 self.newRequests = true;
361 [self.fetchScheduler trigger];
362 }
363 });
364 }];
365
366 // creata a new fetch dependency, for all those who come in while this operation is executing
367 self.newRequests = false;
368 self.successfulFetchDependency = [self createSuccesfulFetchDependency];
369
370 // now let new new fetch go and process it's results
371 self.currentProcessResult.name = @"zone-change-fetcher-worker";
372 [self.currentProcessResult addDependency: fetchAllChanges];
373
374 [self.operationQueue addOperation:self.currentProcessResult];
375
376 self.currentFetch = fetchAllChanges;
377 [self.operationQueue addOperation:self.currentFetch];
378 }
379
380 -(CKKSZoneChangeFetchDependencyOperation*)createSuccesfulFetchDependency {
381 CKKSZoneChangeFetchDependencyOperation* dep = [[CKKSZoneChangeFetchDependencyOperation alloc] init];
382
383 dep.name = @"successful-fetch-dependency";
384 dep.descriptionErrorCode = CKKSResultDescriptionPendingSuccessfulFetch;
385 dep.owner = self;
386
387 return dep;
388 }
389
390 - (void)holdFetchesUntil:(CKKSResultOperation*)holdOperation {
391 self.holdOperation = holdOperation;
392 }
393
394 -(void)cancel {
395 [self.fetchScheduler cancel];
396 }
397
398 @end
399
400 #endif
401
402