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