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