]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSZoneChangeFetcher.m
Security-58286.51.6.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
36 CKKSFetchBecause* const CKKSFetchBecauseAPNS = (CKKSFetchBecause*) @"apns";
37 CKKSFetchBecause* const CKKSFetchBecauseAPIFetchRequest = (CKKSFetchBecause*) @"api";
38 CKKSFetchBecause* const CKKSFetchBecauseCurrentItemFetchRequest = (CKKSFetchBecause*) @"currentitemcheck";
39 CKKSFetchBecause* const CKKSFetchBecauseInitialStart = (CKKSFetchBecause*) @"initialfetch";
40 CKKSFetchBecause* const CKKSFetchBecauseSecuritydRestart = (CKKSFetchBecause*) @"restart";
41 CKKSFetchBecause* const CKKSFetchBecausePreviousFetchFailed = (CKKSFetchBecause*) @"fetchfailed";
42 CKKSFetchBecause* const CKKSFetchBecauseNetwork = (CKKSFetchBecause*) @"network";
43 CKKSFetchBecause* const CKKSFetchBecauseKeyHierarchy = (CKKSFetchBecause*) @"keyhierarchy";
44 CKKSFetchBecause* const CKKSFetchBecauseTesting = (CKKSFetchBecause*) @"testing";
45 CKKSFetchBecause* const CKKSFetchBecauseResync = (CKKSFetchBecause*) @"resync";
46
47 #pragma mark - CKKSZoneChangeFetchDependencyOperation
48 @interface CKKSZoneChangeFetchDependencyOperation : CKKSResultOperation
49 @property CKKSZoneChangeFetcher* owner;
50 @end
51
52 @implementation CKKSZoneChangeFetchDependencyOperation
53 - (NSError* _Nullable)descriptionError {
54 return [NSError errorWithDomain:CKKSResultDescriptionErrorDomain
55 code:CKKSResultDescriptionPendingSuccessfulFetch
56 description:@"Fetch failed"
57 underlying:self.owner.lastCKFetchError];
58 }
59 @end
60
61 #pragma mark - CKKSZoneChangeFetcher
62
63 @interface CKKSZoneChangeFetcher ()
64 @property NSString* name;
65 @property dispatch_queue_t queue;
66
67 @property NSError* lastCKFetchError;
68
69 @property CKKSFetchAllRecordZoneChangesOperation* currentFetch;
70 @property CKKSResultOperation* currentProcessResult;
71
72 @property NSMutableSet<CKKSFetchBecause*>* currentFetchReasons;
73 @property bool newRequests; // true if there's someone pending on successfulFetchDependency
74 @property bool newResyncRequests; // true if someone asked for a refetch operation
75 @property CKKSResultOperation* successfulFetchDependency;
76
77 @property CKKSNearFutureScheduler* fetchScheduler;
78
79 @property CKKSResultOperation* holdOperation;
80 @end
81
82 @implementation CKKSZoneChangeFetcher
83
84 - (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks {
85 if((self = [super init])) {
86 _ckks = ckks;
87 _zoneID = ckks.zoneID;
88
89 _currentFetchReasons = [[NSMutableSet alloc] init];
90
91 _name = [NSString stringWithFormat:@"zone-change-fetcher-%@", _zoneID.zoneName];
92 _queue = dispatch_queue_create([_name UTF8String], DISPATCH_QUEUE_SERIAL);
93 _successfulFetchDependency = [self createSuccesfulFetchDependency];
94
95 _newRequests = false;
96
97 // If we're testing, for the initial delay, use 0.2 second. Otherwise, 2s.
98 dispatch_time_t initialDelay = (SecCKKSReduceRateLimiting() ? 200 * NSEC_PER_MSEC : 2 * NSEC_PER_SEC);
99
100 // If we're testing, for the initial delay, use 2 second. Otherwise, 30s.
101 dispatch_time_t continuingDelay = (SecCKKSReduceRateLimiting() ? 2 * NSEC_PER_SEC : 30 * NSEC_PER_SEC);
102
103 __weak __typeof(self) weakSelf = self;
104 _fetchScheduler = [[CKKSNearFutureScheduler alloc] initWithName:[NSString stringWithFormat:@"zone-change-fetch-scheduler-%@", self.zoneID.zoneName]
105 initialDelay:initialDelay
106 continuingDelay:continuingDelay
107 keepProcessAlive:false
108 dependencyDescriptionCode:CKKSResultDescriptionPendingZoneChangeFetchScheduling
109 block:^{
110 [weakSelf maybeCreateNewFetch];
111 }];
112 }
113 return self;
114 }
115
116 - (NSString*)description {
117 NSDate* nextFetchAt = self.fetchScheduler.nextFireTime;
118 if(nextFetchAt) {
119 NSDateFormatter* dateFormatter = [[NSDateFormatter alloc] init];
120 [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
121 return [NSString stringWithFormat: @"<CKKSZoneChangeFetcher(%@): next fetch at %@", self.name, [dateFormatter stringFromDate: nextFetchAt]];
122 } else {
123 return [NSString stringWithFormat: @"<CKKSZoneChangeFetcher(%@): no pending fetches", self.name];
124 }
125 }
126
127 - (CKKSResultOperation*)requestSuccessfulFetch:(CKKSFetchBecause*)why {
128 return [self requestSuccessfulFetch:why resync:false];
129 }
130
131 - (CKKSResultOperation*)requestSuccessfulResyncFetch:(CKKSFetchBecause*)why {
132 return [self requestSuccessfulFetch:why resync:true];
133 }
134
135 - (CKKSResultOperation*)requestSuccessfulFetch:(CKKSFetchBecause*)why resync:(bool)resync {
136 __block CKKSResultOperation* dependency = nil;
137 dispatch_sync(self.queue, ^{
138 dependency = self.successfulFetchDependency;
139 self.newRequests = true;
140 self.newResyncRequests |= resync;
141 [self.currentFetchReasons addObject: why];
142
143 [self.fetchScheduler trigger];
144 });
145
146 return dependency;
147 }
148
149 -(void)maybeCreateNewFetch {
150 dispatch_sync(self.queue, ^{
151 if(self.newRequests &&
152 (self.currentFetch == nil || [self.currentFetch isFinished]) &&
153 (self.currentProcessResult == nil || [self.currentProcessResult isFinished])) {
154 [self _onqueueCreateNewFetch];
155 }
156 });
157 }
158
159 -(void)_onqueueCreateNewFetch {
160 dispatch_assert_queue(self.queue);
161
162 __weak __typeof(self) weakSelf = self;
163
164 CKKSResultOperation* dependency = self.successfulFetchDependency;
165
166 CKKSKeychainView* ckks = self.ckks; // take a strong reference
167 if(!ckks) {
168 secerror("ckksfetcher: received a null CKKSKeychainView pointer; strange.");
169 return;
170 }
171
172 ckksnotice("ckksfetcher", self.zoneID, "Starting a new fetch for %@", self.zoneID.zoneName);
173
174 NSMutableSet<CKKSFetchBecause*>* lastFetchReasons = self.currentFetchReasons;
175 self.currentFetchReasons = [[NSMutableSet alloc] init];
176 if(self.newResyncRequests) {
177 [lastFetchReasons addObject:CKKSFetchBecauseResync];
178 }
179
180 CKOperationGroup* operationGroup = [CKOperationGroup CKKSGroupWithName: [[lastFetchReasons sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"description" ascending:YES]]] componentsJoinedByString:@","]];
181
182 CKKSFetchAllRecordZoneChangesOperation* fetchAllChanges = [[CKKSFetchAllRecordZoneChangesOperation alloc] initWithCKKSKeychainView:ckks
183 fetchReasons:lastFetchReasons
184 ckoperationGroup:operationGroup];
185 if ([lastFetchReasons containsObject:CKKSFetchBecauseNetwork]) {
186 [fetchAllChanges addNullableDependency: ckks.reachabilityTracker.reachablityDependency]; // wait on network, if its unavailable
187 }
188 [fetchAllChanges addNullableDependency: self.holdOperation];
189 fetchAllChanges.resync = self.newResyncRequests;
190 self.newResyncRequests = false;
191
192 // Can't fetch until the zone is setup.
193 [fetchAllChanges addNullableDependency:ckks.zoneSetupOperation];
194
195 self.currentProcessResult = [CKKSResultOperation operationWithBlock: ^{
196 __strong __typeof(self) strongSelf = weakSelf;
197 if(!strongSelf) {
198 secerror("ckksfetcher: Received a null self pointer; strange.");
199 return;
200 }
201
202 CKKSKeychainView* blockckks = strongSelf.ckks; // take a strong reference
203 if(!blockckks) {
204 secerror("ckksfetcher: Received a null CKKSKeychainView pointer; strange.");
205 return;
206 }
207
208 dispatch_sync(strongSelf.queue, ^{
209 self.lastCKFetchError = fetchAllChanges.error;
210
211 if(!fetchAllChanges.error) {
212 // success! notify the listeners.
213 [blockckks scheduleOperation: dependency];
214
215 // Did new people show up and want another fetch?
216 if(strongSelf.newRequests) {
217 [strongSelf.fetchScheduler trigger];
218 }
219 } else {
220 // The operation errored. Chain the dependency on the current one...
221 [dependency addSuccessDependency: strongSelf.successfulFetchDependency];
222 [blockckks scheduleOperation: dependency];
223
224 if([blockckks isFatalCKFetchError: fetchAllChanges.error]) {
225 ckkserror("ckksfetcher", strongSelf.zoneID, "Notified that %@ is a fatal error. Not restarting fetch.", fetchAllChanges.error);
226 return;
227 }
228
229
230 // And in a bit, try the fetch again.
231 NSNumber* delaySeconds = fetchAllChanges.error.userInfo[CKErrorRetryAfterKey];
232 if([fetchAllChanges.error.domain isEqual: CKErrorDomain] && delaySeconds) {
233 ckksnotice("ckksfetcher", strongSelf.zoneID, "Fetch failed with rate-limiting error, restarting in %@ seconds: %@", delaySeconds, fetchAllChanges.error);
234 [strongSelf.fetchScheduler waitUntil: NSEC_PER_SEC * [delaySeconds unsignedLongValue]];
235 } else {
236 ckksnotice("ckksfetcher", strongSelf.zoneID, "Fetch failed with error, restarting soon: %@", fetchAllChanges.error);
237 }
238
239 // Add the failed fetch reasons to the new fetch reasons
240 [strongSelf.currentFetchReasons unionSet:lastFetchReasons];
241 // If its a network error, make next try depend on network availability
242 if ([blockckks.reachabilityTracker isNetworkError:fetchAllChanges.error]) {
243 [strongSelf.currentFetchReasons addObject:CKKSFetchBecauseNetwork];
244 } else {
245 [strongSelf.currentFetchReasons addObject:CKKSFetchBecausePreviousFetchFailed];
246 }
247 strongSelf.newRequests = true;
248 strongSelf.newResyncRequests |= fetchAllChanges.resync;
249 [strongSelf.fetchScheduler trigger];
250 }
251 });
252 }];
253 self.currentProcessResult.name = @"zone-change-fetcher-worker";
254 [self.currentProcessResult addDependency: fetchAllChanges];
255
256 [ckks scheduleOperation: self.currentProcessResult];
257
258 self.currentFetch = fetchAllChanges;
259 [ckks scheduleOperation: self.currentFetch];
260
261 // creata a new fetch dependency, for all those who come in while this operation is executing
262 self.newRequests = false;
263 self.successfulFetchDependency = [self createSuccesfulFetchDependency];
264 }
265
266 -(CKKSZoneChangeFetchDependencyOperation*)createSuccesfulFetchDependency {
267 CKKSZoneChangeFetchDependencyOperation* dep = [[CKKSZoneChangeFetchDependencyOperation alloc] init];
268 __weak __typeof(dep) weakDep = dep;
269
270 // Since these dependencies might chain, when one runs, break the chain.
271 [dep addExecutionBlock:^{
272 __strong __typeof(dep) strongDep = weakDep;
273
274 // Remove all dependencies
275 NSArray* deps = [strongDep.dependencies copy];
276 for(NSOperation* op in deps) {
277 [strongDep removeDependency: op];
278 }
279 }];
280 dep.name = @"successful-fetch-dependency";
281 dep.descriptionErrorCode = CKKSResultDescriptionPendingSuccessfulFetch;
282 dep.owner = self;
283
284 return dep;
285 }
286
287 - (void)holdFetchesUntil:(CKKSResultOperation*)holdOperation {
288 self.holdOperation = holdOperation;
289 }
290
291 -(void)cancel {
292 [self.fetchScheduler cancel];
293 }
294
295 @end
296
297 #endif
298
299