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