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