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