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