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