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 dispatchSyncWithAccountKeys: ^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 // Note that _onqueueCKWriteFailed is responsible for kicking the key state machine, so we don't need to do it here.
284 // This will wait for the key hierarchy to become 'ready'
285 CKKSReencryptOutgoingItemsOperation* op = [[CKKSReencryptOutgoingItemsOperation alloc] initWithCKKSKeychainView:strongCKKS ckoperationGroup:strongSelf.ckoperationGroup];
286 [strongCKKS scheduleOperation: op];
288 // Quit the loop so we only do this once
291 // CKErrorServerRecordChanged on an item update means that we've been overwritten.
292 if([oqesModified containsObject:recordID]) {
293 [strongSelf _onqueueModifyRecordAsError:recordID recordError:recordError];
296 } else if(recordError.code == CKErrorBatchRequestFailed) {
297 // Also fine. This record only didn't succeed because something else failed.
298 // OQEs should be placed back into the 'new' state, unless they've been overwritten by a new OQE. Other records should be ignored.
300 if([oqesModified containsObject:recordID]) {
301 NSError* error = nil;
302 CKKSOutgoingQueueEntry* inflightOQE = [CKKSOutgoingQueueEntry tryFromDatabase:recordID.recordName state:SecCKKSStateInFlight zoneID:recordID.zoneID error:&error];
303 CKKSOutgoingQueueEntry* newOQE = [CKKSOutgoingQueueEntry tryFromDatabase:recordID.recordName state:SecCKKSStateNew zoneID:recordID.zoneID error:&error];
305 ckkserror("ckksoutgoing", strongCKKS, "Couldn't try to fetch an overwriting OQE: %@", error);
309 ckksnotice("ckksoutgoing", strongCKKS, "New modification has come in behind failed change for %@; dropping failed change", inflightOQE);
310 [strongCKKS _onqueueChangeOutgoingQueueEntry:inflightOQE toState:SecCKKSStateDeleted error:&error];
312 ckkserror("ckksoutgoing", strongCKKS, "Couldn't delete in-flight OQE: %@", error);
315 [strongCKKS _onqueueChangeOutgoingQueueEntry:inflightOQE toState:SecCKKSStateNew error:&error];
319 } else if ([recordID.recordName hasPrefix:@"Manifest:-:"] || [recordID.recordName hasPrefix:@"ManifestLeafRecord:-:"]) {
320 [[CKKSAnalyticsLogger logger] logSoftFailureForEventNamed:@"ManifestUpload" withAttributes:@{CKKSManifestZoneKey : strongCKKS.zoneID.zoneName, CKKSManifestSignerIDKey : strongCKKS.egoManifest.signerID, CKKSManifestGenCountKey : @(strongCKKS.egoManifest.generationCount)}];
322 // Some unknown error occurred on this record. If it's an OQE, move it to the error state.
323 ckkserror("ckksoutgoing", strongCKKS, "Unknown error on row: %@ %@", recordID, recordError);
324 if([oqesModified containsObject:recordID]) {
325 [strongSelf _onqueueModifyRecordAsError:recordID recordError:recordError];
331 strongSelf.error = error;
335 ckksnotice("ckksoutgoing", strongCKKS, "Completed processing outgoing queue");
336 NSError* error = NULL;
337 CKKSPowerCollection *plstats = [[CKKSPowerCollection alloc] init];
339 for(CKRecord* record in savedRecords) {
340 // Save the item records
341 if([record.recordType isEqualToString: SecCKRecordItemType]) {
342 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry fromDatabase: record.recordID.recordName state: SecCKKSStateInFlight zoneID:strongCKKS.zoneID error:&error];
343 [strongCKKS _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateDeleted error:&error];
345 ckkserror("ckksoutgoing", strongCKKS, "Couldn't update %@ in outgoingqueue: %@", record.recordID.recordName, error);
346 strongSelf.error = error;
349 CKKSMirrorEntry* ckme = [[CKKSMirrorEntry alloc] initWithCKRecord: record];
350 [ckme saveToDatabase: &error];
352 ckkserror("ckksoutgoing", strongCKKS, "Couldn't save %@ to ckmirror: %@", record.recordID.recordName, error);
353 strongSelf.error = error;
356 [plstats storedOQE:oqe];
358 // And the CKCurrentKeyRecords (do we need to do this? Will the server update the change tag on a save which saves nothing?)
359 } else if([record.recordType isEqualToString: SecCKRecordCurrentKeyType]) {
360 CKKSCurrentKeyPointer* currentkey = [[CKKSCurrentKeyPointer alloc] initWithCKRecord: record];
361 [currentkey saveToDatabase: &error];
363 ckkserror("ckksoutgoing", strongCKKS, "Couldn't save %@ to currentkey: %@", record.recordID.recordName, error);
364 strongSelf.error = error;
367 } else if ([record.recordType isEqualToString:SecCKRecordDeviceStateType]) {
368 CKKSDeviceStateEntry* newcdse = [[CKKSDeviceStateEntry alloc] initWithCKRecord:record];
369 [newcdse saveToDatabase:&error];
371 ckkserror("ckksoutgoing", strongCKKS, "Couldn't save %@ to ckdevicestate: %@", record.recordID.recordName, error);
372 strongSelf.error = error;
375 } else if ([record.recordType isEqualToString:SecCKRecordManifestType]) {
376 [[CKKSAnalyticsLogger logger] logSuccessForEventNamed:@"ManifestUpload"];
377 } else if (![record.recordType isEqualToString:SecCKRecordManifestLeafType]) {
378 ckkserror("ckksoutgoing", strongCKKS, "unknown record type in results: %@", record);
382 // Delete the deleted record IDs
383 for(CKRecordID* ckrecordID in deletedRecordIDs) {
385 NSError* error = nil;
386 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry fromDatabase: ckrecordID.recordName state: SecCKKSStateInFlight zoneID:strongCKKS.zoneID error:&error];
387 [strongCKKS _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateDeleted error:&error];
389 ckkserror("ckksoutgoing", strongCKKS, "Couldn't delete %@ from outgoingqueue: %@", ckrecordID.recordName, error);
390 strongSelf.error = error;
393 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase: ckrecordID.recordName zoneID:strongCKKS.zoneID error:&error];
394 [ckme deleteFromDatabase: &error];
396 ckkserror("ckksoutgoing", strongCKKS, "Couldn't delete %@ from ckmirror: %@", ckrecordID.recordName, error);
397 strongSelf.error = error;
400 [plstats deletedOQE:oqe];
405 if(strongSelf.error) {
406 ckkserror("ckksoutgoing", strongCKKS, "Operation failed; rolling back: %@", strongSelf.error);
413 [strongSelf.operationQueue addOperation: modifyComplete];
414 // Kick off another queue process. We expect it to exit instantly, but who knows!
415 [strongCKKS processOutgoingQueue:strongSelf.ckoperationGroup];
418 ckksinfo("ckksoutgoing", ckks, "Current keys to update: %@", currentKeysToSave);
419 for(CKKSCurrentKeyPointer* keypointer in currentKeysToSave.allValues) {
420 CKRecord* record = [keypointer CKRecordWithZoneID: ckks.zoneID];
421 recordsToSave[record.recordID] = record;
424 // Piggyback on this operation to update our device state
425 NSError* cdseError = nil;
426 CKKSDeviceStateEntry* cdse = [ckks _onqueueCurrentDeviceStateEntry:&cdseError];
427 CKRecord* cdseRecord = [cdse CKRecordWithZoneID: ckks.zoneID];
429 ckkserror("ckksoutgoing", ckks, "Can't make current device state: %@", cdseError);
430 } else if(!cdseRecord) {
431 ckkserror("ckksoutgoing", ckks, "Can't make current device state cloudkit record, but no reason why");
433 // Add the CDSE to the outgoing records
434 // TODO: maybe only do this every few hours?
435 ckksnotice("ckksoutgoing", ckks, "Updating device state: %@", cdse);
436 recordsToSave[cdseRecord.recordID] = cdseRecord;
439 ckksinfo("ckksoutgoing", ckks, "Saving records %@ to CloudKit zone %@", recordsToSave, ckks.zoneID);
441 self.modifyRecordsOperation = [[CKModifyRecordsOperation alloc] initWithRecordsToSave:recordsToSave.allValues recordIDsToDelete:recordIDsToDelete];
442 self.modifyRecordsOperation.atomic = TRUE;
443 self.modifyRecordsOperation.timeoutIntervalForRequest = 2;
444 self.modifyRecordsOperation.qualityOfService = NSQualityOfServiceUtility;
445 self.modifyRecordsOperation.savePolicy = CKRecordSaveIfServerRecordUnchanged;
446 self.modifyRecordsOperation.group = self.ckoperationGroup;
447 ckksnotice("ckksoutgoing", ckks, "Operation group is %@", self.ckoperationGroup);
449 self.modifyRecordsOperation.perRecordCompletionBlock = ^(CKRecord *record, NSError * _Nullable error) {
450 __strong __typeof(weakSelf) strongSelf = weakSelf;
451 __strong __typeof(strongSelf.ckks) blockCKKS = strongSelf.ckks;
454 ckksnotice("ckksoutgoing", blockCKKS, "Record upload successful for %@", record.recordID.recordName);
456 ckkserror("ckksoutgoing", blockCKKS, "error on row: %@ %@", error, record);
460 self.modifyRecordsOperation.modifyRecordsCompletionBlock = modifyRecordsCompletionBlock;
461 [self dependOnBeforeGroupFinished: self.modifyRecordsOperation];
462 [ckks.database addOperation: self.modifyRecordsOperation];
468 - (void)_onqueueModifyRecordAsError:(CKRecordID*)recordID recordError:(NSError*)itemerror {
469 CKKSKeychainView* ckks = self.ckks;
471 ckkserror("ckksoutgoing", ckks, "no CKKS object");
475 dispatch_assert_queue(ckks.queue);
477 NSError* error = nil;
480 // At this stage, cloudkit doesn't give us record types
481 if([recordID.recordName isEqualToString: SecCKKSKeyClassA] ||
482 [recordID.recordName isEqualToString: SecCKKSKeyClassC] ||
483 [recordID.recordName hasPrefix:@"Manifest:-:"] ||
484 [recordID.recordName hasPrefix:@"ManifestLeafRecord:-:"]) {
485 // Nothing to do here. We need a whole key refetch and synchronize.
487 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry fromDatabase:recordID.recordName state: SecCKKSStateInFlight zoneID:ckks.zoneID error:&error];
488 [ckks _onqueueErrorOutgoingQueueEntry: oqe itemError: itemerror error:&error];
490 ckkserror("ckksoutgoing", ckks, "Couldn't set OQE %@ as error: %@", recordID.recordName, error);
498 - (void)_onqueueModifyAllRecordsAsReencrypt: (NSArray<CKRecordID*>*) recordIDs {
499 CKKSKeychainView* ckks = self.ckks;
501 ckkserror("ckksoutgoing", ckks, "no CKKS object");
505 dispatch_assert_queue(ckks.queue);
507 NSError* error = nil;
510 for(CKRecordID* recordID in recordIDs) {
511 // At this stage, cloudkit doesn't give us record types
512 if([recordID.recordName isEqualToString: SecCKKSKeyClassA] ||
513 [recordID.recordName isEqualToString: SecCKKSKeyClassC]) {
514 // Nothing to do here. We need a whole key refetch and synchronize.
516 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry fromDatabase:recordID.recordName state: SecCKKSStateInFlight zoneID:ckks.zoneID error:&error];
517 [ckks _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateReencrypt error:&error];
519 ckkserror("ckksoutgoing", ckks, "Couldn't set OQE %@ as reencrypt: %@", recordID.recordName, error);
526 SecADAddValueForScalarKey((__bridge CFStringRef) SecCKKSAggdItemReencryption, count);