]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSOutgoingQueueOperation.m
Security-58286.251.4.tar.gz
[apple/security.git] / keychain / ckks / CKKSOutgoingQueueOperation.m
1 /*
2 * Copyright (c) 2016 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 #if OCTAGON
25
26 #import <CloudKit/CloudKit.h>
27 #import <CloudKit/CloudKit_Private.h>
28
29 #import "CKKSKeychainView.h"
30 #import "CKKSCurrentKeyPointer.h"
31 #import "CKKSOutgoingQueueOperation.h"
32 #import "CKKSIncomingQueueEntry.h"
33 #import "CKKSItemEncrypter.h"
34 #import "CKKSOutgoingQueueEntry.h"
35 #import "CKKSReencryptOutgoingItemsOperation.h"
36 #import "CKKSManifest.h"
37 #import "CKKSAnalytics.h"
38
39 #include <securityd/SecItemServer.h>
40 #include <securityd/SecItemDb.h>
41 #include <Security/SecItemPriv.h>
42 #include <utilities/SecADWrapper.h>
43 #import "CKKSPowerCollection.h"
44
45 @interface CKKSOutgoingQueueOperation()
46 @property CKModifyRecordsOperation* modifyRecordsOperation;
47 @end
48
49 @implementation CKKSOutgoingQueueOperation
50
51 - (instancetype)init {
52 if(self = [super init]) {
53 }
54 return nil;
55 }
56 - (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks ckoperationGroup:(CKOperationGroup*)ckoperationGroup {
57 if(self = [super init]) {
58 _ckks = ckks;
59 _ckoperationGroup = ckoperationGroup;
60
61 [self addNullableDependency:ckks.holdOutgoingQueueOperation];
62
63 // Depend on all previous CKKSOutgoingQueueOperations
64 [self linearDependencies:ckks.outgoingQueueOperations];
65
66 // We also depend on the view being setup and the key hierarchy being reasonable
67 [self addNullableDependency:ckks.keyStateReadyDependency];
68 }
69 return self;
70 }
71
72 - (void) groupStart {
73 // Synchronous, on some thread. Get back on the CKKS queue for thread-safety.
74 __weak __typeof(self) weakSelf = self;
75
76 CKKSKeychainView* ckks = self.ckks;
77 if(!ckks) {
78 ckkserror("ckksoutgoing", ckks, "no ckks object");
79 return;
80 }
81
82 [ckks dispatchSync: ^bool{
83 ckks.lastOutgoingQueueOperation = self;
84 if(self.cancelled) {
85 ckksnotice("ckksoutgoing", ckks, "CKKSOutgoingQueueOperation cancelled, quitting");
86 return false;
87 }
88
89 NSError* error = nil;
90
91
92 // We only actually care about queue items in the 'new' state
93 NSArray<CKKSOutgoingQueueEntry*> * queueEntries = [CKKSOutgoingQueueEntry fetch:SecCKKSOutgoingQueueItemsAtOnce state: SecCKKSStateNew zoneID:ckks.zoneID error:&error];
94
95 if(error != nil) {
96 ckkserror("ckksoutgoing", ckks, "Error fetching outgoing queue records: %@", error);
97 self.error = error;
98 return false;
99 }
100
101 [CKKSPowerCollection CKKSPowerEvent:kCKKSPowerEventOutgoingQueue zone:ckks.zoneName count:[queueEntries count]];
102
103 ckksinfo("ckksoutgoing", ckks, "processing outgoing queue: %@", queueEntries);
104
105 NSMutableDictionary<CKRecordID*, CKRecord*>* recordsToSave = [[NSMutableDictionary alloc] init];
106 NSMutableSet<CKRecordID*>* recordIDsModified = [[NSMutableSet alloc] init];
107 NSMutableSet<CKKSOutgoingQueueEntry*>*oqesModified = [[NSMutableSet alloc] init];
108 NSMutableArray<CKRecordID *>* recordIDsToDelete = [[NSMutableArray alloc] init];
109
110 CKKSCurrentKeyPointer* currentClassAKeyPointer = [CKKSCurrentKeyPointer fromDatabase: SecCKKSKeyClassA zoneID:ckks.zoneID error: &error];
111 CKKSCurrentKeyPointer* currentClassCKeyPointer = [CKKSCurrentKeyPointer fromDatabase: SecCKKSKeyClassC zoneID:ckks.zoneID error: &error];
112 NSMutableDictionary<CKKSKeyClass*, CKKSCurrentKeyPointer*>* currentKeysToSave = [[NSMutableDictionary alloc] init];
113 bool needsReencrypt = false;
114
115 if(error != nil) {
116 ckkserror("ckksoutgoing", ckks, "Couldn't load current class keys: %@", error);
117 return false;
118 }
119
120 for(CKKSOutgoingQueueEntry* oqe in queueEntries) {
121 if(self.cancelled) {
122 secdebug("ckksoutgoing", "CKKSOutgoingQueueOperation cancelled, quitting");
123 return false;
124 }
125
126 CKKSOutgoingQueueEntry* inflight = [CKKSOutgoingQueueEntry tryFromDatabase: oqe.uuid state:SecCKKSStateInFlight zoneID:ckks.zoneID error: &error];
127 if(!error && inflight) {
128 // There is an inflight request with this UUID. Leave this request in-queue until CloudKit returns and we resolve the inflight request.
129 continue;
130 }
131
132 // If this item is not a delete, check the encryption status of this item.
133 if(![oqe.action isEqualToString: SecCKKSActionDelete]) {
134 // Check if this item is encrypted under a current key
135 if([oqe.item.parentKeyUUID isEqualToString: currentClassAKeyPointer.currentKeyUUID]) {
136 // Excellent.
137 currentKeysToSave[SecCKKSKeyClassA] = currentClassAKeyPointer;
138
139 } else if([oqe.item.parentKeyUUID isEqualToString: currentClassCKeyPointer.currentKeyUUID]) {
140 // Works for us!
141 currentKeysToSave[SecCKKSKeyClassC] = currentClassCKeyPointer;
142
143 } else {
144 // This item is encrypted under an old key. Set it up for reencryption and move on.
145 ckksnotice("ckksoutgoing", ckks, "Item's encryption key (%@ %@) is neither %@ or %@", oqe, oqe.item.parentKeyUUID, currentClassAKeyPointer, currentClassCKeyPointer);
146 [ckks _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateReencrypt error:&error];
147 if(error) {
148 ckkserror("ckksoutgoing", ckks, "couldn't save oqe to database: %@", error);
149 self.error = error;
150 error = nil;
151 }
152 needsReencrypt = true;
153 continue;
154 }
155 }
156
157 if([oqe.action isEqualToString: SecCKKSActionAdd]) {
158 CKRecord* record = [oqe.item CKRecordWithZoneID: ckks.zoneID];
159 recordsToSave[record.recordID] = record;
160 [recordIDsModified addObject: record.recordID];
161 [oqesModified addObject:oqe];
162
163 [ckks _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateInFlight error:&error];
164 if(error) {
165 ckkserror("ckksoutgoing", ckks, "couldn't save state for CKKSOutgoingQueueEntry: %@", error);
166 self.error = error;
167 }
168
169 } else if ([oqe.action isEqualToString: SecCKKSActionDelete]) {
170 CKRecordID* recordIDToDelete = [[CKRecordID alloc] initWithRecordName: oqe.item.uuid zoneID: ckks.zoneID];
171 [recordIDsToDelete addObject: recordIDToDelete];
172 [recordIDsModified addObject: recordIDToDelete];
173 [oqesModified addObject:oqe];
174
175 [ckks _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateInFlight error:&error];
176 if(error) {
177 ckkserror("ckksoutgoing", ckks, "couldn't save state for CKKSOutgoingQueueEntry: %@", error);
178 }
179
180 } else if ([oqe.action isEqualToString: SecCKKSActionModify]) {
181 // Load the existing item from the ckmirror.
182 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase: oqe.item.uuid zoneID:ckks.zoneID error:&error];
183 if(!ckme) {
184 // This is a problem: we have an update to an item that doesn't exist.
185 // Either: an Add operation we launched failed due to a CloudKit error (conflict?) and this is a follow-on update
186 // Or: ?
187 ckkserror("ckksoutgoing", ckks, "update to a record that doesn't exist? %@", oqe.item.uuid);
188 // treat as an add.
189 CKRecord* record = [oqe.item CKRecordWithZoneID: ckks.zoneID];
190 recordsToSave[record.recordID] = record;
191 [recordIDsModified addObject: record.recordID];
192 [oqesModified addObject:oqe];
193
194 [ckks _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateInFlight error:&error];
195 if(error) {
196 ckkserror("ckksoutgoing", ckks, "couldn't save state for CKKSOutgoingQueueEntry: %@", error);
197 self.error = error;
198 }
199 } else {
200 if(![oqe.item.storedCKRecord.recordChangeTag isEqual: ckme.item.storedCKRecord.recordChangeTag]) {
201 // The mirror entry has updated since this item was created. If we proceed, we might end up with
202 // a badly-authenticated record.
203 ckksnotice("ckksoutgoing", ckks, "Record (%@)'s change tag doesn't match ckmirror's change tag, reencrypting", oqe);
204 [ckks _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateReencrypt error:&error];
205 if(error) {
206 ckkserror("ckksoutgoing", ckks, "couldn't save oqe to database: %@", error);
207 self.error = error;
208 error = nil;
209 }
210 needsReencrypt = true;
211 continue;
212 }
213 // Grab the old ckrecord and update it
214 CKRecord* record = [oqe.item updateCKRecord: ckme.item.storedCKRecord zoneID: ckks.zoneID];
215 recordsToSave[record.recordID] = record;
216 [recordIDsModified addObject: record.recordID];
217 [oqesModified addObject:oqe];
218
219 [ckks _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateInFlight error:&error];
220 if(error) {
221 ckkserror("ckksoutgoing", ckks, "couldn't save state for CKKSOutgoingQueueEntry: %@", error);
222 }
223 }
224 }
225 }
226
227 if(needsReencrypt) {
228 ckksnotice("ckksoutgoing", ckks, "An item needs reencryption!");
229
230 CKKSReencryptOutgoingItemsOperation* op = [[CKKSReencryptOutgoingItemsOperation alloc] initWithCKKSKeychainView:ckks ckoperationGroup:self.ckoperationGroup];
231 [ckks scheduleOperation: op];
232 }
233
234 if([recordsToSave count] == 0 && [recordIDsToDelete count] == 0) {
235 // Nothing to do! exit.
236 ckksnotice("ckksoutgoing", ckks, "Nothing in outgoing queue to process");
237 if(self.ckoperationGroup) {
238 ckksnotice("ckksoutgoing", ckks, "End of operation group: %@", self.ckoperationGroup);
239 }
240 return true;
241 }
242
243 self.itemsProcessed = recordsToSave.count;
244
245 NSBlockOperation* modifyComplete = [[NSBlockOperation alloc] init];
246 modifyComplete.name = @"modifyRecordsComplete";
247 [self dependOnBeforeGroupFinished: modifyComplete];
248
249 if ([CKKSManifest shouldSyncManifests]) {
250 if (ckks.egoManifest) {
251 [ckks.egoManifest updateWithNewOrChangedRecords:recordsToSave.allValues deletedRecordIDs:recordIDsToDelete];
252 for(CKRecord* record in [ckks.egoManifest allCKRecordsWithZoneID:ckks.zoneID]) {
253 recordsToSave[record.recordID] = record;
254 }
255 NSError* saveError = nil;
256 if (![ckks.egoManifest saveToDatabase:&saveError]) {
257 self.error = saveError;
258 ckkserror("ckksoutgoing", ckks, "could not save ego manifest with error: %@", saveError);
259 }
260 }
261 else {
262 ckkserror("ckksoutgoing", ckks, "could not get current ego manifest to update");
263 }
264 }
265
266 void (^modifyRecordsCompletionBlock)(NSArray<CKRecord*>*, NSArray<CKRecordID*>*, NSError*) = ^(NSArray<CKRecord *> *savedRecords, NSArray<CKRecordID *> *deletedRecordIDs, NSError *ckerror) {
267 __strong __typeof(weakSelf) strongSelf = weakSelf;
268 __strong __typeof(strongSelf.ckks) strongCKKS = strongSelf.ckks;
269 if(!strongSelf || !strongCKKS) {
270 ckkserror("ckksoutgoing", strongCKKS, "received callback for released object");
271 return;
272 }
273
274 CKKSAnalytics* logger = [CKKSAnalytics logger];
275
276 [strongCKKS dispatchSyncWithAccountKeys: ^bool{
277 if(ckerror) {
278 ckkserror("ckksoutgoing", strongCKKS, "error processing outgoing queue: %@", ckerror);
279
280 [logger logRecoverableError:ckerror
281 forEvent:CKKSEventProcessOutgoingQueue
282 inView:strongCKKS
283 withAttributes:NULL];
284
285 // Tell CKKS about any out-of-date records
286 [strongCKKS _onqueueCKWriteFailed:ckerror attemptedRecordsChanged:recordsToSave];
287
288 // Check if these are due to key records being out of date. We'll see a CKErrorBatchRequestFailed, with a bunch of errors inside
289 if([ckerror.domain isEqualToString:CKErrorDomain] && (ckerror.code == CKErrorPartialFailure)) {
290 NSMutableDictionary<CKRecordID*, NSError*>* failedRecords = ckerror.userInfo[CKPartialErrorsByItemIDKey];
291
292 bool askForReencrypt = false;
293
294 if([strongSelf _onqueueIsErrorBadEtagOnKeyPointersOnly:ckerror]) {
295 // The current key pointers have updated without our knowledge, so CloudKit failed this operation. Mark all records as 'needs reencryption' and kick that off.
296 ckksnotice("ckksoutgoing", strongCKKS, "Error is simply due to current key pointers changing; marking all records as 'needs reencrypt'");
297 [strongSelf _onqueueModifyAllRecords:failedRecords.allKeys as:SecCKKSStateReencrypt];
298 askForReencrypt = true;
299 } else {
300 // Iterate all failures, and reset each item
301 for(CKRecordID* recordID in failedRecords) {
302 NSError* recordError = failedRecords[recordID];
303
304 ckksnotice("ckksoutgoing", strongCKKS, "failed record: %@ %@", recordID, recordError);
305
306 if(recordError.code == CKErrorServerRecordChanged) {
307 if([recordID.recordName isEqualToString: SecCKKSKeyClassA] ||
308 [recordID.recordName isEqualToString: SecCKKSKeyClassC]) {
309 // Note that _onqueueCKWriteFailed is responsible for kicking the key state machine, so we don't need to do it here.
310 askForReencrypt = true;
311 } else {
312 // CKErrorServerRecordChanged on an item update means that we've been overwritten.
313 if([recordIDsModified containsObject:recordID]) {
314 [strongSelf _onqueueModifyRecordAsError:recordID recordError:recordError];
315 }
316 }
317 } else if(recordError.code == CKErrorBatchRequestFailed) {
318 // Also fine. This record only didn't succeed because something else failed.
319 // OQEs should be placed back into the 'new' state, unless they've been overwritten by a new OQE. Other records should be ignored.
320
321 if([recordIDsModified containsObject:recordID]) {
322 NSError* localerror = nil;
323 CKKSOutgoingQueueEntry* inflightOQE = [CKKSOutgoingQueueEntry tryFromDatabase:recordID.recordName state:SecCKKSStateInFlight zoneID:recordID.zoneID error:&localerror];
324 [strongCKKS _onqueueChangeOutgoingQueueEntry:inflightOQE toState:SecCKKSStateNew error:&localerror];
325 if(localerror) {
326 ckkserror("ckksoutgoing", strongCKKS, "Couldn't clean up outgoing queue entry: %@", localerror);
327 }
328 }
329
330 } else {
331 // Some unknown error occurred on this record. If it's an OQE, move it to the error state.
332 ckkserror("ckksoutgoing", strongCKKS, "Unknown error on row: %@ %@", recordID, recordError);
333 if([recordIDsModified containsObject:recordID]) {
334 [strongSelf _onqueueModifyRecordAsError:recordID recordError:recordError];
335 }
336 }
337 }
338 }
339
340 if(askForReencrypt) {
341 // This will wait for the key hierarchy to become 'ready'
342 ckkserror("ckksoutgoing", strongCKKS, "Starting new Reencrypt items operation");
343 CKKSReencryptOutgoingItemsOperation* op = [[CKKSReencryptOutgoingItemsOperation alloc] initWithCKKSKeychainView:strongCKKS
344 ckoperationGroup:strongSelf.ckoperationGroup];
345 [strongCKKS scheduleOperation: op];
346 }
347 } else {
348 // Some non-partial error occured. We should place all "inflight" OQEs back into the outgoing queue.
349 ckksnotice("ckks", strongCKKS, "Error is scary: putting all inflight OQEs back into state 'new'");
350 [strongSelf _onqueueModifyAllRecords:[recordIDsModified allObjects] as:SecCKKSStateNew];
351 }
352
353 strongSelf.error = ckerror;
354 return true;
355 }
356
357 ckksnotice("ckksoutgoing", strongCKKS, "Completed processing outgoing queue (%d modifications, %d deletions)", (int)savedRecords.count, (int)deletedRecordIDs.count);
358 NSError* error = NULL;
359 CKKSPowerCollection *plstats = [[CKKSPowerCollection alloc] init];
360
361 for(CKRecord* record in savedRecords) {
362 // Save the item records
363 if([record.recordType isEqualToString: SecCKRecordItemType]) {
364 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry fromDatabase: record.recordID.recordName state: SecCKKSStateInFlight zoneID:strongCKKS.zoneID error:&error];
365 [strongCKKS _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateDeleted error:&error];
366 if(error) {
367 ckkserror("ckksoutgoing", strongCKKS, "Couldn't update %@ in outgoingqueue: %@", record.recordID.recordName, error);
368 strongSelf.error = error;
369 }
370 error = nil;
371 CKKSMirrorEntry* ckme = [[CKKSMirrorEntry alloc] initWithCKRecord: record];
372 [ckme saveToDatabase: &error];
373 if(error) {
374 ckkserror("ckksoutgoing", strongCKKS, "Couldn't save %@ to ckmirror: %@", record.recordID.recordName, error);
375 strongSelf.error = error;
376 }
377
378 [plstats storedOQE:oqe];
379
380 // And the CKCurrentKeyRecords (do we need to do this? Will the server update the change tag on a save which saves nothing?)
381 } else if([record.recordType isEqualToString: SecCKRecordCurrentKeyType]) {
382 CKKSCurrentKeyPointer* currentkey = [[CKKSCurrentKeyPointer alloc] initWithCKRecord: record];
383 [currentkey saveToDatabase: &error];
384 if(error) {
385 ckkserror("ckksoutgoing", strongCKKS, "Couldn't save %@ to currentkey: %@", record.recordID.recordName, error);
386 strongSelf.error = error;
387 }
388
389 } else if ([record.recordType isEqualToString:SecCKRecordDeviceStateType]) {
390 CKKSDeviceStateEntry* newcdse = [[CKKSDeviceStateEntry alloc] initWithCKRecord:record];
391 [newcdse saveToDatabase:&error];
392 if(error) {
393 ckkserror("ckksoutgoing", strongCKKS, "Couldn't save %@ to ckdevicestate: %@", record.recordID.recordName, error);
394 strongSelf.error = error;
395 }
396
397 } else if ([record.recordType isEqualToString:SecCKRecordManifestType]) {
398
399 } else if (![record.recordType isEqualToString:SecCKRecordManifestLeafType]) {
400 ckkserror("ckksoutgoing", strongCKKS, "unknown record type in results: %@", record);
401 }
402 }
403
404 // Delete the deleted record IDs
405 for(CKRecordID* ckrecordID in deletedRecordIDs) {
406
407 NSError* error = nil;
408 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry fromDatabase: ckrecordID.recordName state: SecCKKSStateInFlight zoneID:strongCKKS.zoneID error:&error];
409 [strongCKKS _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateDeleted error:&error];
410 if(error) {
411 ckkserror("ckksoutgoing", strongCKKS, "Couldn't delete %@ from outgoingqueue: %@", ckrecordID.recordName, error);
412 strongSelf.error = error;
413 }
414 error = nil;
415 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase: ckrecordID.recordName zoneID:strongCKKS.zoneID error:&error];
416 [ckme deleteFromDatabase: &error];
417 if(error) {
418 ckkserror("ckksoutgoing", strongCKKS, "Couldn't delete %@ from ckmirror: %@", ckrecordID.recordName, error);
419 strongSelf.error = error;
420 }
421
422 [plstats deletedOQE:oqe];
423 }
424
425 [plstats commit];
426
427 if(strongSelf.error) {
428 ckkserror("ckksoutgoing", strongCKKS, "Operation failed; rolling back: %@", strongSelf.error);
429 [logger logRecoverableError:strongSelf.error
430 forEvent:CKKSEventProcessOutgoingQueue
431 inView:strongCKKS
432 withAttributes:NULL];
433 return false;
434 } else {
435 [logger logSuccessForEvent:CKKSEventProcessOutgoingQueue inView:strongCKKS];
436 }
437 return true;
438 }];
439
440 [strongSelf.operationQueue addOperation: modifyComplete];
441 // Kick off another queue process. We expect it to exit instantly, but who knows!
442 // If we think the network is iffy, though, wait for it to come back
443 CKKSResultOperation* possibleNetworkDependency = nil;
444 CKKSReachabilityTracker* reachabilityTracker = strongCKKS.reachabilityTracker;
445 if(ckerror && [reachabilityTracker isNetworkError:ckerror]) {
446 possibleNetworkDependency = reachabilityTracker.reachabilityDependency;
447 }
448
449 [strongCKKS processOutgoingQueueAfter:possibleNetworkDependency ckoperationGroup:strongSelf.ckoperationGroup];
450 };
451
452 ckksinfo("ckksoutgoing", ckks, "Current keys to update: %@", currentKeysToSave);
453 for(CKKSCurrentKeyPointer* keypointer in currentKeysToSave.allValues) {
454 CKRecord* record = [keypointer CKRecordWithZoneID: ckks.zoneID];
455 recordsToSave[record.recordID] = record;
456 }
457
458 // Piggyback on this operation to update our device state
459 NSError* cdseError = nil;
460 CKKSDeviceStateEntry* cdse = [ckks _onqueueCurrentDeviceStateEntry:&cdseError];
461 CKRecord* cdseRecord = [cdse CKRecordWithZoneID: ckks.zoneID];
462 if(cdseError) {
463 ckkserror("ckksoutgoing", ckks, "Can't make current device state: %@", cdseError);
464 } else if(!cdseRecord) {
465 ckkserror("ckksoutgoing", ckks, "Can't make current device state cloudkit record, but no reason why");
466 } else {
467 // Add the CDSE to the outgoing records
468 // TODO: maybe only do this every few hours?
469 ckksnotice("ckksoutgoing", ckks, "Updating device state: %@", cdse);
470 recordsToSave[cdseRecord.recordID] = cdseRecord;
471 }
472
473 ckksinfo("ckksoutgoing", ckks, "Saving records %@ to CloudKit zone %@", recordsToSave, ckks.zoneID);
474
475 self.modifyRecordsOperation = [[CKModifyRecordsOperation alloc] initWithRecordsToSave:recordsToSave.allValues recordIDsToDelete:recordIDsToDelete];
476 self.modifyRecordsOperation.atomic = TRUE;
477
478 // Until <rdar://problem/38725728> Changes to discretionary-ness (explicit or derived from QoS) should be "live", all requests should be nondiscretionary
479 self.modifyRecordsOperation.configuration.automaticallyRetryNetworkFailures = NO;
480 self.modifyRecordsOperation.configuration.discretionaryNetworkBehavior = CKOperationDiscretionaryNetworkBehaviorNonDiscretionary;
481
482 self.modifyRecordsOperation.savePolicy = CKRecordSaveIfServerRecordUnchanged;
483 self.modifyRecordsOperation.group = self.ckoperationGroup;
484 ckksnotice("ckksoutgoing", ckks, "QoS: %d; operation group is %@", (int)self.modifyRecordsOperation.qualityOfService, self.modifyRecordsOperation.group);
485 ckksnotice("ckksoutgoing", ckks, "Beginning upload for %@ %@", recordsToSave.allKeys, recordIDsToDelete);
486
487 self.modifyRecordsOperation.perRecordCompletionBlock = ^(CKRecord *record, NSError * _Nullable error) {
488 __strong __typeof(weakSelf) strongSelf = weakSelf;
489 __strong __typeof(strongSelf.ckks) blockCKKS = strongSelf.ckks;
490
491 if(!error) {
492 ckksnotice("ckksoutgoing", blockCKKS, "Record upload successful for %@ (%@)", record.recordID.recordName, record.recordChangeTag);
493 } else {
494 ckkserror("ckksoutgoing", blockCKKS, "error on row: %@ %@", error, record);
495 }
496 };
497
498 self.modifyRecordsOperation.modifyRecordsCompletionBlock = modifyRecordsCompletionBlock;
499 [self dependOnBeforeGroupFinished: self.modifyRecordsOperation];
500 [ckks.database addOperation: self.modifyRecordsOperation];
501
502 return true;
503 }];
504 }
505
506 - (void)_onqueueModifyRecordAsError:(CKRecordID*)recordID recordError:(NSError*)itemerror {
507 CKKSKeychainView* ckks = self.ckks;
508 if(!ckks) {
509 ckkserror("ckksoutgoing", ckks, "no CKKS object");
510 return;
511 }
512
513 dispatch_assert_queue(ckks.queue);
514
515 NSError* error = nil;
516 uint64_t count = 0;
517
518 // At this stage, cloudkit doesn't give us record types
519 if([recordID.recordName isEqualToString: SecCKKSKeyClassA] ||
520 [recordID.recordName isEqualToString: SecCKKSKeyClassC] ||
521 [recordID.recordName hasPrefix:@"Manifest:-:"] ||
522 [recordID.recordName hasPrefix:@"ManifestLeafRecord:-:"]) {
523 // Nothing to do here. We need a whole key refetch and synchronize.
524 } else {
525 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry fromDatabase:recordID.recordName state: SecCKKSStateInFlight zoneID:ckks.zoneID error:&error];
526 [ckks _onqueueErrorOutgoingQueueEntry: oqe itemError: itemerror error:&error];
527 if(error) {
528 ckkserror("ckksoutgoing", ckks, "Couldn't set OQE %@ as error: %@", recordID.recordName, error);
529 self.error = error;
530 }
531 count ++;
532 }
533 }
534
535
536 - (void)_onqueueModifyAllRecords:(NSArray<CKRecordID*>*)recordIDs as:(CKKSItemState*)state {
537 CKKSKeychainView* ckks = self.ckks;
538 if(!ckks) {
539 ckkserror("ckksoutgoing", ckks, "no CKKS object");
540 return;
541 }
542
543 dispatch_assert_queue(ckks.queue);
544
545 NSError* error = nil;
546 uint64_t count = 0;
547
548 for(CKRecordID* recordID in recordIDs) {
549 // At this stage, cloudkit doesn't give us record types
550 if([recordID.recordName isEqualToString: SecCKKSKeyClassA] ||
551 [recordID.recordName isEqualToString: SecCKKSKeyClassC]) {
552 // Nothing to do here. We need a whole key refetch and synchronize.
553 } else {
554 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry fromDatabase:recordID.recordName state: SecCKKSStateInFlight zoneID:ckks.zoneID error:&error];
555 [ckks _onqueueChangeOutgoingQueueEntry:oqe toState:state error:&error];
556 if(error) {
557 ckkserror("ckksoutgoing", ckks, "Couldn't set OQE %@ as %@: %@", recordID.recordName, state, error);
558 self.error = error;
559 }
560 count ++;
561 }
562 }
563
564 if([state isEqualToString:SecCKKSStateReencrypt]) {
565 SecADAddValueForScalarKey((__bridge CFStringRef) SecCKKSAggdItemReencryption, count);
566 }
567 }
568
569 - (bool)_onqueueIsErrorBadEtagOnKeyPointersOnly:(NSError*)ckerror {
570 bool anyOtherErrors = false;
571
572 if([ckerror.domain isEqualToString:CKErrorDomain] && (ckerror.code == CKErrorPartialFailure)) {
573 NSMutableDictionary<CKRecordID*, NSError*>* failedRecords = ckerror.userInfo[CKPartialErrorsByItemIDKey];
574
575 for(CKRecordID* recordID in failedRecords) {
576 NSError* recordError = failedRecords[recordID];
577
578 if(recordError.code == CKErrorServerRecordChanged) {
579 if([recordID.recordName isEqualToString: SecCKKSKeyClassA] ||
580 [recordID.recordName isEqualToString: SecCKKSKeyClassC]) {
581 // this is fine!
582 } else {
583 // Some record other than the key pointers changed.
584 anyOtherErrors |= true;
585 break;
586 }
587 } else {
588 // Some other error than ServerRecordChanged
589 anyOtherErrors |= true;
590 break;
591 }
592 }
593 }
594
595 return !anyOtherErrors;
596 }
597
598 @end;
599
600 #endif