]>
Commit | Line | Data |
---|---|---|
866f8763 | 1 | /* |
d64be36e | 2 | * Copyright (c) 2016-2020 Apple Inc. All Rights Reserved. |
866f8763 A |
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 | ||
d64be36e A |
24 | #import "keychain/analytics/CKKSPowerCollection.h" |
25 | #import "keychain/ckks/CKKSAnalytics.h" | |
866f8763 | 26 | #import "keychain/ckks/CKKSCurrentItemPointer.h" |
d64be36e A |
27 | #import "keychain/ckks/CKKSIncomingQueueEntry.h" |
28 | #import "keychain/ckks/CKKSIncomingQueueOperation.h" | |
29 | #import "keychain/ckks/CKKSItemEncrypter.h" | |
30 | #import "keychain/ckks/CKKSKey.h" | |
31 | #import "keychain/ckks/CKKSKeychainView.h" | |
32 | #import "keychain/ckks/CKKSOutgoingQueueEntry.h" | |
33 | #import "keychain/ckks/CKKSStates.h" | |
34 | #import "keychain/ckks/CKKSViewManager.h" | |
35 | #import "keychain/ckks/CloudKitCategories.h" | |
b54c578e | 36 | #import "keychain/ot/ObjCImprovements.h" |
866f8763 | 37 | |
7fb2cbd2 A |
38 | #include "keychain/securityd/SecItemServer.h" |
39 | #include "keychain/securityd/SecItemDb.h" | |
866f8763 A |
40 | #include <Security/SecItemPriv.h> |
41 | ||
b54c578e | 42 | #import <utilities/SecCoreAnalytics.h> |
866f8763 A |
43 | |
44 | #if OCTAGON | |
45 | ||
46 | @interface CKKSIncomingQueueOperation () | |
47 | @property bool newOutgoingEntries; | |
48 | @property bool pendingClassAEntries; | |
ecaf5866 | 49 | @property bool missingKey; |
d64be36e A |
50 | |
51 | @property NSMutableSet<NSString*>* viewsToScan; | |
866f8763 A |
52 | @end |
53 | ||
54 | @implementation CKKSIncomingQueueOperation | |
d64be36e A |
55 | @synthesize nextState = _nextState; |
56 | @synthesize intendedState = _intendedState; | |
866f8763 A |
57 | |
58 | - (instancetype)init { | |
59 | return nil; | |
60 | } | |
d64be36e A |
61 | |
62 | - (instancetype)initWithDependencies:(CKKSOperationDependencies*)dependencies | |
63 | ckks:(CKKSKeychainView*)ckks | |
64 | intending:(OctagonState*)intending | |
65 | errorState:(OctagonState*)errorState | |
66 | errorOnClassAFailure:(bool)errorOnClassAFailure | |
67 | handleMismatchedViewItems:(bool)handleMismatchedViewItems | |
68 | { | |
866f8763 | 69 | if(self = [super init]) { |
d64be36e | 70 | _deps = dependencies; |
866f8763 A |
71 | _ckks = ckks; |
72 | ||
d64be36e A |
73 | _intendedState = intending; |
74 | _nextState = errorState; | |
75 | ||
866f8763 A |
76 | // Can't process unless we have a reasonable key hierarchy. |
77 | if(ckks.keyStateReadyDependency) { | |
78 | [self addDependency: ckks.keyStateReadyDependency]; | |
79 | } | |
80 | ||
ecaf5866 A |
81 | [self addNullableDependency: ckks.holdIncomingQueueOperation]; |
82 | ||
866f8763 A |
83 | _errorOnClassAFailure = errorOnClassAFailure; |
84 | _pendingClassAEntries = false; | |
85 | ||
d64be36e | 86 | _handleMismatchedViewItems = handleMismatchedViewItems; |
866f8763 | 87 | |
d64be36e | 88 | _viewsToScan = [NSMutableSet set]; |
866f8763 | 89 | |
d64be36e | 90 | [self linearDependencies:ckks.incomingQueueOperations]; |
866f8763 A |
91 | } |
92 | return self; | |
93 | } | |
94 | ||
d64be36e | 95 | - (bool)processNewCurrentItemPointers:(NSArray<CKKSCurrentItemPointer*>*)queueEntries |
866f8763 | 96 | { |
866f8763 A |
97 | NSError* error = nil; |
98 | for(CKKSCurrentItemPointer* p in queueEntries) { | |
79b9da22 | 99 | @autoreleasepool { |
79b9da22 | 100 | p.state = SecCKKSProcessedStateLocal; |
866f8763 | 101 | |
79b9da22 | 102 | [p saveToDatabase:&error]; |
d64be36e | 103 | ckksnotice("ckkspointer", self.deps.zoneID, "Saving new current item pointer: %@", p); |
79b9da22 | 104 | if(error) { |
d64be36e | 105 | ckkserror("ckksincoming", self.deps.zoneID, "Error saving new current item pointer: %@ %@", error, p); |
79b9da22 | 106 | } |
79b9da22 | 107 | } |
866f8763 A |
108 | } |
109 | ||
110 | if(queueEntries.count > 0) { | |
d64be36e | 111 | [self.deps.notifyViewChangedScheduler trigger]; |
866f8763 A |
112 | } |
113 | ||
114 | return (error == nil); | |
115 | } | |
116 | ||
d64be36e | 117 | - (bool)processQueueEntries:(NSArray<CKKSIncomingQueueEntry*>*)queueEntries |
866f8763 A |
118 | { |
119 | CKKSKeychainView* ckks = self.ckks; | |
d64be36e | 120 | dispatch_assert_queue(ckks.queue); |
866f8763 A |
121 | |
122 | NSMutableArray* newOrChangedRecords = [[NSMutableArray alloc] init]; | |
123 | NSMutableArray* deletedRecordIDs = [[NSMutableArray alloc] init]; | |
866f8763 A |
124 | |
125 | for(id entry in queueEntries) { | |
79b9da22 | 126 | @autoreleasepool { |
79b9da22 A |
127 | NSError* error = nil; |
128 | ||
129 | CKKSIncomingQueueEntry* iqe = (CKKSIncomingQueueEntry*) entry; | |
d64be36e | 130 | ckksnotice("ckksincoming", self.deps.zoneID, "ready to process an incoming queue entry: %@ %@ %@", iqe, iqe.uuid, iqe.action); |
79b9da22 A |
131 | |
132 | // Note that we currently unencrypt the item before deleting it, instead of just deleting it | |
133 | // This finds the class, which is necessary for the deletion process. We could just try to delete | |
134 | // across all classes, though... | |
b54c578e A |
135 | NSDictionary* attributes = [CKKSIncomingQueueOperation decryptCKKSItemToAttributes:iqe.item error:&error]; |
136 | ||
79b9da22 | 137 | if(!attributes || error) { |
d64be36e | 138 | if([self.deps.lockStateTracker isLockedError:error]) { |
79b9da22 | 139 | NSError* localerror = nil; |
d64be36e A |
140 | ckkserror("ckksincoming", self.deps.zoneID, "Keychain is locked; can't decrypt IQE %@", iqe); |
141 | CKKSKey* key = [CKKSKey tryFromDatabase:iqe.item.parentKeyUUID zoneID:self.deps.zoneID error:&localerror]; | |
79b9da22 A |
142 | if(localerror || ([key.keyclass isEqualToString:SecCKKSKeyClassA] && self.errorOnClassAFailure)) { |
143 | self.error = error; | |
144 | } | |
866f8763 | 145 | |
79b9da22 A |
146 | // If this isn't an error, make sure it gets processed later. |
147 | if([key.keyclass isEqualToString:SecCKKSKeyClassA] && !self.errorOnClassAFailure) { | |
148 | self.pendingClassAEntries = true; | |
149 | } | |
866f8763 | 150 | |
79b9da22 | 151 | } else if ([error.domain isEqualToString:@"securityd"] && error.code == errSecItemNotFound) { |
d64be36e | 152 | ckkserror("ckksincoming", self.deps.zoneID, "Coudn't find key in keychain; will attempt to poke key hierarchy: %@", error) |
79b9da22 | 153 | self.missingKey = true; |
d64be36e | 154 | self.error = error; |
866f8763 | 155 | |
79b9da22 | 156 | } else { |
d64be36e | 157 | ckkserror("ckksincoming", self.deps.zoneID, "Couldn't decrypt IQE %@ for some reason: %@", iqe, error); |
79b9da22 A |
158 | self.error = error; |
159 | } | |
160 | self.errorItemsProcessed += 1; | |
161 | continue; | |
866f8763 | 162 | } |
866f8763 | 163 | |
79b9da22 A |
164 | NSString* classStr = [attributes objectForKey: (__bridge NSString*) kSecClass]; |
165 | if(![classStr isKindOfClass: [NSString class]]) { | |
166 | self.error = [NSError errorWithDomain:@"securityd" | |
167 | code:errSecInternalError | |
168 | userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Item did not have a reasonable class: %@", classStr]}]; | |
d64be36e | 169 | ckkserror("ckksincoming", self.deps.zoneID, "Synced item seems wrong: %@", self.error); |
79b9da22 A |
170 | self.errorItemsProcessed += 1; |
171 | continue; | |
866f8763 | 172 | } |
866f8763 | 173 | |
79b9da22 | 174 | const SecDbClass * classP = !classStr ? NULL : kc_class_with_name((__bridge CFStringRef) classStr); |
3f0f0d49 | 175 | |
79b9da22 | 176 | if(!classP) { |
d64be36e | 177 | ckkserror("ckksincoming", self.deps.zoneID, "unknown class in object: %@ %@", classStr, iqe); |
79b9da22 A |
178 | iqe.state = SecCKKSStateError; |
179 | [iqe saveToDatabase:&error]; | |
180 | if(error) { | |
d64be36e | 181 | ckkserror("ckksincoming", self.deps.zoneID, "Couldn't save errored IQE to database: %@", error); |
79b9da22 | 182 | self.error = error; |
866f8763 | 183 | } |
3f0f0d49 | 184 | self.errorItemsProcessed += 1; |
866f8763 A |
185 | continue; |
186 | } | |
3f0f0d49 | 187 | |
d64be36e A |
188 | NSString* intendedView = [self.deps.syncingPolicy mapDictionaryToView:attributes]; |
189 | if(![self.deps.zoneID.zoneName isEqualToString:intendedView]) { | |
190 | if(self.handleMismatchedViewItems) { | |
191 | [self _onqueueHandleMismatchedViewItem:iqe | |
192 | secDbClass:classP | |
193 | attributes:attributes | |
194 | intendedView:intendedView]; | |
195 | } else { | |
196 | ckksnotice("ckksincoming", ckks, "Received an item (%@), but our current policy claims it should be in view %@", iqe.uuid, intendedView); | |
79b9da22 | 197 | |
d64be36e A |
198 | [self _onqueueUpdateIQE:iqe withState:SecCKKSStateMismatchedView error:&error]; |
199 | if(error) { | |
200 | ckkserror("ckksincoming", ckks, "Couldn't save mismatched IQE to database: %@", error); | |
79b9da22 | 201 | self.errorItemsProcessed += 1; |
d64be36e | 202 | self.error = error; |
79b9da22 | 203 | } |
d64be36e A |
204 | |
205 | [ckks receivedItemForWrongView]; | |
866f8763 | 206 | } |
d64be36e A |
207 | continue; |
208 | } | |
209 | ||
210 | if([iqe.action isEqualToString: SecCKKSActionAdd] || [iqe.action isEqualToString: SecCKKSActionModify]) { | |
211 | [self _onqueueHandleIQEChange:iqe | |
212 | attributes:attributes | |
213 | class:classP | |
214 | sortedForThisView:YES]; | |
215 | [newOrChangedRecords addObject:[iqe.item CKRecordWithZoneID:self.deps.zoneID]]; | |
216 | ||
217 | } else if ([iqe.action isEqualToString: SecCKKSActionDelete]) { | |
218 | [self _onqueueHandleIQEDelete: iqe class:classP]; | |
219 | [deletedRecordIDs addObject:[[CKRecordID alloc] initWithRecordName:iqe.uuid zoneID:self.deps.zoneID]]; | |
866f8763 A |
220 | } |
221 | } | |
222 | } | |
223 | ||
224 | if(newOrChangedRecords.count > 0 || deletedRecordIDs > 0) { | |
225 | // Schedule a view change notification | |
d64be36e | 226 | [self.deps.notifyViewChangedScheduler trigger]; |
866f8763 A |
227 | } |
228 | ||
ecaf5866 | 229 | if(self.missingKey) { |
d64be36e A |
230 | // TODO: will be removed when the IncomingQueueOperation is part of the state machine |
231 | [ckks.stateMachine _onqueuePokeStateMachine]; | |
232 | self.nextState = SecCKKSZoneKeyStateUnhealthy; | |
ecaf5866 A |
233 | } |
234 | ||
866f8763 A |
235 | return true; |
236 | } | |
237 | ||
d64be36e A |
238 | - (void)_onqueueHandleMismatchedViewItem:(CKKSIncomingQueueEntry*)iqe |
239 | secDbClass:(const SecDbClass*)secDbClass | |
240 | attributes:(NSDictionary*)attributes | |
241 | intendedView:(NSString* _Nullable)intendedView | |
242 | { | |
243 | ckksnotice("ckksincoming", self.deps.zoneID, "Received an item (%@), which should be in view %@", iqe.uuid, intendedView); | |
244 | ||
245 | // Here's the plan: | |
246 | // | |
247 | // If this is an add or a modify, we will execute the modification _if we do not currently have this item_. | |
248 | // Then, ask the view that should handle this item to scan. | |
249 | // | |
250 | // When, we will leave the CloudKit record in the existing 'wrong' view. | |
251 | // This will allow garbage to collect, but should prevent item loss in complicated multi-device scenarios. | |
252 | // | |
253 | // If this is a deletion, then we will inspect the other zone's current on-disk state. If it knows about the item, | |
254 | // we will ignore the deletion from this view. Otherwise, we will proceed with the deletion. | |
255 | // Note that the deletion approach already ensures that the UUID of the deleted item matches the UUID of the CKRecord. | |
256 | // This protects against an item being in multiple views, and deleted from only one. | |
257 | ||
258 | if([iqe.action isEqualToString:SecCKKSActionAdd] || [iqe.action isEqualToString:SecCKKSActionModify]) { | |
259 | CFErrorRef cferror = NULL; | |
260 | SecDbItemRef item = SecDbItemCreateWithAttributes(NULL, secDbClass, (__bridge CFDictionaryRef) attributes, KEYBAG_DEVICE, &cferror); | |
261 | ||
262 | if(!item || cferror) { | |
263 | ckkserror("ckksincoming", self.deps.zoneID, "Unable to create SecDbItemRef from IQE: %@", cferror); | |
264 | return; | |
265 | } | |
266 | ||
267 | [self _onqueueHandleIQEChange:iqe item:item sortedForThisView:NO]; | |
268 | [self.viewsToScan addObject:intendedView]; | |
269 | ||
270 | CFReleaseNull(item); | |
271 | ||
272 | } else if ([iqe.action isEqualToString:SecCKKSActionDelete]) { | |
273 | NSError* loadError = nil; | |
274 | ||
275 | CKRecordZoneID* otherZoneID = [[CKRecordZoneID alloc] initWithZoneName:intendedView ownerName:CKCurrentUserDefaultName]; | |
276 | CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase:iqe.uuid zoneID:otherZoneID error:&loadError]; | |
277 | ||
278 | if(!ckme || loadError) { | |
279 | ckksnotice("ckksincoming", self.deps.zoneID, "Unable to load CKKSMirrorEntry from database* %@", loadError); | |
280 | return; | |
281 | } | |
282 | ||
283 | if(ckme) { | |
284 | ckksnotice("ckksincoming", self.deps.zoneID, "Other view (%@) already knows about this item, dropping incoming queue entry: %@", intendedView, ckme); | |
285 | NSError* saveError = nil; | |
286 | [iqe deleteFromDatabase:&saveError]; | |
287 | if(saveError) { | |
288 | ckkserror("ckksincoming", self.deps.zoneID, "Unable to delete IQE: %@", saveError); | |
289 | } | |
290 | ||
291 | } else { | |
292 | ckksnotice("ckksincoming", self.deps.zoneID, "Other view (%@) does not know about this item; processing delete for %@", intendedView, iqe); | |
293 | [self _onqueueHandleIQEDelete:iqe class:secDbClass]; | |
294 | } | |
295 | ||
296 | } else { | |
297 | // We don't recognize this action. Do nothing. | |
298 | } | |
299 | } | |
300 | ||
b54c578e A |
301 | + (NSDictionary* _Nullable)decryptCKKSItemToAttributes:(CKKSItem*)item error:(NSError**)error |
302 | { | |
303 | NSMutableDictionary* attributes = [[CKKSItemEncrypter decryptItemToDictionary:item error:error] mutableCopy]; | |
304 | if(!attributes) { | |
305 | return nil; | |
306 | } | |
307 | ||
308 | // Add the UUID (which isn't stored encrypted) | |
309 | attributes[(__bridge NSString*)kSecAttrUUID] = item.uuid; | |
310 | ||
311 | // Add the PCS plaintext fields, if they exist | |
312 | if(item.plaintextPCSServiceIdentifier) { | |
313 | attributes[(__bridge NSString*)kSecAttrPCSPlaintextServiceIdentifier] = item.plaintextPCSServiceIdentifier; | |
314 | } | |
315 | if(item.plaintextPCSPublicKey) { | |
316 | attributes[(__bridge NSString*)kSecAttrPCSPlaintextPublicKey] = item.plaintextPCSPublicKey; | |
317 | } | |
318 | if(item.plaintextPCSPublicIdentity) { | |
319 | attributes[(__bridge NSString*)kSecAttrPCSPlaintextPublicIdentity] = item.plaintextPCSPublicIdentity; | |
320 | } | |
321 | ||
322 | // This item is also synchronizable (by definition) | |
323 | [attributes setValue:@(YES) forKey:(__bridge NSString*)kSecAttrSynchronizable]; | |
324 | ||
325 | return attributes; | |
326 | } | |
327 | ||
866f8763 A |
328 | - (bool)_onqueueUpdateIQE:(CKKSIncomingQueueEntry*)iqe withState:(NSString*)newState error:(NSError**)error |
329 | { | |
330 | if (![iqe.state isEqualToString:newState]) { | |
331 | NSMutableDictionary* oldWhereClause = iqe.whereClauseToFindSelf.mutableCopy; | |
332 | oldWhereClause[@"state"] = iqe.state; | |
333 | iqe.state = newState; | |
334 | if ([iqe saveToDatabase:error]) { | |
335 | if (![CKKSSQLDatabaseObject deleteFromTable:[iqe.class sqlTable] where:oldWhereClause connection:NULL error:error]) { | |
336 | return false; | |
337 | } | |
338 | } | |
339 | else { | |
340 | return false; | |
341 | } | |
342 | } | |
343 | ||
344 | return true; | |
345 | } | |
346 | ||
d64be36e A |
347 | - (void)main |
348 | { | |
866f8763 | 349 | CKKSKeychainView* ckks = self.ckks; |
d64be36e A |
350 | |
351 | if(!ckks.itemSyncingEnabled) { | |
352 | ckkserror("ckksincoming", self.deps.zoneID, "Item syncing for this view is disabled"); | |
353 | self.nextState = self.intendedState; | |
866f8763 A |
354 | return; |
355 | } | |
356 | ||
b54c578e | 357 | WEAKIFY(self); |
29734401 | 358 | self.completionBlock = ^(void) { |
b54c578e A |
359 | STRONGIFY(self); |
360 | if (!self) { | |
d64be36e | 361 | ckkserror("ckksincoming", self.deps.zoneID, "received callback for released object"); |
29734401 A |
362 | return; |
363 | } | |
364 | ||
365 | CKKSAnalytics* logger = [CKKSAnalytics logger]; | |
366 | ||
b54c578e | 367 | if (!self.error) { |
d64be36e A |
368 | [logger logSuccessForEvent:CKKSEventProcessIncomingQueueClassC zoneName:self.deps.zoneID.zoneName]; |
369 | ||
b54c578e | 370 | if (!self.pendingClassAEntries) { |
d64be36e | 371 | [logger logSuccessForEvent:CKKSEventProcessIncomingQueueClassA zoneName:self.deps.zoneID.zoneName]; |
29734401 A |
372 | } |
373 | } else { | |
b54c578e A |
374 | [logger logRecoverableError:self.error |
375 | forEvent:self.errorOnClassAFailure ? CKKSEventProcessIncomingQueueClassA : CKKSEventProcessIncomingQueueClassC | |
d64be36e | 376 | zoneName:self.deps.zoneID.zoneName |
29734401 A |
377 | withAttributes:NULL]; |
378 | } | |
379 | }; | |
380 | ||
d64be36e | 381 | ckksnotice("ckksincoming", self.deps.zoneID, "Processing incoming queue"); |
866f8763 | 382 | |
d64be36e A |
383 | // First, process all item deletions. |
384 | // Then, process all modifications and additions. | |
385 | // Therefore, if there's both a delete and a re-add of a single Primary Key item in the queue, | |
386 | // we should end up with the item still existing in tthe keychain afterward. | |
387 | // But, since we're dropping off the queue inbetween, we might accidentally tell our clients that | |
388 | // their item doesn't exist. Fixing that would take quite a bit of complexity and memory. | |
866f8763 | 389 | |
d64be36e A |
390 | BOOL success = [self loadAndProcessEntriesWithActionFilter:SecCKKSActionDelete]; |
391 | if(!success) { | |
392 | ckksnotice("ckksincoming", self.deps.zoneID, "Early-exiting from IncomingQueueOperation (after processing deletes): %@", self.error); | |
393 | return; | |
394 | } | |
866f8763 | 395 | |
d64be36e A |
396 | success = [self loadAndProcessEntriesWithActionFilter:nil]; |
397 | if(!success) { | |
398 | ckksnotice("ckksincoming", self.deps.zoneID, "Early-exiting from IncomingQueueOperation (after processing all incoming entries): %@", self.error); | |
399 | return; | |
400 | } | |
866f8763 | 401 | |
d64be36e | 402 | ckksnotice("ckksincoming", self.deps.zoneID, "Processed %lu items in incoming queue (%lu errors)", (unsigned long)self.successfulItemsProcessed, (unsigned long)self.errorItemsProcessed); |
866f8763 | 403 | |
d64be36e A |
404 | if(![self fixMismatchedViewItems]) { |
405 | ckksnotice("ckksincoming", ckks, "Early-exiting from IncomingQueueOperation due to failure fixing mismatched items"); | |
406 | return; | |
407 | } | |
866f8763 | 408 | |
d64be36e A |
409 | [self.deps.databaseProvider dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{ |
410 | NSError* error = nil; | |
866f8763 | 411 | |
d64be36e A |
412 | NSArray<CKKSCurrentItemPointer*>* newCIPs = [CKKSCurrentItemPointer remoteItemPointers:self.deps.zoneID error:&error]; |
413 | if(error || !newCIPs) { | |
414 | ckkserror("ckksincoming", self.deps.zoneID, "Could not load remote item pointers: %@", error); | |
415 | } else { | |
416 | if (![self processNewCurrentItemPointers:newCIPs]) { | |
417 | return CKKSDatabaseTransactionRollback; | |
866f8763 | 418 | } |
d64be36e | 419 | ckksnotice("ckksincoming", self.deps.zoneID, "Processed %lu items in CIP queue", (unsigned long)newCIPs.count); |
866f8763 | 420 | } |
d64be36e A |
421 | |
422 | return CKKSDatabaseTransactionCommit; | |
7512f6be A |
423 | }]; |
424 | ||
d64be36e A |
425 | if(self.newOutgoingEntries) { |
426 | //[self.deps.flagHandler handleFlag:CKKSFlagProcessOutgoingQueue]; | |
427 | [ckks processOutgoingQueue:[CKOperationGroup CKKSGroupWithName:@"incoming-queue-response"]]; | |
428 | } | |
429 | ||
430 | if(self.pendingClassAEntries) { | |
431 | [ckks processIncomingQueueAfterNextUnlock]; | |
432 | //OctagonPendingFlag* whenUnlocked = [[OctagonPendingFlag alloc] initWithFlag:CKKSFlagProcessIncomingQueue | |
433 | // conditions:OctagonPendingConditionsDeviceUnlocked]; | |
434 | //[self.deps.flagHandler handlePendingFlag:whenUnlocked]; | |
435 | } | |
436 | ||
437 | for(NSString* viewName in self.viewsToScan) { | |
438 | CKKSKeychainView* view = [[CKKSViewManager manager] findView:viewName]; | |
439 | ckksnotice("ckksincoming", ckks, "Requesting scan for %@ (%@)", view, viewName); | |
440 | [view scanLocalItems:@"policy-mismatch"]; | |
7512f6be A |
441 | } |
442 | ||
d64be36e A |
443 | self.nextState = self.intendedState; |
444 | } | |
445 | ||
446 | - (BOOL)loadAndProcessEntriesWithActionFilter:(NSString* _Nullable)actionFilter | |
447 | { | |
448 | __block bool errored = false; | |
449 | ||
7512f6be A |
450 | // Now for the tricky bit: take and drop the account queue for each batch of queue entries |
451 | // This is for peak memory concerns, but also to allow keychain API clients to make changes while we're processing many items | |
452 | // Note that IncomingQueueOperations are no longer transactional: they can partially succeed. This might make them harder to reason about. | |
453 | __block NSUInteger lastCount = SecCKKSIncomingQueueItemsAtOnce; | |
454 | __block NSString* lastMaxUUID = nil; | |
866f8763 | 455 | |
d64be36e A |
456 | id<CKKSDatabaseProviderProtocol> databaseProvider = self.deps.databaseProvider; |
457 | ||
7512f6be | 458 | while(lastCount == SecCKKSIncomingQueueItemsAtOnce) { |
d64be36e | 459 | [databaseProvider dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{ |
7512f6be | 460 | NSArray<CKKSIncomingQueueEntry*> * queueEntries = nil; |
866f8763 | 461 | if(self.cancelled) { |
d64be36e | 462 | ckksnotice("ckksincoming", self.deps.zoneID, "CKKSIncomingQueueOperation cancelled, quitting"); |
7512f6be | 463 | errored = true; |
d64be36e | 464 | return CKKSDatabaseTransactionRollback; |
866f8763 A |
465 | } |
466 | ||
7512f6be A |
467 | NSError* error = nil; |
468 | ||
d64be36e | 469 | queueEntries = [CKKSIncomingQueueEntry fetch:SecCKKSIncomingQueueItemsAtOnce |
866f8763 A |
470 | startingAtUUID:lastMaxUUID |
471 | state:SecCKKSStateNew | |
d64be36e A |
472 | action:actionFilter |
473 | zoneID:self.deps.zoneID | |
474 | error:&error]; | |
866f8763 A |
475 | |
476 | if(error != nil) { | |
d64be36e | 477 | ckkserror("ckksincoming", self.deps.zoneID, "Error fetching incoming queue records: %@", error); |
866f8763 | 478 | self.error = error; |
d64be36e | 479 | return CKKSDatabaseTransactionRollback; |
866f8763 A |
480 | } |
481 | ||
7512f6be A |
482 | lastCount = queueEntries.count; |
483 | ||
866f8763 A |
484 | if([queueEntries count] == 0) { |
485 | // Nothing to do! exit. | |
d64be36e A |
486 | ckksinfo("ckksincoming", self.deps.zoneID, "Nothing in incoming queue to process (filter: %@)", actionFilter); |
487 | return CKKSDatabaseTransactionCommit; | |
866f8763 A |
488 | } |
489 | ||
d64be36e | 490 | [CKKSPowerCollection CKKSPowerEvent:kCKKSPowerEventIncommingQueue zone:self.deps.zoneID.zoneName count:[queueEntries count]]; |
3f0f0d49 | 491 | |
d64be36e A |
492 | if (![self processQueueEntries:queueEntries]) { |
493 | ckksnotice("ckksincoming", self.deps.zoneID, "processQueueEntries didn't complete successfully"); | |
7512f6be | 494 | errored = true; |
d64be36e | 495 | return CKKSDatabaseTransactionRollback; |
866f8763 | 496 | } |
866f8763 A |
497 | |
498 | // Find the highest UUID for the next fetch. | |
499 | for(CKKSIncomingQueueEntry* iqe in queueEntries) { | |
500 | lastMaxUUID = ([lastMaxUUID compare:iqe.uuid] == NSOrderedDescending) ? lastMaxUUID : iqe.uuid; | |
d64be36e A |
501 | } |
502 | ||
503 | return CKKSDatabaseTransactionCommit; | |
7512f6be A |
504 | }]; |
505 | ||
506 | if(errored) { | |
d64be36e A |
507 | ckksnotice("ckksincoming", self.deps.zoneID, "Early-exiting from IncomingQueueOperation"); |
508 | return false; | |
866f8763 | 509 | } |
7512f6be | 510 | } |
866f8763 | 511 | |
d64be36e A |
512 | return true; |
513 | } | |
514 | - (BOOL)fixMismatchedViewItems | |
515 | { | |
516 | if(!self.handleMismatchedViewItems) { | |
517 | return YES; | |
518 | } | |
7512f6be | 519 | |
d64be36e A |
520 | ckksnotice("ckksincoming", self.deps.zoneID, "Handling policy-mismatched items"); |
521 | __block NSUInteger lastCount = SecCKKSIncomingQueueItemsAtOnce; | |
522 | __block NSString* lastMaxUUID = nil; | |
523 | __block BOOL errored = NO; | |
866f8763 | 524 | |
d64be36e A |
525 | id<CKKSDatabaseProviderProtocol> databaseProvider = self.deps.databaseProvider; |
526 | ||
527 | while(lastCount == SecCKKSIncomingQueueItemsAtOnce) { | |
528 | [databaseProvider dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{ | |
529 | NSError* error = nil; | |
530 | NSArray<CKKSIncomingQueueEntry*>* queueEntries = [CKKSIncomingQueueEntry fetch:SecCKKSIncomingQueueItemsAtOnce | |
531 | startingAtUUID:lastMaxUUID | |
532 | state:SecCKKSStateMismatchedView | |
533 | action:nil | |
534 | zoneID:self.deps.zoneID | |
535 | error:&error]; | |
536 | if(error) { | |
537 | ckksnotice("ckksincoming", self.deps.zoneID, "Cannot fetch mismatched view items"); | |
538 | self.error = error; | |
539 | errored = true; | |
540 | return CKKSDatabaseTransactionRollback; | |
866f8763 | 541 | } |
866f8763 | 542 | |
d64be36e | 543 | lastCount = queueEntries.count; |
866f8763 | 544 | |
d64be36e A |
545 | if(queueEntries.count == 0) { |
546 | ckksnotice("ckksincoming", self.deps.zoneID, "No mismatched view items"); | |
547 | return CKKSDatabaseTransactionCommit; | |
548 | } | |
866f8763 | 549 | |
d64be36e | 550 | ckksnotice("ckksincoming", self.deps.zoneID, "Inspecting %lu mismatched items", (unsigned long)queueEntries.count); |
866f8763 | 551 | |
d64be36e A |
552 | if (![self processQueueEntries:queueEntries]) { |
553 | ckksnotice("ckksincoming", self.deps.zoneID, "processQueueEntries didn't complete successfully"); | |
554 | errored = true; | |
555 | return CKKSDatabaseTransactionRollback; | |
556 | } | |
557 | ||
558 | for(CKKSIncomingQueueEntry* iqe in queueEntries) { | |
559 | lastMaxUUID = ([lastMaxUUID compare:iqe.uuid] == NSOrderedDescending) ? lastMaxUUID : iqe.uuid; | |
560 | } | |
561 | ||
562 | return CKKSDatabaseTransactionCommit; | |
563 | }]; | |
866f8763 A |
564 | } |
565 | ||
d64be36e A |
566 | return !errored; |
567 | } | |
568 | ||
569 | - (void)_onqueueHandleIQEChange:(CKKSIncomingQueueEntry*)iqe | |
570 | attributes:(NSDictionary*)attributes | |
571 | class:(const SecDbClass *)classP | |
572 | sortedForThisView:(BOOL)sortedForThisView | |
573 | { | |
574 | __block CFErrorRef cferror = NULL; | |
575 | SecDbItemRef item = SecDbItemCreateWithAttributes(NULL, classP, (__bridge CFDictionaryRef) attributes, KEYBAG_DEVICE, &cferror); | |
866f8763 | 576 | |
d64be36e A |
577 | if(!item || cferror) { |
578 | ckkserror("ckksincoming", self.deps.zoneID, "Unable to make SecDbItemRef out of attributes: %@", cferror); | |
579 | return; | |
580 | } | |
581 | CFReleaseNull(cferror); | |
582 | ||
583 | [self _onqueueHandleIQEChange:iqe | |
584 | item:item | |
585 | sortedForThisView:sortedForThisView]; | |
586 | ||
587 | CFReleaseNull(item); | |
588 | } | |
589 | ||
590 | - (void)_onqueueHandleIQEChange:(CKKSIncomingQueueEntry*)iqe | |
591 | item:(SecDbItemRef)item | |
592 | sortedForThisView:(BOOL)sortedForThisView | |
593 | { | |
866f8763 A |
594 | bool ok = false; |
595 | __block CFErrorRef cferror = NULL; | |
596 | __block NSError* error = NULL; | |
597 | ||
d64be36e A |
598 | if(SecDbItemIsTombstone(item)) { |
599 | ckkserror("ckksincoming", self.deps.zoneID, "Rejecting a tombstone item addition from CKKS(%@): %@", iqe.uuid, item); | |
600 | ||
601 | NSError* error = nil; | |
602 | CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry withItem:item action:SecCKKSActionDelete zoneID:self.deps.zoneID error:&error]; | |
603 | [oqe saveToDatabase:&error]; | |
604 | ||
605 | if(error) { | |
606 | ckkserror("ckksincoming", self.deps.zoneID, "Unable to save new deletion OQE: %@", error); | |
607 | } else { | |
608 | [iqe deleteFromDatabase: &error]; | |
609 | if(error) { | |
610 | ckkserror("ckksincoming", self.deps.zoneID, "couldn't delete CKKSIncomingQueueEntry: %@", error); | |
611 | self.error = error; | |
612 | self.errorItemsProcessed += 1; | |
613 | } else { | |
614 | self.successfulItemsProcessed += 1; | |
615 | } | |
616 | } | |
617 | self.newOutgoingEntries = true; | |
618 | ||
619 | return; | |
620 | } | |
866f8763 A |
621 | |
622 | __block NSDate* moddate = (__bridge NSDate*) CFDictionaryGetValue(item->attributes, kSecAttrModificationDate); | |
623 | ||
624 | ok = kc_with_dbt(true, &cferror, ^(SecDbConnectionRef dbt){ | |
625 | bool replaceok = SecDbItemInsertOrReplace(item, dbt, &cferror, ^(SecDbItemRef olditem, SecDbItemRef *replace) { | |
d64be36e A |
626 | // If the UUIDs do not match, then check to be sure that the local item is known to CKKS. If not, accept the cloud value. |
627 | // Otherwise, when the UUIDs do not match, then select the item with the 'lower' UUID, and tell CKKS to | |
866f8763 A |
628 | // delete the item with the 'higher' UUID. |
629 | // Otherwise, the cloud wins. | |
630 | ||
d64be36e | 631 | [SecCoreAnalytics sendEvent:SecCKKSAggdPrimaryKeyConflict event:@{SecCoreAnalyticsValue: @1}]; |
866f8763 A |
632 | |
633 | // Note that SecDbItemInsertOrReplace CFReleases any replace pointer it's given, so, be careful | |
634 | ||
635 | if(!CFDictionaryContainsKey(olditem->attributes, kSecAttrUUID)) { | |
636 | // No UUID -> no good. | |
d64be36e | 637 | ckksnotice("ckksincoming", self.deps.zoneID, "Replacing item (it doesn't have a UUID) for %@", iqe.uuid); |
866f8763 A |
638 | if(replace) { |
639 | *replace = CFRetainSafe(item); | |
640 | } | |
641 | return; | |
642 | } | |
643 | ||
d64be36e A |
644 | // If this item arrived in what we believe to be the wrong view, drop the modification entirely. |
645 | if(!sortedForThisView) { | |
646 | ckksnotice("ckksincoming", self.deps.zoneID, "Primary key conflict; dropping CK item (arriving from wrong view) %@", item); | |
647 | return; | |
648 | } | |
649 | ||
866f8763 A |
650 | CFStringRef itemUUID = CFDictionaryGetValue(item->attributes, kSecAttrUUID); |
651 | CFStringRef olditemUUID = CFDictionaryGetValue(olditem->attributes, kSecAttrUUID); | |
652 | ||
d64be36e A |
653 | // Is the old item already somewhere in CKKS? |
654 | NSError* ckmeError = nil; | |
655 | CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase:(__bridge NSString*)olditemUUID | |
656 | zoneID:self.deps.zoneID | |
657 | error:&ckmeError]; | |
658 | ||
659 | if(ckmeError) { | |
660 | ckksnotice("ckksincoming", self.deps.zoneID, "Unable to fetch ckme: %@", ckmeError); | |
661 | // We'll just have to assume that there is a CKME, and let the comparison analysis below win | |
662 | } | |
663 | ||
866f8763 A |
664 | CFComparisonResult compare = CFStringCompare(itemUUID, olditemUUID, 0); |
665 | CKKSOutgoingQueueEntry* oqe = nil; | |
d64be36e | 666 | if (compare == kCFCompareGreaterThan && (ckme || ckmeError)) { |
b54c578e | 667 | // olditem wins; don't change olditem; delete item |
d64be36e A |
668 | ckksnotice("ckksincoming", self.deps.zoneID, "Primary key conflict; dropping CK item %@", item); |
669 | oqe = [CKKSOutgoingQueueEntry withItem:item action:SecCKKSActionDelete zoneID:self.deps.zoneID error:&error]; | |
b54c578e A |
670 | [oqe saveToDatabase: &error]; |
671 | self.newOutgoingEntries = true; | |
672 | moddate = nil; | |
673 | } else { | |
d64be36e A |
674 | // item wins, either due to the UUID winning or the item not being in CKKS yet |
675 | ckksnotice("ckksincoming", self.deps.zoneID, "Primary key conflict; replacing %@%@ with CK item %@", | |
676 | ckme ? @"" : @"non-onboarded", olditem, item); | |
b54c578e A |
677 | if(replace) { |
678 | *replace = CFRetainSafe(item); | |
679 | moddate = (__bridge NSDate*) CFDictionaryGetValue(item->attributes, kSecAttrModificationDate); | |
680 | } | |
681 | // delete olditem if UUID differs (same UUID is the normal update case) | |
682 | if (compare != kCFCompareEqualTo) { | |
d64be36e | 683 | oqe = [CKKSOutgoingQueueEntry withItem:olditem action:SecCKKSActionDelete zoneID:self.deps.zoneID error:&error]; |
866f8763 A |
684 | [oqe saveToDatabase: &error]; |
685 | self.newOutgoingEntries = true; | |
b54c578e | 686 | } |
866f8763 A |
687 | } |
688 | }); | |
689 | ||
690 | // SecDbItemInsertOrReplace returns an error even when it succeeds. | |
691 | if(!replaceok && SecErrorIsSqliteDuplicateItemError(cferror)) { | |
692 | CFReleaseNull(cferror); | |
693 | replaceok = true; | |
694 | } | |
695 | return replaceok; | |
696 | }); | |
697 | ||
866f8763 | 698 | if(cferror) { |
d64be36e | 699 | ckkserror("ckksincoming", self.deps.zoneID, "couldn't process item from IncomingQueue: %@", cferror); |
866f8763 A |
700 | SecTranslateError(&error, cferror); |
701 | self.error = error; | |
702 | ||
703 | iqe.state = SecCKKSStateError; | |
704 | [iqe saveToDatabase:&error]; | |
705 | if(error) { | |
d64be36e | 706 | ckkserror("ckksincoming", self.deps.zoneID, "Couldn't save errored IQE to database: %@", error); |
866f8763 A |
707 | self.error = error; |
708 | } | |
709 | return; | |
710 | } | |
711 | ||
712 | if(error) { | |
d64be36e | 713 | ckkserror("ckksincoming", self.deps.zoneID, "Couldn't handle IQE, but why?: %@", error); |
866f8763 A |
714 | self.error = error; |
715 | return; | |
716 | } | |
717 | ||
718 | if(ok) { | |
d64be36e | 719 | ckksinfo("ckksincoming", self.deps.zoneID, "Correctly processed an IQE; deleting"); |
866f8763 A |
720 | [iqe deleteFromDatabase: &error]; |
721 | ||
722 | if(error) { | |
d64be36e | 723 | ckkserror("ckksincoming", self.deps.zoneID, "couldn't delete CKKSIncomingQueueEntry: %@", error); |
866f8763 | 724 | self.error = error; |
3f0f0d49 A |
725 | self.errorItemsProcessed += 1; |
726 | } else { | |
727 | self.successfulItemsProcessed += 1; | |
866f8763 A |
728 | } |
729 | ||
730 | if(moddate) { | |
b54c578e A |
731 | // Log the number of ms it took to propagate this change |
732 | uint64_t delayInMS = [[NSDate date] timeIntervalSinceDate:moddate] * 1000; | |
d64be36e | 733 | [SecCoreAnalytics sendEvent:@"com.apple.self.deps.item.propagation" event:@{ |
b54c578e A |
734 | @"time" : @(delayInMS) |
735 | }]; | |
736 | ||
866f8763 A |
737 | } |
738 | ||
739 | } else { | |
d64be36e | 740 | ckksnotice("ckksincoming", self.deps.zoneID, "IQE not correctly processed, but why? %@ %@", error, cferror); |
866f8763 A |
741 | self.error = error; |
742 | ||
743 | iqe.state = SecCKKSStateError; | |
744 | [iqe saveToDatabase:&error]; | |
745 | if(error) { | |
d64be36e | 746 | ckkserror("ckksincoming", self.deps.zoneID, "Couldn't save errored IQE to database: %@", error); |
866f8763 A |
747 | self.error = error; |
748 | } | |
3f0f0d49 A |
749 | |
750 | self.errorItemsProcessed += 1; | |
866f8763 A |
751 | } |
752 | } | |
753 | ||
754 | - (void)_onqueueHandleIQEDelete: (CKKSIncomingQueueEntry*) iqe class:(const SecDbClass *)classP { | |
866f8763 A |
755 | bool ok = false; |
756 | __block CFErrorRef cferror = NULL; | |
757 | NSError* error = NULL; | |
758 | NSDictionary* queryAttributes = @{(__bridge NSString*) kSecClass: (__bridge NSString*) classP->name, | |
759 | (__bridge NSString*) kSecAttrUUID: iqe.uuid, | |
866f8763 | 760 | (__bridge NSString*) kSecAttrSynchronizable: @(YES)}; |
d64be36e A |
761 | ckksnotice("ckksincoming", self.deps.zoneID, "trying to delete with query: %@", queryAttributes); |
762 | Query *q = query_create_with_limit( (__bridge CFDictionaryRef) queryAttributes, NULL, kSecMatchUnlimited, NULL, &cferror); | |
763 | q->q_tombstone_use_mdat_from_item = true; | |
866f8763 | 764 | |
866f8763 | 765 | if(cferror) { |
d64be36e | 766 | ckkserror("ckksincoming", self.deps.zoneID, "couldn't create query: %@", cferror); |
866f8763 A |
767 | SecTranslateError(&error, cferror); |
768 | self.error = error; | |
769 | return; | |
770 | } | |
771 | ||
772 | ok = kc_with_dbt(true, &cferror, ^(SecDbConnectionRef dbt) { | |
773 | return s3dl_query_delete(dbt, q, NULL, &cferror); | |
774 | }); | |
775 | ||
776 | if(cferror) { | |
777 | if(CFErrorGetCode(cferror) == errSecItemNotFound) { | |
d64be36e | 778 | ckkserror("ckksincoming", self.deps.zoneID, "couldn't delete item (as it's already gone); this is okay: %@", cferror); |
866f8763 A |
779 | ok = true; |
780 | CFReleaseNull(cferror); | |
781 | } else { | |
d64be36e | 782 | ckkserror("ckksincoming", self.deps.zoneID, "couldn't delete item: %@", cferror); |
866f8763 A |
783 | SecTranslateError(&error, cferror); |
784 | self.error = error; | |
785 | query_destroy(q, NULL); | |
786 | return; | |
787 | } | |
788 | } | |
789 | ||
790 | ||
791 | ok = query_notify_and_destroy(q, ok, &cferror); | |
792 | ||
793 | if(cferror) { | |
d64be36e | 794 | ckkserror("ckksincoming", self.deps.zoneID, "couldn't delete query: %@", cferror); |
866f8763 A |
795 | SecTranslateError(&error, cferror); |
796 | self.error = error; | |
797 | return; | |
798 | } | |
799 | ||
800 | if(ok) { | |
d64be36e | 801 | ckksnotice("ckksincoming", self.deps.zoneID, "Correctly processed an IQE; deleting"); |
866f8763 A |
802 | [iqe deleteFromDatabase: &error]; |
803 | ||
804 | if(error) { | |
d64be36e | 805 | ckkserror("ckksincoming", self.deps.zoneID, "couldn't delete CKKSIncomingQueueEntry: %@", error); |
866f8763 | 806 | self.error = error; |
3f0f0d49 A |
807 | self.errorItemsProcessed += 1; |
808 | } else { | |
809 | self.successfulItemsProcessed += 1; | |
866f8763 A |
810 | } |
811 | } else { | |
d64be36e | 812 | ckkserror("ckksincoming", self.deps.zoneID, "IQE not correctly processed, but why? %@ %@", error, cferror); |
866f8763 | 813 | self.error = error; |
3f0f0d49 | 814 | self.errorItemsProcessed += 1; |
866f8763 A |
815 | } |
816 | } | |
817 | ||
818 | @end; | |
819 | ||
820 | #endif |