]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSIncomingQueueOperation.m
Security-59754.41.1.tar.gz
[apple/security.git] / keychain / ckks / CKKSIncomingQueueOperation.m
1 /*
2 * Copyright (c) 2016-2020 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 "keychain/analytics/CKKSPowerCollection.h"
25 #import "keychain/ckks/CKKSAnalytics.h"
26 #import "keychain/ckks/CKKSCurrentItemPointer.h"
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"
36 #import "keychain/ot/ObjCImprovements.h"
37
38 #include "keychain/securityd/SecItemServer.h"
39 #include "keychain/securityd/SecItemDb.h"
40 #include <Security/SecItemPriv.h>
41
42 #import <utilities/SecCoreAnalytics.h>
43
44 #if OCTAGON
45
46 @interface CKKSIncomingQueueOperation ()
47 @property bool newOutgoingEntries;
48 @property bool pendingClassAEntries;
49 @property bool missingKey;
50
51 @property NSMutableSet<NSString*>* viewsToScan;
52 @end
53
54 @implementation CKKSIncomingQueueOperation
55 @synthesize nextState = _nextState;
56 @synthesize intendedState = _intendedState;
57
58 - (instancetype)init {
59 return nil;
60 }
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 {
69 if(self = [super init]) {
70 _deps = dependencies;
71 _ckks = ckks;
72
73 _intendedState = intending;
74 _nextState = errorState;
75
76 // Can't process unless we have a reasonable key hierarchy.
77 if(ckks.keyStateReadyDependency) {
78 [self addDependency: ckks.keyStateReadyDependency];
79 }
80
81 [self addNullableDependency: ckks.holdIncomingQueueOperation];
82
83 _errorOnClassAFailure = errorOnClassAFailure;
84 _pendingClassAEntries = false;
85
86 _handleMismatchedViewItems = handleMismatchedViewItems;
87
88 _viewsToScan = [NSMutableSet set];
89
90 [self linearDependencies:ckks.incomingQueueOperations];
91 }
92 return self;
93 }
94
95 - (bool)processNewCurrentItemPointers:(NSArray<CKKSCurrentItemPointer*>*)queueEntries
96 {
97 NSError* error = nil;
98 for(CKKSCurrentItemPointer* p in queueEntries) {
99 @autoreleasepool {
100 p.state = SecCKKSProcessedStateLocal;
101
102 [p saveToDatabase:&error];
103 ckksnotice("ckkspointer", self.deps.zoneID, "Saving new current item pointer: %@", p);
104 if(error) {
105 ckkserror("ckksincoming", self.deps.zoneID, "Error saving new current item pointer: %@ %@", error, p);
106 }
107 }
108 }
109
110 if(queueEntries.count > 0) {
111 [self.deps.notifyViewChangedScheduler trigger];
112 }
113
114 return (error == nil);
115 }
116
117 - (bool)processQueueEntries:(NSArray<CKKSIncomingQueueEntry*>*)queueEntries
118 {
119 CKKSKeychainView* ckks = self.ckks;
120 dispatch_assert_queue(ckks.queue);
121
122 NSMutableArray* newOrChangedRecords = [[NSMutableArray alloc] init];
123 NSMutableArray* deletedRecordIDs = [[NSMutableArray alloc] init];
124
125 for(id entry in queueEntries) {
126 @autoreleasepool {
127 NSError* error = nil;
128
129 CKKSIncomingQueueEntry* iqe = (CKKSIncomingQueueEntry*) entry;
130 ckksnotice("ckksincoming", self.deps.zoneID, "ready to process an incoming queue entry: %@ %@ %@", iqe, iqe.uuid, iqe.action);
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...
135 NSDictionary* attributes = [CKKSIncomingQueueOperation decryptCKKSItemToAttributes:iqe.item error:&error];
136
137 if(!attributes || error) {
138 if([self.deps.lockStateTracker isLockedError:error]) {
139 NSError* localerror = nil;
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];
142 if(localerror || ([key.keyclass isEqualToString:SecCKKSKeyClassA] && self.errorOnClassAFailure)) {
143 self.error = error;
144 }
145
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 }
150
151 } else if ([error.domain isEqualToString:@"securityd"] && error.code == errSecItemNotFound) {
152 ckkserror("ckksincoming", self.deps.zoneID, "Coudn't find key in keychain; will attempt to poke key hierarchy: %@", error)
153 self.missingKey = true;
154 self.error = error;
155
156 } else {
157 ckkserror("ckksincoming", self.deps.zoneID, "Couldn't decrypt IQE %@ for some reason: %@", iqe, error);
158 self.error = error;
159 }
160 self.errorItemsProcessed += 1;
161 continue;
162 }
163
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]}];
169 ckkserror("ckksincoming", self.deps.zoneID, "Synced item seems wrong: %@", self.error);
170 self.errorItemsProcessed += 1;
171 continue;
172 }
173
174 const SecDbClass * classP = !classStr ? NULL : kc_class_with_name((__bridge CFStringRef) classStr);
175
176 if(!classP) {
177 ckkserror("ckksincoming", self.deps.zoneID, "unknown class in object: %@ %@", classStr, iqe);
178 iqe.state = SecCKKSStateError;
179 [iqe saveToDatabase:&error];
180 if(error) {
181 ckkserror("ckksincoming", self.deps.zoneID, "Couldn't save errored IQE to database: %@", error);
182 self.error = error;
183 }
184 self.errorItemsProcessed += 1;
185 continue;
186 }
187
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);
197
198 [self _onqueueUpdateIQE:iqe withState:SecCKKSStateMismatchedView error:&error];
199 if(error) {
200 ckkserror("ckksincoming", ckks, "Couldn't save mismatched IQE to database: %@", error);
201 self.errorItemsProcessed += 1;
202 self.error = error;
203 }
204
205 [ckks receivedItemForWrongView];
206 }
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]];
220 }
221 }
222 }
223
224 if(newOrChangedRecords.count > 0 || deletedRecordIDs > 0) {
225 // Schedule a view change notification
226 [self.deps.notifyViewChangedScheduler trigger];
227 }
228
229 if(self.missingKey) {
230 // TODO: will be removed when the IncomingQueueOperation is part of the state machine
231 [ckks.stateMachine _onqueuePokeStateMachine];
232 self.nextState = SecCKKSZoneKeyStateUnhealthy;
233 }
234
235 return true;
236 }
237
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
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
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
347 - (void)main
348 {
349 CKKSKeychainView* ckks = self.ckks;
350
351 if(!ckks.itemSyncingEnabled) {
352 ckkserror("ckksincoming", self.deps.zoneID, "Item syncing for this view is disabled");
353 self.nextState = self.intendedState;
354 return;
355 }
356
357 WEAKIFY(self);
358 self.completionBlock = ^(void) {
359 STRONGIFY(self);
360 if (!self) {
361 ckkserror("ckksincoming", self.deps.zoneID, "received callback for released object");
362 return;
363 }
364
365 CKKSAnalytics* logger = [CKKSAnalytics logger];
366
367 if (!self.error) {
368 [logger logSuccessForEvent:CKKSEventProcessIncomingQueueClassC zoneName:self.deps.zoneID.zoneName];
369
370 if (!self.pendingClassAEntries) {
371 [logger logSuccessForEvent:CKKSEventProcessIncomingQueueClassA zoneName:self.deps.zoneID.zoneName];
372 }
373 } else {
374 [logger logRecoverableError:self.error
375 forEvent:self.errorOnClassAFailure ? CKKSEventProcessIncomingQueueClassA : CKKSEventProcessIncomingQueueClassC
376 zoneName:self.deps.zoneID.zoneName
377 withAttributes:NULL];
378 }
379 };
380
381 ckksnotice("ckksincoming", self.deps.zoneID, "Processing incoming queue");
382
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.
389
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 }
395
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 }
401
402 ckksnotice("ckksincoming", self.deps.zoneID, "Processed %lu items in incoming queue (%lu errors)", (unsigned long)self.successfulItemsProcessed, (unsigned long)self.errorItemsProcessed);
403
404 if(![self fixMismatchedViewItems]) {
405 ckksnotice("ckksincoming", ckks, "Early-exiting from IncomingQueueOperation due to failure fixing mismatched items");
406 return;
407 }
408
409 [self.deps.databaseProvider dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
410 NSError* error = nil;
411
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;
418 }
419 ckksnotice("ckksincoming", self.deps.zoneID, "Processed %lu items in CIP queue", (unsigned long)newCIPs.count);
420 }
421
422 return CKKSDatabaseTransactionCommit;
423 }];
424
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"];
441 }
442
443 self.nextState = self.intendedState;
444 }
445
446 - (BOOL)loadAndProcessEntriesWithActionFilter:(NSString* _Nullable)actionFilter
447 {
448 __block bool errored = false;
449
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;
455
456 id<CKKSDatabaseProviderProtocol> databaseProvider = self.deps.databaseProvider;
457
458 while(lastCount == SecCKKSIncomingQueueItemsAtOnce) {
459 [databaseProvider dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
460 NSArray<CKKSIncomingQueueEntry*> * queueEntries = nil;
461 if(self.cancelled) {
462 ckksnotice("ckksincoming", self.deps.zoneID, "CKKSIncomingQueueOperation cancelled, quitting");
463 errored = true;
464 return CKKSDatabaseTransactionRollback;
465 }
466
467 NSError* error = nil;
468
469 queueEntries = [CKKSIncomingQueueEntry fetch:SecCKKSIncomingQueueItemsAtOnce
470 startingAtUUID:lastMaxUUID
471 state:SecCKKSStateNew
472 action:actionFilter
473 zoneID:self.deps.zoneID
474 error:&error];
475
476 if(error != nil) {
477 ckkserror("ckksincoming", self.deps.zoneID, "Error fetching incoming queue records: %@", error);
478 self.error = error;
479 return CKKSDatabaseTransactionRollback;
480 }
481
482 lastCount = queueEntries.count;
483
484 if([queueEntries count] == 0) {
485 // Nothing to do! exit.
486 ckksinfo("ckksincoming", self.deps.zoneID, "Nothing in incoming queue to process (filter: %@)", actionFilter);
487 return CKKSDatabaseTransactionCommit;
488 }
489
490 [CKKSPowerCollection CKKSPowerEvent:kCKKSPowerEventIncommingQueue zone:self.deps.zoneID.zoneName count:[queueEntries count]];
491
492 if (![self processQueueEntries:queueEntries]) {
493 ckksnotice("ckksincoming", self.deps.zoneID, "processQueueEntries didn't complete successfully");
494 errored = true;
495 return CKKSDatabaseTransactionRollback;
496 }
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;
501 }
502
503 return CKKSDatabaseTransactionCommit;
504 }];
505
506 if(errored) {
507 ckksnotice("ckksincoming", self.deps.zoneID, "Early-exiting from IncomingQueueOperation");
508 return false;
509 }
510 }
511
512 return true;
513 }
514 - (BOOL)fixMismatchedViewItems
515 {
516 if(!self.handleMismatchedViewItems) {
517 return YES;
518 }
519
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;
524
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;
541 }
542
543 lastCount = queueEntries.count;
544
545 if(queueEntries.count == 0) {
546 ckksnotice("ckksincoming", self.deps.zoneID, "No mismatched view items");
547 return CKKSDatabaseTransactionCommit;
548 }
549
550 ckksnotice("ckksincoming", self.deps.zoneID, "Inspecting %lu mismatched items", (unsigned long)queueEntries.count);
551
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 }];
564 }
565
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);
576
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 {
594 bool ok = false;
595 __block CFErrorRef cferror = NULL;
596 __block NSError* error = NULL;
597
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 }
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) {
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
628 // delete the item with the 'higher' UUID.
629 // Otherwise, the cloud wins.
630
631 [SecCoreAnalytics sendEvent:SecCKKSAggdPrimaryKeyConflict event:@{SecCoreAnalyticsValue: @1}];
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.
637 ckksnotice("ckksincoming", self.deps.zoneID, "Replacing item (it doesn't have a UUID) for %@", iqe.uuid);
638 if(replace) {
639 *replace = CFRetainSafe(item);
640 }
641 return;
642 }
643
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
650 CFStringRef itemUUID = CFDictionaryGetValue(item->attributes, kSecAttrUUID);
651 CFStringRef olditemUUID = CFDictionaryGetValue(olditem->attributes, kSecAttrUUID);
652
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
664 CFComparisonResult compare = CFStringCompare(itemUUID, olditemUUID, 0);
665 CKKSOutgoingQueueEntry* oqe = nil;
666 if (compare == kCFCompareGreaterThan && (ckme || ckmeError)) {
667 // olditem wins; don't change olditem; delete item
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];
670 [oqe saveToDatabase: &error];
671 self.newOutgoingEntries = true;
672 moddate = nil;
673 } else {
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);
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) {
683 oqe = [CKKSOutgoingQueueEntry withItem:olditem action:SecCKKSActionDelete zoneID:self.deps.zoneID error:&error];
684 [oqe saveToDatabase: &error];
685 self.newOutgoingEntries = true;
686 }
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
698 if(cferror) {
699 ckkserror("ckksincoming", self.deps.zoneID, "couldn't process item from IncomingQueue: %@", cferror);
700 SecTranslateError(&error, cferror);
701 self.error = error;
702
703 iqe.state = SecCKKSStateError;
704 [iqe saveToDatabase:&error];
705 if(error) {
706 ckkserror("ckksincoming", self.deps.zoneID, "Couldn't save errored IQE to database: %@", error);
707 self.error = error;
708 }
709 return;
710 }
711
712 if(error) {
713 ckkserror("ckksincoming", self.deps.zoneID, "Couldn't handle IQE, but why?: %@", error);
714 self.error = error;
715 return;
716 }
717
718 if(ok) {
719 ckksinfo("ckksincoming", self.deps.zoneID, "Correctly processed an IQE; deleting");
720 [iqe deleteFromDatabase: &error];
721
722 if(error) {
723 ckkserror("ckksincoming", self.deps.zoneID, "couldn't delete CKKSIncomingQueueEntry: %@", error);
724 self.error = error;
725 self.errorItemsProcessed += 1;
726 } else {
727 self.successfulItemsProcessed += 1;
728 }
729
730 if(moddate) {
731 // Log the number of ms it took to propagate this change
732 uint64_t delayInMS = [[NSDate date] timeIntervalSinceDate:moddate] * 1000;
733 [SecCoreAnalytics sendEvent:@"com.apple.self.deps.item.propagation" event:@{
734 @"time" : @(delayInMS)
735 }];
736
737 }
738
739 } else {
740 ckksnotice("ckksincoming", self.deps.zoneID, "IQE not correctly processed, but why? %@ %@", error, cferror);
741 self.error = error;
742
743 iqe.state = SecCKKSStateError;
744 [iqe saveToDatabase:&error];
745 if(error) {
746 ckkserror("ckksincoming", self.deps.zoneID, "Couldn't save errored IQE to database: %@", error);
747 self.error = error;
748 }
749
750 self.errorItemsProcessed += 1;
751 }
752 }
753
754 - (void)_onqueueHandleIQEDelete: (CKKSIncomingQueueEntry*) iqe class:(const SecDbClass *)classP {
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,
760 (__bridge NSString*) kSecAttrSynchronizable: @(YES)};
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;
764
765 if(cferror) {
766 ckkserror("ckksincoming", self.deps.zoneID, "couldn't create query: %@", cferror);
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) {
778 ckkserror("ckksincoming", self.deps.zoneID, "couldn't delete item (as it's already gone); this is okay: %@", cferror);
779 ok = true;
780 CFReleaseNull(cferror);
781 } else {
782 ckkserror("ckksincoming", self.deps.zoneID, "couldn't delete item: %@", cferror);
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) {
794 ckkserror("ckksincoming", self.deps.zoneID, "couldn't delete query: %@", cferror);
795 SecTranslateError(&error, cferror);
796 self.error = error;
797 return;
798 }
799
800 if(ok) {
801 ckksnotice("ckksincoming", self.deps.zoneID, "Correctly processed an IQE; deleting");
802 [iqe deleteFromDatabase: &error];
803
804 if(error) {
805 ckkserror("ckksincoming", self.deps.zoneID, "couldn't delete CKKSIncomingQueueEntry: %@", error);
806 self.error = error;
807 self.errorItemsProcessed += 1;
808 } else {
809 self.successfulItemsProcessed += 1;
810 }
811 } else {
812 ckkserror("ckksincoming", self.deps.zoneID, "IQE not correctly processed, but why? %@ %@", error, cferror);
813 self.error = error;
814 self.errorItemsProcessed += 1;
815 }
816 }
817
818 @end;
819
820 #endif