]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSZoneModifier.m
Security-59306.61.1.tar.gz
[apple/security.git] / keychain / ckks / CKKSZoneModifier.m
1 #if OCTAGON
2
3 #import <CloudKit/CloudKit_Private.h>
4
5 #import "keychain/ckks/CloudKitCategories.h"
6 #import "keychain/ckks/CKKS.h"
7 #import "keychain/ckks/CKKSNearFutureScheduler.h"
8 #import "keychain/ckks/CKKSReachabilityTracker.h"
9 #import "keychain/ckks/CKKSZoneModifier.h"
10
11 #import "utilities/debugging.h"
12 #import "keychain/ot/ObjCImprovements.h"
13
14 @implementation CKKSZoneModifyOperations
15
16 - (instancetype)init
17 {
18 return nil;
19 }
20
21 - (instancetype)initWithZoneModificationOperation:(CKKSResultOperation*)zoneModificationOperationDependency
22 zoneSubscriptionOperation:(CKKSResultOperation*)zoneSubscriptionOperationDependency
23 {
24 if((self = [super init])) {
25 _zoneModificationOperation = zoneModificationOperationDependency;
26 _zoneSubscriptionOperation = zoneSubscriptionOperationDependency;
27
28 _zonesToCreate = [NSMutableArray array];
29 _subscriptionsToSubscribe = [NSMutableArray array];
30 _zoneIDsToDelete = [NSMutableArray array];
31 }
32 return self;
33 }
34 @end
35
36 // CKKSZoneModifier
37
38 @interface CKKSZoneModifier ()
39
40 // Returned to clients that call createZone/deleteZone before it launches
41 @property CKKSZoneModifyOperations* pendingOperations;
42
43 @property bool halted;
44
45 @property NSOperationQueue* operationQueue;
46 @property dispatch_queue_t queue;
47
48 // Used to linearize all CK operations to avoid tripping over our own feet
49 @property NSHashTable<CKDatabaseOperation*>* ckOperations;
50 @property CKKSZoneModifyOperations* inflightOperations;
51
52 // Set to true if the last CK operation finished with a network error
53 // Cleared if the last CK operation didn't
54 // Used as an heuristic to wait on the reachability tracker
55 @property bool networkFailure;
56 @end
57
58 @implementation CKKSZoneModifier
59 - (instancetype)init
60 {
61 return nil;
62 }
63
64 - (instancetype)initWithContainer:(CKContainer*)container
65 reachabilityTracker:(CKKSReachabilityTracker*)reachabilityTracker
66 cloudkitDependencies:(CKKSCloudKitClassDependencies*)cloudKitClassDependencies
67 {
68 if((self = [super init])) {
69 _container = container;
70 _reachabilityTracker = reachabilityTracker;
71 _cloudKitClassDependencies = cloudKitClassDependencies;
72
73 _database = [_container privateCloudDatabase];
74 _operationQueue = [[NSOperationQueue alloc] init];
75 _queue = dispatch_queue_create([[NSString stringWithFormat:@"CKKSZoneModifier.%@", container.containerIdentifier] UTF8String],
76 DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
77
78 WEAKIFY(self);
79
80 // This does double-duty: if there's pending zone creation/deletions, it launches them
81 _cloudkitRetryAfter = [[CKKSNearFutureScheduler alloc] initWithName:@"zonemodifier-ckretryafter"
82 initialDelay:100*NSEC_PER_MSEC
83 continuingDelay:100*NSEC_PER_MSEC
84 keepProcessAlive:false
85 dependencyDescriptionCode:CKKSResultDescriptionPendingCloudKitRetryAfter
86 block:^{
87 STRONGIFY(self);
88 [self launchOperations];
89 }];
90
91 _ckOperations = [NSHashTable weakObjectsHashTable];
92 }
93 return self;
94 }
95
96 - (void)_onqueueCreatePendingObjects
97 {
98 dispatch_assert_queue(self.queue);
99
100 if(!self.pendingOperations) {
101 CKKSResultOperation* zoneModificationOperationDependency = [CKKSResultOperation named:@"zone-modification" withBlockTakingSelf:^(CKKSResultOperation * _Nonnull op) {
102 secnotice("ckkszonemodifier", "finished creating zones");
103 }];
104
105 CKKSResultOperation* zoneSubscriptionOperationDependency = [CKKSResultOperation named:@"zone-subscription" withBlockTakingSelf:^(CKKSResultOperation * _Nonnull op) {
106 secnotice("ckkszonemodifier", "finished subscribing to zones");
107 }];
108
109 self.pendingOperations = [[CKKSZoneModifyOperations alloc] initWithZoneModificationOperation:zoneModificationOperationDependency
110 zoneSubscriptionOperation:zoneSubscriptionOperationDependency];
111 }
112 }
113
114 - (CKKSZoneModifyOperations*)createZone:(CKRecordZone*)zone
115 {
116 __block CKKSZoneModifyOperations* ops = nil;
117
118 dispatch_sync(self.queue, ^{
119 [self _onqueueCreatePendingObjects];
120 ops = self.pendingOperations;
121
122 [ops.zonesToCreate addObject:zone];
123 CKRecordZoneSubscription* subscription = [[CKRecordZoneSubscription alloc] initWithZoneID:zone.zoneID
124 subscriptionID:[@"zone:" stringByAppendingString: zone.zoneID.zoneName]];
125 [ops.subscriptionsToSubscribe addObject:subscription];
126 });
127
128 [self.cloudkitRetryAfter trigger];
129
130 return ops;
131 }
132
133 - (CKKSZoneModifyOperations*)deleteZone:(CKRecordZoneID*)zoneID
134 {
135 __block CKKSZoneModifyOperations* ops = nil;
136
137 dispatch_sync(self.queue, ^{
138 [self _onqueueCreatePendingObjects];
139 ops = self.pendingOperations;
140
141 [ops.zoneIDsToDelete addObject:zoneID];
142 });
143
144 [self.cloudkitRetryAfter trigger];
145
146 return ops;
147 }
148
149 - (void)halt
150 {
151 dispatch_sync(self.queue, ^{
152 self.halted = true;
153 });
154 }
155
156 // Called by the NearFutureScheduler
157 - (void)launchOperations
158 {
159 dispatch_sync(self.queue, ^{
160 if(self.halted) {
161 secnotice("ckkszonemodifier", "Halted; not launching operations");
162 return;
163 }
164
165 CKKSZoneModifyOperations* ops = self.pendingOperations;
166 if(!ops) {
167 secinfo("ckkszonemodifier", "No pending zone modification operations; quitting");
168 return;
169 }
170
171 if(self.inflightOperations && (![self.inflightOperations.zoneModificationOperation isFinished] ||
172 ![self.inflightOperations.zoneSubscriptionOperation isFinished])) {
173 secnotice("ckkszonemodifier", "Have in-flight zone modification operations, will retry later");
174
175 WEAKIFY(self);
176 CKKSResultOperation* retrigger = [CKKSResultOperation named:@"retry" withBlock:^{
177 STRONGIFY(self);
178 [self.cloudkitRetryAfter trigger];
179 }];
180 [retrigger addNullableDependency:self.inflightOperations.zoneModificationOperation];
181 [retrigger addNullableDependency:self.inflightOperations.zoneSubscriptionOperation];
182 [self.operationQueue addOperation:retrigger];
183 return;
184 }
185
186 self.pendingOperations = nil;
187 self.inflightOperations = ops;
188
189 CKDatabaseOperation<CKKSModifyRecordZonesOperation>* modifyZonesOperation = [self createModifyZonesOperation:ops];
190 CKDatabaseOperation<CKKSModifySubscriptionsOperation>* zoneSubscriptionOperation = [self createModifySubscriptionsOperation:ops];
191
192 [self.database addOperation:modifyZonesOperation];
193 if(zoneSubscriptionOperation) {
194 [self.database addOperation:zoneSubscriptionOperation];
195 }
196 });
197 }
198
199 - (CKDatabaseOperation<CKKSModifyRecordZonesOperation>*)createModifyZonesOperation:(CKKSZoneModifyOperations*)ops
200 {
201 secnotice("ckkszonemodifier", "Attempting to create zones %@, delete zones %@", ops.zonesToCreate, ops.zoneIDsToDelete);
202
203 CKDatabaseOperation<CKKSModifyRecordZonesOperation>* zoneModifyOperation = [[self.cloudKitClassDependencies.modifyRecordZonesOperationClass alloc] initWithRecordZonesToSave:ops.zonesToCreate recordZoneIDsToDelete:ops.zoneIDsToDelete];
204 [zoneModifyOperation linearDependencies:self.ckOperations];
205
206 zoneModifyOperation.configuration.automaticallyRetryNetworkFailures = NO;
207 zoneModifyOperation.configuration.discretionaryNetworkBehavior = CKOperationDiscretionaryNetworkBehaviorNonDiscretionary;
208 zoneModifyOperation.configuration.isCloudKitSupportOperation = YES;
209 zoneModifyOperation.database = self.database;
210 zoneModifyOperation.name = @"zone-creation-operation";
211 zoneModifyOperation.group = [CKOperationGroup CKKSGroupWithName:@"zone-creation"];;
212
213 // We will use the zoneCreationOperation operation in ops to signal completion
214 WEAKIFY(self);
215
216 zoneModifyOperation.modifyRecordZonesCompletionBlock = ^(NSArray<CKRecordZone *> *savedRecordZones,
217 NSArray<CKRecordZoneID *> *deletedRecordZoneIDs,
218 NSError *operationError) {
219 STRONGIFY(self);
220
221 if(operationError) {
222 secerror("ckkszonemodifier: Zone modification failed: %@", operationError);
223 [self inspectErrorForRetryAfter:operationError];
224
225 if ([self.reachabilityTracker isNetworkError:operationError]){
226 self.networkFailure = true;
227 }
228 }
229 secnotice("ckkszonemodifier", "created zones: %@", savedRecordZones);
230 secnotice("ckkszonemodifier", "deleted zones: %@", deletedRecordZoneIDs);
231
232 ops.savedRecordZones = savedRecordZones;
233 ops.deletedRecordZoneIDs = deletedRecordZoneIDs;
234 ops.zoneModificationOperation.error = operationError;
235
236 [self.operationQueue addOperation:ops.zoneModificationOperation];
237 };
238
239 if(self.networkFailure) {
240 secnotice("ckkszonemodifier", "Waiting for reachabilty before issuing zone creation");
241 [zoneModifyOperation addNullableDependency:self.reachabilityTracker.reachabilityDependency];
242 }
243
244 return zoneModifyOperation;
245 }
246
247 - (CKDatabaseOperation<CKKSModifySubscriptionsOperation>* _Nullable)createModifySubscriptionsOperation:(CKKSZoneModifyOperations*)ops
248 {
249 secnotice("ckkszonemodifier", "Attempting to subscribe to zones %@", ops.subscriptionsToSubscribe);
250
251 if(ops.subscriptionsToSubscribe.count == 0) {
252 [self.operationQueue addOperation: ops.zoneSubscriptionOperation];
253 return nil;
254 }
255
256 CKDatabaseOperation<CKKSModifySubscriptionsOperation>* zoneSubscriptionOperation = [[self.cloudKitClassDependencies.modifySubscriptionsOperationClass alloc] initWithSubscriptionsToSave:ops.subscriptionsToSubscribe subscriptionIDsToDelete:nil];
257 [zoneSubscriptionOperation linearDependencies:self.ckOperations];
258
259 zoneSubscriptionOperation.configuration.automaticallyRetryNetworkFailures = NO;
260 zoneSubscriptionOperation.configuration.discretionaryNetworkBehavior = CKOperationDiscretionaryNetworkBehaviorNonDiscretionary;
261 zoneSubscriptionOperation.configuration.isCloudKitSupportOperation = YES;
262 zoneSubscriptionOperation.database = self.database;
263 zoneSubscriptionOperation.name = @"zone-subscription-operation";
264
265 WEAKIFY(self);
266 zoneSubscriptionOperation.modifySubscriptionsCompletionBlock = ^(NSArray<CKSubscription *> * _Nullable savedSubscriptions,
267 NSArray<NSString *> * _Nullable deletedSubscriptionIDs,
268 NSError * _Nullable operationError) {
269 STRONGIFY(self);
270
271 if(operationError) {
272 secerror("ckkszonemodifier: Couldn't create cloudkit zone subscription; keychain syncing is severely degraded: %@", operationError);
273 [self inspectErrorForRetryAfter:operationError];
274
275 if ([self.reachabilityTracker isNetworkError:operationError]){
276 self.networkFailure = true;
277 }
278 }
279 secnotice("ckkszonemodifier", "Successfully subscribed to %@", savedSubscriptions);
280
281 ops.savedSubscriptions = savedSubscriptions;
282 ops.deletedSubscriptionIDs = deletedSubscriptionIDs;
283 ops.zoneSubscriptionOperation.error = operationError;
284
285 [self.operationQueue addOperation: ops.zoneSubscriptionOperation];
286 };
287
288 [zoneSubscriptionOperation addNullableDependency:ops.zoneModificationOperation];
289
290 if(self.networkFailure) {
291 secnotice("ckkszonemodifier", "Waiting for reachabilty before issuing zone subscription");
292 [zoneSubscriptionOperation addNullableDependency:self.reachabilityTracker.reachabilityDependency];
293 }
294
295 return zoneSubscriptionOperation;
296 }
297
298 - (void)inspectErrorForRetryAfter:(NSError*)ckerror
299 {
300 NSTimeInterval delay = CKRetryAfterSecondsForError(ckerror);
301 if(delay) {
302 uint64_t ns_delay = NSEC_PER_SEC * ((uint64_t) delay);
303 secnotice("ckkszonemodifier", "CK operation failed with rate-limit, scheduling delay for %.1f seconds: %@", delay, ckerror);
304 [self.cloudkitRetryAfter waitUntil:ns_delay];
305 }
306 }
307
308 @end
309
310 #endif // OCTAGON