]>
Commit | Line | Data |
---|---|---|
866f8763 A |
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 | #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" | |
33 | ||
34 | #include <securityd/SecItemServer.h> | |
35 | #include <securityd/SecItemDb.h> | |
36 | #include <Security/SecItemPriv.h> | |
37 | #include <utilities/SecADWrapper.h> | |
38 | #import "CKKSPowerCollection.h" | |
39 | ||
40 | #if OCTAGON | |
41 | ||
42 | @interface CKKSOutgoingQueueOperation() | |
43 | @property CKModifyRecordsOperation* modifyRecordsOperation; | |
44 | @end | |
45 | ||
46 | @implementation CKKSOutgoingQueueOperation | |
47 | ||
48 | - (instancetype)init { | |
49 | if(self = [super init]) { | |
50 | } | |
51 | return nil; | |
52 | } | |
53 | - (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks ckoperationGroup:(CKOperationGroup*)ckoperationGroup { | |
54 | if(self = [super init]) { | |
55 | _ckks = ckks; | |
56 | _ckoperationGroup = ckoperationGroup; | |
57 | ||
58 | [self addNullableDependency:ckks.viewSetupOperation]; | |
59 | [self addNullableDependency:ckks.holdOutgoingQueueOperation]; | |
60 | ||
61 | // Depend on all previous CKKSOutgoingQueueOperations | |
62 | [self linearDependencies:ckks.outgoingQueueOperations]; | |
63 | ||
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]; | |
67 | } | |
68 | return self; | |
69 | } | |
70 | ||
71 | - (void) groupStart { | |
72 | // Synchronous, on some thread. Get back on the CKKS queue for thread-safety. | |
73 | __weak __typeof(self) weakSelf = self; | |
74 | ||
75 | CKKSKeychainView* ckks = self.ckks; | |
76 | if(!ckks) { | |
77 | ckkserror("ckksoutgoing", ckks, "no ckks object"); | |
78 | return; | |
79 | } | |
80 | ||
81 | [ckks dispatchSyncWithAccountQueue: ^bool{ | |
82 | ckks.lastOutgoingQueueOperation = self; | |
83 | if(self.cancelled) { | |
84 | ckksnotice("ckksoutgoing", ckks, "CKKSOutgoingQueueOperation cancelled, quitting"); | |
85 | return false; | |
86 | } | |
87 | ||
88 | NSError* error = nil; | |
89 | ||
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]; | |
92 | ||
93 | if(error != nil) { | |
94 | ckkserror("ckksoutgoing", ckks, "Error fetching outgoing queue records: %@", error); | |
95 | self.error = error; | |
96 | return false; | |
97 | } | |
98 | ||
99 | ckksinfo("ckksoutgoing", ckks, "processing outgoing queue: %@", queueEntries); | |
100 | ||
101 | NSMutableDictionary<CKRecordID*, CKRecord*>* recordsToSave = [[NSMutableDictionary alloc] init]; | |
102 | NSMutableSet<CKRecordID*>* oqesModified = [[NSMutableSet alloc] init]; | |
103 | NSMutableArray<CKRecordID *>* recordIDsToDelete = [[NSMutableArray alloc] init]; | |
104 | ||
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; | |
109 | ||
110 | if(error != nil) { | |
111 | ckkserror("ckksoutgoing", ckks, "Couldn't load current class keys: %@", error); | |
112 | return false; | |
113 | } | |
114 | ||
115 | for(CKKSOutgoingQueueEntry* oqe in queueEntries) { | |
116 | if(self.cancelled) { | |
117 | secdebug("ckksoutgoing", "CKKSOutgoingQueueOperation cancelled, quitting"); | |
118 | return false; | |
119 | } | |
120 | ||
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. | |
124 | continue; | |
125 | } | |
126 | ||
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]) { | |
131 | // Excellent. | |
132 | currentKeysToSave[SecCKKSKeyClassA] = currentClassAKeyPointer; | |
133 | ||
134 | } else if([oqe.item.parentKeyUUID isEqualToString: currentClassCKeyPointer.currentKeyUUID]) { | |
135 | // Works for us! | |
136 | currentKeysToSave[SecCKKSKeyClassC] = currentClassCKeyPointer; | |
137 | ||
138 | } else { | |
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]; | |
142 | if(error) { | |
143 | ckkserror("ckksoutgoing", ckks, "couldn't save oqe to database: %@", error); | |
144 | self.error = error; | |
145 | error = nil; | |
146 | } | |
147 | needsReencrypt = true; | |
148 | continue; | |
149 | } | |
150 | } | |
151 | ||
152 | if([oqe.action isEqualToString: SecCKKSActionAdd]) { | |
153 | CKRecord* record = [oqe.item CKRecordWithZoneID: ckks.zoneID]; | |
154 | recordsToSave[record.recordID] = record; | |
155 | [oqesModified addObject: record.recordID]; | |
156 | ||
157 | [ckks _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateInFlight error:&error]; | |
158 | if(error) { | |
159 | ckkserror("ckksoutgoing", ckks, "couldn't save state for CKKSOutgoingQueueEntry: %@", error); | |
160 | self.error = error; | |
161 | } | |
162 | ||
163 | } else if ([oqe.action isEqualToString: SecCKKSActionDelete]) { | |
164 | [recordIDsToDelete addObject: [[CKRecordID alloc] initWithRecordName: oqe.item.uuid zoneID: ckks.zoneID]]; | |
165 | ||
166 | [ckks _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateInFlight error:&error]; | |
167 | if(error) { | |
168 | ckkserror("ckksoutgoing", ckks, "couldn't save state for CKKSOutgoingQueueEntry: %@", error); | |
169 | } | |
170 | ||
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]; | |
174 | if(!ckme) { | |
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 | |
177 | // Or: ? | |
178 | ckkserror("ckksoutgoing", ckks, "update to a record that doesn't exist? %@", oqe.item.uuid); | |
179 | // treat as an add. | |
180 | CKRecord* record = [oqe.item CKRecordWithZoneID: ckks.zoneID]; | |
181 | recordsToSave[record.recordID] = record; | |
182 | [oqesModified addObject: record.recordID]; | |
183 | ||
184 | [ckks _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateInFlight error:&error]; | |
185 | if(error) { | |
186 | ckkserror("ckksoutgoing", ckks, "couldn't save state for CKKSOutgoingQueueEntry: %@", error); | |
187 | self.error = error; | |
188 | } | |
189 | } else { | |
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]; | |
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 | // 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]; | |
207 | ||
208 | [ckks _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateInFlight error:&error]; | |
209 | if(error) { | |
210 | ckkserror("ckksoutgoing", ckks, "couldn't save state for CKKSOutgoingQueueEntry: %@", error); | |
211 | } | |
212 | } | |
213 | } | |
214 | } | |
215 | ||
216 | if(needsReencrypt) { | |
217 | ckksnotice("ckksoutgoing", ckks, "An item needs reencryption!"); | |
218 | ||
219 | CKKSReencryptOutgoingItemsOperation* op = [[CKKSReencryptOutgoingItemsOperation alloc] initWithCKKSKeychainView:ckks ckoperationGroup:self.ckoperationGroup]; | |
220 | [ckks scheduleOperation: op]; | |
221 | } | |
222 | ||
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); | |
228 | } | |
229 | return true; | |
230 | } | |
231 | ||
232 | self.itemsProcessed = recordsToSave.count; | |
233 | ||
234 | NSBlockOperation* modifyComplete = [[NSBlockOperation alloc] init]; | |
235 | modifyComplete.name = @"modifyRecordsComplete"; | |
236 | [self dependOnBeforeGroupFinished: modifyComplete]; | |
237 | ||
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; | |
243 | } | |
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); | |
248 | } | |
249 | } | |
250 | else { | |
251 | ckkserror("ckksoutgoing", ckks, "could not get current ego manifest to update"); | |
252 | } | |
253 | } | |
254 | ||
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"); | |
260 | return; | |
261 | } | |
262 | ||
263 | [strongCKKS dispatchSync: ^bool{ | |
264 | if(ckerror) { | |
265 | ckkserror("ckksoutgoing", strongCKKS, "error processing outgoing queue: %@", ckerror); | |
266 | ||
267 | // Tell CKKS about any out-of-date records | |
268 | [strongCKKS _onqueueCKWriteFailed:ckerror attemptedRecordsChanged:recordsToSave]; | |
269 | ||
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]; | |
276 | ||
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]; | |
282 | ||
283 | ckksnotice("ckksoutgoing", strongCKKS, "initiate key fetch and reencrypt"); | |
284 | // Nudge the key state machine, so that it runs off to fetch the new keys | |
285 | [strongCKKS _onqueueKeyStateMachineRequestFetch]; | |
286 | // This will wait for the key hierarchy to become 'ready' | |
287 | CKKSReencryptOutgoingItemsOperation* op = [[CKKSReencryptOutgoingItemsOperation alloc] initWithCKKSKeychainView:strongCKKS ckoperationGroup:strongSelf.ckoperationGroup]; | |
288 | [strongCKKS scheduleOperation: op]; | |
289 | ||
290 | // Quit the loop so we only do this once | |
291 | break; | |
292 | } else { | |
293 | // CKErrorServerRecordChanged on an item update means that we've been overwritten. | |
294 | if([oqesModified containsObject:recordID]) { | |
295 | [self _onqueueModifyRecordAsError:recordID recordError:recordError]; | |
296 | } | |
297 | } | |
298 | } else if(recordError.code == CKErrorBatchRequestFailed) { | |
299 | // Also fine. This record only didn't succeed because something else failed. | |
300 | // OQEs should be placed back into the 'new' state, unless they've been overwritten by a new OQE. Other records should be ignored. | |
301 | ||
302 | if([oqesModified containsObject:recordID]) { | |
303 | NSError* error = nil; | |
304 | CKKSOutgoingQueueEntry* inflightOQE = [CKKSOutgoingQueueEntry tryFromDatabase:recordID.recordName state:SecCKKSStateInFlight zoneID:recordID.zoneID error:&error]; | |
305 | CKKSOutgoingQueueEntry* newOQE = [CKKSOutgoingQueueEntry tryFromDatabase:recordID.recordName state:SecCKKSStateNew zoneID:recordID.zoneID error:&error]; | |
306 | if(error) { | |
307 | ckkserror("ckksoutgoing", strongCKKS, "Couldn't try to fetch an overwriting OQE: %@", error); | |
308 | } | |
309 | ||
310 | if(newOQE) { | |
311 | ckksnotice("ckksoutgoing", strongCKKS, "New modification has come in behind failed change for %@; dropping failed change", inflightOQE); | |
312 | [strongCKKS _onqueueChangeOutgoingQueueEntry:inflightOQE toState:SecCKKSStateDeleted error:&error]; | |
313 | if(error) { | |
314 | ckkserror("ckksoutgoing", strongCKKS, "Couldn't delete in-flight OQE: %@", error); | |
315 | } | |
316 | } else { | |
317 | [strongCKKS _onqueueChangeOutgoingQueueEntry:inflightOQE toState:SecCKKSStateNew error:&error]; | |
318 | } | |
319 | } | |
320 | ||
321 | } else if ([recordID.recordName hasPrefix:@"Manifest:-:"] || [recordID.recordName hasPrefix:@"ManifestLeafRecord:-:"]) { | |
322 | [[CKKSAnalyticsLogger logger] logSoftFailureForEventNamed:@"ManifestUpload" withAttributes:@{CKKSManifestZoneKey : strongCKKS.zoneID.zoneName, CKKSManifestSignerIDKey : strongCKKS.egoManifest.signerID, CKKSManifestGenCountKey : @(strongCKKS.egoManifest.generationCount)}]; | |
323 | } else { | |
324 | // Some unknown error occurred on this record. If it's an OQE, move it to the error state. | |
325 | ckkserror("ckksoutgoing", strongCKKS, "Unknown error on row: %@ %@", recordID, recordError); | |
326 | if([oqesModified containsObject:recordID]) { | |
327 | [self _onqueueModifyRecordAsError:recordID recordError:recordError]; | |
328 | } | |
329 | } | |
330 | } | |
331 | } | |
332 | ||
333 | strongSelf.error = error; | |
334 | return true; | |
335 | } | |
336 | ||
337 | ckksnotice("ckksoutgoing", strongCKKS, "Completed processing outgoing queue"); | |
338 | NSError* error = NULL; | |
339 | CKKSPowerCollection *plstats = [[CKKSPowerCollection alloc] init]; | |
340 | ||
341 | for(CKRecord* record in savedRecords) { | |
342 | // Save the item records | |
343 | if([record.recordType isEqualToString: SecCKRecordItemType]) { | |
344 | CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry fromDatabase: record.recordID.recordName state: SecCKKSStateInFlight zoneID:strongCKKS.zoneID error:&error]; | |
345 | [strongCKKS _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateDeleted error:&error]; | |
346 | if(error) { | |
347 | ckkserror("ckksoutgoing", strongCKKS, "Couldn't update %@ in outgoingqueue: %@", record.recordID.recordName, error); | |
348 | strongSelf.error = error; | |
349 | } | |
350 | error = nil; | |
351 | CKKSMirrorEntry* ckme = [[CKKSMirrorEntry alloc] initWithCKRecord: record]; | |
352 | [ckme saveToDatabase: &error]; | |
353 | if(error) { | |
354 | ckkserror("ckksoutgoing", strongCKKS, "Couldn't save %@ to ckmirror: %@", record.recordID.recordName, error); | |
355 | strongSelf.error = error; | |
356 | } | |
357 | ||
358 | [plstats storedOQE:oqe]; | |
359 | ||
360 | // And the CKCurrentKeyRecords (do we need to do this? Will the server update the change tag on a save which saves nothing?) | |
361 | } else if([record.recordType isEqualToString: SecCKRecordCurrentKeyType]) { | |
362 | CKKSCurrentKeyPointer* currentkey = [[CKKSCurrentKeyPointer alloc] initWithCKRecord: record]; | |
363 | [currentkey saveToDatabase: &error]; | |
364 | if(error) { | |
365 | ckkserror("ckksoutgoing", strongCKKS, "Couldn't save %@ to currentkey: %@", record.recordID.recordName, error); | |
366 | strongSelf.error = error; | |
367 | } | |
368 | ||
369 | } else if ([record.recordType isEqualToString:SecCKRecordDeviceStateType]) { | |
370 | CKKSDeviceStateEntry* newcdse = [[CKKSDeviceStateEntry alloc] initWithCKRecord:record]; | |
371 | [newcdse saveToDatabase:&error]; | |
372 | if(error) { | |
373 | ckkserror("ckksoutgoing", strongCKKS, "Couldn't save %@ to ckdevicestate: %@", record.recordID.recordName, error); | |
374 | strongSelf.error = error; | |
375 | } | |
376 | ||
377 | } else if ([record.recordType isEqualToString:SecCKRecordManifestType]) { | |
378 | [[CKKSAnalyticsLogger logger] logSuccessForEventNamed:@"ManifestUpload"]; | |
379 | } else if (![record.recordType isEqualToString:SecCKRecordManifestLeafType]) { | |
380 | ckkserror("ckksoutgoing", strongCKKS, "unknown record type in results: %@", record); | |
381 | } | |
382 | } | |
383 | ||
384 | // Delete the deleted record IDs | |
385 | for(CKRecordID* ckrecordID in deletedRecordIDs) { | |
386 | ||
387 | NSError* error = nil; | |
388 | CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry fromDatabase: ckrecordID.recordName state: SecCKKSStateInFlight zoneID:strongCKKS.zoneID error:&error]; | |
389 | [strongCKKS _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateDeleted error:&error]; | |
390 | if(error) { | |
391 | ckkserror("ckksoutgoing", strongCKKS, "Couldn't delete %@ from outgoingqueue: %@", ckrecordID.recordName, error); | |
392 | strongSelf.error = error; | |
393 | } | |
394 | error = nil; | |
395 | CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase: ckrecordID.recordName zoneID:strongCKKS.zoneID error:&error]; | |
396 | [ckme deleteFromDatabase: &error]; | |
397 | if(error) { | |
398 | ckkserror("ckksoutgoing", strongCKKS, "Couldn't delete %@ from ckmirror: %@", ckrecordID.recordName, error); | |
399 | strongSelf.error = error; | |
400 | } | |
401 | ||
402 | [plstats deletedOQE:oqe]; | |
403 | } | |
404 | ||
405 | [plstats commit]; | |
406 | ||
407 | if(strongSelf.error) { | |
408 | ckkserror("ckksoutgoing", strongCKKS, "Operation failed; rolling back: %@", strongSelf.error); | |
409 | return false; | |
410 | } | |
411 | return true; | |
412 | }]; | |
413 | ||
414 | ||
415 | [strongSelf.operationQueue addOperation: modifyComplete]; | |
416 | // Kick off another queue process. We expect it to exit instantly, but who knows! | |
417 | [strongCKKS processOutgoingQueue:self.ckoperationGroup]; | |
418 | }; | |
419 | ||
420 | ckksinfo("ckksoutgoing", ckks, "Current keys to update: %@", currentKeysToSave); | |
421 | for(CKKSCurrentKeyPointer* keypointer in currentKeysToSave.allValues) { | |
422 | CKRecord* record = [keypointer CKRecordWithZoneID: ckks.zoneID]; | |
423 | recordsToSave[record.recordID] = record; | |
424 | } | |
425 | ||
426 | // Piggyback on this operation to update our device state | |
427 | NSError* cdseError = nil; | |
428 | CKKSDeviceStateEntry* cdse = [ckks _onqueueCurrentDeviceStateEntry:&cdseError]; | |
429 | CKRecord* cdseRecord = [cdse CKRecordWithZoneID: ckks.zoneID]; | |
430 | if(cdseError) { | |
431 | ckkserror("ckksoutgoing", ckks, "Can't make current device state: %@", cdseError); | |
432 | } else if(!cdseRecord) { | |
433 | ckkserror("ckksoutgoing", ckks, "Can't make current device state cloudkit record, but no reason why"); | |
434 | } else { | |
435 | // Add the CDSE to the outgoing records | |
436 | // TODO: maybe only do this every few hours? | |
437 | ckksnotice("ckksoutgoing", ckks, "Updating device state: %@", cdse); | |
438 | recordsToSave[cdseRecord.recordID] = cdseRecord; | |
439 | } | |
440 | ||
441 | ckksinfo("ckksoutgoing", ckks, "Saving records %@ to CloudKit zone %@", recordsToSave, ckks.zoneID); | |
442 | ||
443 | self.modifyRecordsOperation = [[CKModifyRecordsOperation alloc] initWithRecordsToSave:recordsToSave.allValues recordIDsToDelete:recordIDsToDelete]; | |
444 | self.modifyRecordsOperation.atomic = TRUE; | |
445 | self.modifyRecordsOperation.timeoutIntervalForRequest = 2; | |
446 | self.modifyRecordsOperation.qualityOfService = NSQualityOfServiceUtility; | |
447 | self.modifyRecordsOperation.savePolicy = CKRecordSaveIfServerRecordUnchanged; | |
448 | self.modifyRecordsOperation.group = self.ckoperationGroup; | |
449 | ckksnotice("ckksoutgoing", ckks, "Operation group is %@", self.ckoperationGroup); | |
450 | ||
451 | self.modifyRecordsOperation.perRecordCompletionBlock = ^(CKRecord *record, NSError * _Nullable error) { | |
452 | __strong __typeof(weakSelf) strongSelf = weakSelf; | |
453 | __strong __typeof(strongSelf.ckks) blockCKKS = strongSelf.ckks; | |
454 | ||
455 | if(!error) { | |
456 | ckksnotice("ckksoutgoing", blockCKKS, "Record upload successful for %@", record.recordID.recordName); | |
457 | } else { | |
458 | ckkserror("ckksoutgoing", blockCKKS, "error on row: %@ %@", record, error); | |
459 | } | |
460 | }; | |
461 | ||
462 | self.modifyRecordsOperation.modifyRecordsCompletionBlock = modifyRecordsCompletionBlock; | |
463 | [self dependOnBeforeGroupFinished: self.modifyRecordsOperation]; | |
464 | [ckks.database addOperation: self.modifyRecordsOperation]; | |
465 | ||
466 | return true; | |
467 | }]; | |
468 | } | |
469 | ||
470 | - (void)_onqueueModifyRecordAsError:(CKRecordID*)recordID recordError:(NSError*)itemerror { | |
471 | CKKSKeychainView* ckks = self.ckks; | |
472 | if(!ckks) { | |
473 | ckkserror("ckksoutgoing", ckks, "no CKKS object"); | |
474 | return; | |
475 | } | |
476 | ||
477 | dispatch_assert_queue(ckks.queue); | |
478 | ||
479 | NSError* error = nil; | |
480 | uint64_t count = 0; | |
481 | ||
482 | // At this stage, cloudkit doesn't give us record types | |
483 | if([recordID.recordName isEqualToString: SecCKKSKeyClassA] || | |
484 | [recordID.recordName isEqualToString: SecCKKSKeyClassC] || | |
485 | [recordID.recordName hasPrefix:@"Manifest:-:"] || | |
486 | [recordID.recordName hasPrefix:@"ManifestLeafRecord:-:"]) { | |
487 | // Nothing to do here. We need a whole key refetch and synchronize. | |
488 | } else { | |
489 | CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry fromDatabase:recordID.recordName state: SecCKKSStateInFlight zoneID:ckks.zoneID error:&error]; | |
490 | [ckks _onqueueErrorOutgoingQueueEntry: oqe itemError: itemerror error:&error]; | |
491 | if(error) { | |
492 | ckkserror("ckksoutgoing", ckks, "Couldn't set OQE %@ as error: %@", recordID.recordName, error); | |
493 | self.error = error; | |
494 | } | |
495 | count ++; | |
496 | } | |
497 | } | |
498 | ||
499 | ||
500 | - (void)_onqueueModifyAllRecordsAsReencrypt: (NSArray<CKRecordID*>*) recordIDs { | |
501 | CKKSKeychainView* ckks = self.ckks; | |
502 | if(!ckks) { | |
503 | ckkserror("ckksoutgoing", ckks, "no CKKS object"); | |
504 | return; | |
505 | } | |
506 | ||
507 | dispatch_assert_queue(ckks.queue); | |
508 | ||
509 | NSError* error = nil; | |
510 | uint64_t count = 0; | |
511 | ||
512 | for(CKRecordID* recordID in recordIDs) { | |
513 | // At this stage, cloudkit doesn't give us record types | |
514 | if([recordID.recordName isEqualToString: SecCKKSKeyClassA] || | |
515 | [recordID.recordName isEqualToString: SecCKKSKeyClassC]) { | |
516 | // Nothing to do here. We need a whole key refetch and synchronize. | |
517 | } else { | |
518 | CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry fromDatabase:recordID.recordName state: SecCKKSStateInFlight zoneID:ckks.zoneID error:&error]; | |
519 | [ckks _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateReencrypt error:&error]; | |
520 | if(error) { | |
521 | ckkserror("ckksoutgoing", ckks, "Couldn't set OQE %@ as reencrypt: %@", recordID.recordName, error); | |
522 | self.error = error; | |
523 | } | |
524 | count ++; | |
525 | } | |
526 | } | |
527 | ||
528 | SecADAddValueForScalarKey((__bridge CFStringRef) SecCKKSAggdItemReencryption, count); | |
529 | } | |
530 | ||
531 | @end; | |
532 | ||
533 | #endif |