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