2 * Copyright (c) 2016 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
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
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.
21 * @APPLE_LICENSE_HEADER_END@
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"
34 #include <securityd/SecItemServer.h>
35 #include <securityd/SecItemDb.h>
36 #include <Security/SecItemPriv.h>
37 #include <utilities/SecADWrapper.h>
38 #import "CKKSPowerCollection.h"
42 @interface CKKSOutgoingQueueOperation()
43 @property CKModifyRecordsOperation* modifyRecordsOperation;
46 @implementation CKKSOutgoingQueueOperation
48 - (instancetype)init {
49 if(self = [super init]) {
53 - (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks ckoperationGroup:(CKOperationGroup*)ckoperationGroup {
54 if(self = [super init]) {
56 _ckoperationGroup = ckoperationGroup;
58 [self addNullableDependency:ckks.viewSetupOperation];
59 [self addNullableDependency:ckks.holdOutgoingQueueOperation];
61 // Depend on all previous CKKSOutgoingQueueOperations
62 [self linearDependencies:ckks.outgoingQueueOperations];
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];
72 // Synchronous, on some thread. Get back on the CKKS queue for thread-safety.
73 __weak __typeof(self) weakSelf = self;
75 CKKSKeychainView* ckks = self.ckks;
77 ckkserror("ckksoutgoing", ckks, "no ckks object");
81 [ckks dispatchSyncWithAccountQueue: ^bool{
82 ckks.lastOutgoingQueueOperation = self;
84 ckksnotice("ckksoutgoing", ckks, "CKKSOutgoingQueueOperation cancelled, quitting");
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];
94 ckkserror("ckksoutgoing", ckks, "Error fetching outgoing queue records: %@", error);
99 ckksinfo("ckksoutgoing", ckks, "processing outgoing queue: %@", queueEntries);
101 NSMutableDictionary<CKRecordID*, CKRecord*>* recordsToSave = [[NSMutableDictionary alloc] init];
102 NSMutableSet<CKRecordID*>* oqesModified = [[NSMutableSet alloc] init];
103 NSMutableArray<CKRecordID *>* recordIDsToDelete = [[NSMutableArray alloc] init];
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;
111 ckkserror("ckksoutgoing", ckks, "Couldn't load current class keys: %@", error);
115 for(CKKSOutgoingQueueEntry* oqe in queueEntries) {
117 secdebug("ckksoutgoing", "CKKSOutgoingQueueOperation cancelled, quitting");
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.
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]) {
132 currentKeysToSave[SecCKKSKeyClassA] = currentClassAKeyPointer;
134 } else if([oqe.item.parentKeyUUID isEqualToString: currentClassCKeyPointer.currentKeyUUID]) {
136 currentKeysToSave[SecCKKSKeyClassC] = currentClassCKeyPointer;
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];
143 ckkserror("ckksoutgoing", ckks, "couldn't save oqe to database: %@", error);
147 needsReencrypt = true;
152 if([oqe.action isEqualToString: SecCKKSActionAdd]) {
153 CKRecord* record = [oqe.item CKRecordWithZoneID: ckks.zoneID];
154 recordsToSave[record.recordID] = record;
155 [oqesModified addObject: record.recordID];
157 [ckks _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateInFlight error:&error];
159 ckkserror("ckksoutgoing", ckks, "couldn't save state for CKKSOutgoingQueueEntry: %@", error);
163 } else if ([oqe.action isEqualToString: SecCKKSActionDelete]) {
164 [recordIDsToDelete addObject: [[CKRecordID alloc] initWithRecordName: oqe.item.uuid zoneID: ckks.zoneID]];
166 [ckks _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateInFlight error:&error];
168 ckkserror("ckksoutgoing", ckks, "couldn't save state for CKKSOutgoingQueueEntry: %@", error);
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];
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
178 ckkserror("ckksoutgoing", ckks, "update to a record that doesn't exist? %@", oqe.item.uuid);
180 CKRecord* record = [oqe.item CKRecordWithZoneID: ckks.zoneID];
181 recordsToSave[record.recordID] = record;
182 [oqesModified addObject: record.recordID];
184 [ckks _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateInFlight error:&error];
186 ckkserror("ckksoutgoing", ckks, "couldn't save state for CKKSOutgoingQueueEntry: %@", error);
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];
196 ckkserror("ckksoutgoing", ckks, "couldn't save oqe to database: %@", error);
200 needsReencrypt = true;
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];
208 [ckks _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateInFlight error:&error];
210 ckkserror("ckksoutgoing", ckks, "couldn't save state for CKKSOutgoingQueueEntry: %@", error);
217 ckksnotice("ckksoutgoing", ckks, "An item needs reencryption!");
219 CKKSReencryptOutgoingItemsOperation* op = [[CKKSReencryptOutgoingItemsOperation alloc] initWithCKKSKeychainView:ckks ckoperationGroup:self.ckoperationGroup];
220 [ckks scheduleOperation: op];
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);
232 self.itemsProcessed = recordsToSave.count;
234 NSBlockOperation* modifyComplete = [[NSBlockOperation alloc] init];
235 modifyComplete.name = @"modifyRecordsComplete";
236 [self dependOnBeforeGroupFinished: modifyComplete];
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;
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);
251 ckkserror("ckksoutgoing", ckks, "could not get current ego manifest to update");
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");
263 [strongCKKS dispatchSync: ^bool{
265 ckkserror("ckksoutgoing", strongCKKS, "error processing outgoing queue: %@", ckerror);
267 // Tell CKKS about any out-of-date records
268 [strongCKKS _onqueueCKWriteFailed:ckerror attemptedRecordsChanged:recordsToSave];
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];
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];
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];
290 // Quit the loop so we only do this once
293 // CKErrorServerRecordChanged on an item update means that we've been overwritten.
294 if([oqesModified containsObject:recordID]) {
295 [self _onqueueModifyRecordAsError:recordID recordError:recordError];
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.
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];
307 ckkserror("ckksoutgoing", strongCKKS, "Couldn't try to fetch an overwriting OQE: %@", error);
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];
314 ckkserror("ckksoutgoing", strongCKKS, "Couldn't delete in-flight OQE: %@", error);
317 [strongCKKS _onqueueChangeOutgoingQueueEntry:inflightOQE toState:SecCKKSStateNew error:&error];
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)}];
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];
333 strongSelf.error = error;
337 ckksnotice("ckksoutgoing", strongCKKS, "Completed processing outgoing queue");
338 NSError* error = NULL;
339 CKKSPowerCollection *plstats = [[CKKSPowerCollection alloc] init];
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];
347 ckkserror("ckksoutgoing", strongCKKS, "Couldn't update %@ in outgoingqueue: %@", record.recordID.recordName, error);
348 strongSelf.error = error;
351 CKKSMirrorEntry* ckme = [[CKKSMirrorEntry alloc] initWithCKRecord: record];
352 [ckme saveToDatabase: &error];
354 ckkserror("ckksoutgoing", strongCKKS, "Couldn't save %@ to ckmirror: %@", record.recordID.recordName, error);
355 strongSelf.error = error;
358 [plstats storedOQE:oqe];
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];
365 ckkserror("ckksoutgoing", strongCKKS, "Couldn't save %@ to currentkey: %@", record.recordID.recordName, error);
366 strongSelf.error = error;
369 } else if ([record.recordType isEqualToString:SecCKRecordDeviceStateType]) {
370 CKKSDeviceStateEntry* newcdse = [[CKKSDeviceStateEntry alloc] initWithCKRecord:record];
371 [newcdse saveToDatabase:&error];
373 ckkserror("ckksoutgoing", strongCKKS, "Couldn't save %@ to ckdevicestate: %@", record.recordID.recordName, error);
374 strongSelf.error = error;
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);
384 // Delete the deleted record IDs
385 for(CKRecordID* ckrecordID in deletedRecordIDs) {
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];
391 ckkserror("ckksoutgoing", strongCKKS, "Couldn't delete %@ from outgoingqueue: %@", ckrecordID.recordName, error);
392 strongSelf.error = error;
395 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase: ckrecordID.recordName zoneID:strongCKKS.zoneID error:&error];
396 [ckme deleteFromDatabase: &error];
398 ckkserror("ckksoutgoing", strongCKKS, "Couldn't delete %@ from ckmirror: %@", ckrecordID.recordName, error);
399 strongSelf.error = error;
402 [plstats deletedOQE:oqe];
407 if(strongSelf.error) {
408 ckkserror("ckksoutgoing", strongCKKS, "Operation failed; rolling back: %@", strongSelf.error);
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];
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;
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];
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");
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;
441 ckksinfo("ckksoutgoing", ckks, "Saving records %@ to CloudKit zone %@", recordsToSave, ckks.zoneID);
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);
451 self.modifyRecordsOperation.perRecordCompletionBlock = ^(CKRecord *record, NSError * _Nullable error) {
452 __strong __typeof(weakSelf) strongSelf = weakSelf;
453 __strong __typeof(strongSelf.ckks) blockCKKS = strongSelf.ckks;
456 ckksnotice("ckksoutgoing", blockCKKS, "Record upload successful for %@", record.recordID.recordName);
458 ckkserror("ckksoutgoing", blockCKKS, "error on row: %@ %@", record, error);
462 self.modifyRecordsOperation.modifyRecordsCompletionBlock = modifyRecordsCompletionBlock;
463 [self dependOnBeforeGroupFinished: self.modifyRecordsOperation];
464 [ckks.database addOperation: self.modifyRecordsOperation];
470 - (void)_onqueueModifyRecordAsError:(CKRecordID*)recordID recordError:(NSError*)itemerror {
471 CKKSKeychainView* ckks = self.ckks;
473 ckkserror("ckksoutgoing", ckks, "no CKKS object");
477 dispatch_assert_queue(ckks.queue);
479 NSError* error = nil;
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.
489 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry fromDatabase:recordID.recordName state: SecCKKSStateInFlight zoneID:ckks.zoneID error:&error];
490 [ckks _onqueueErrorOutgoingQueueEntry: oqe itemError: itemerror error:&error];
492 ckkserror("ckksoutgoing", ckks, "Couldn't set OQE %@ as error: %@", recordID.recordName, error);
500 - (void)_onqueueModifyAllRecordsAsReencrypt: (NSArray<CKRecordID*>*) recordIDs {
501 CKKSKeychainView* ckks = self.ckks;
503 ckkserror("ckksoutgoing", ckks, "no CKKS object");
507 dispatch_assert_queue(ckks.queue);
509 NSError* error = nil;
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.
518 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry fromDatabase:recordID.recordName state: SecCKKSStateInFlight zoneID:ckks.zoneID error:&error];
519 [ckks _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateReencrypt error:&error];
521 ckkserror("ckksoutgoing", ckks, "Couldn't set OQE %@ as reencrypt: %@", recordID.recordName, error);
528 SecADAddValueForScalarKey((__bridge CFStringRef) SecCKKSAggdItemReencryption, count);