2 * Copyright (c) 2016 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
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
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.
21 * @APPLE_LICENSE_HEADER_END@
25 #import <TrustedPeers/TPSyncingPolicy.h>
26 #import <TrustedPeers/TPPBPolicyKeyViewMapping.h>
27 #import <TrustedPeers/TPDictionaryMatchingRules.h>
29 #import "keychain/ckks/CKKSAnalytics.h"
30 #import "keychain/ckks/CKKSKeychainView.h"
31 #import "keychain/ckks/CKKSNearFutureScheduler.h"
32 #import "keychain/ckks/CKKSScanLocalItemsOperation.h"
33 #import "keychain/ckks/CKKSMirrorEntry.h"
34 #import "keychain/ckks/CKKSIncomingQueueEntry.h"
35 #import "keychain/ckks/CKKSOutgoingQueueEntry.h"
36 #import "keychain/ckks/CKKSGroupOperation.h"
37 #import "keychain/ckks/CKKSKey.h"
38 #import "keychain/ckks/CKKSViewManager.h"
39 #import "keychain/ckks/CKKSItemEncrypter.h"
40 #import "keychain/ckks/CKKSStates.h"
41 #import "keychain/ckks/CKKSZoneStateEntry.h"
43 #import "CKKSPowerCollection.h"
45 #include "keychain/securityd/SecItemSchema.h"
46 #include "keychain/securityd/SecItemServer.h"
47 #include "keychain/securityd/SecItemDb.h"
48 #include <Security/SecItemPriv.h>
49 #include <utilities/SecInternalReleasePriv.h>
50 #import <IMCore/IMCore_Private.h>
51 #import <IMCore/IMCloudKitHooks.h>
53 @interface CKKSScanLocalItemsOperation ()
54 @property (assign) NSUInteger processedItems;
56 @property BOOL newCKKSEntries;
59 @implementation CKKSScanLocalItemsOperation
60 @synthesize nextState = _nextState;
61 @synthesize intendedState = _intendedState;
63 - (instancetype)init {
66 - (instancetype)initWithDependencies:(CKKSOperationDependencies*)dependencies
67 ckks:(CKKSKeychainView*)ckks
68 intending:(OctagonState*)intendedState
69 errorState:(OctagonState*)errorState
70 ckoperationGroup:(CKOperationGroup*)ckoperationGroup
72 if((self = [super init])) {
75 _ckoperationGroup = ckoperationGroup;
77 _nextState = errorState;
78 _intendedState = intendedState;
86 - (NSDictionary*)queryPredicatesForViewMapping {
87 TPPBPolicyKeyViewMapping* viewRule = nil;
89 // If there's more than one rule matching this view, then exit with an empty dictionary: the language doesn't support ORs.
90 for(TPPBPolicyKeyViewMapping* mapping in [CKKSViewManager manager].policy.keyViewMapping) {
91 if([mapping.view isEqualToString:self.deps.zoneID.zoneName]) {
95 // Too many rules for this view! Don't perform optimization.
96 ckksnotice("ckksscan", self.deps.zoneID, "Too many policy rules for view %@", self.deps.zoneID.zoneName);
102 if(viewRule.hasMatchingRule &&
103 viewRule.matchingRule.andsCount == 0 &&
104 viewRule.matchingRule.orsCount == 0 &&
105 !viewRule.matchingRule.hasNot &&
106 !viewRule.matchingRule.hasExists &&
107 viewRule.matchingRule.hasMatch) {
108 if([((id)kSecAttrSyncViewHint) isEqualToString:viewRule.matchingRule.match.fieldName] &&
109 [viewRule.matchingRule.match.regex isEqualToString:[NSString stringWithFormat:@"^%@$", self.deps.zoneID.zoneName]]) {
111 (id)kSecAttrSyncViewHint: self.deps.zoneID.zoneName,
113 } else if([((id)kSecAttrAccessGroup) isEqualToString:viewRule.matchingRule.match.fieldName] &&
114 [viewRule.matchingRule.match.regex isEqualToString:@"^com\\.apple\\.cfnetwork$"]) {
115 // We can't match on any regex agrp match, because it might be some actually difficult regex. But, we know about this one!
117 (id)kSecAttrAccessGroup: @"com.apple.cfnetwork",
120 } else if([((id)kSecAttrAccessGroup) isEqualToString:viewRule.matchingRule.match.fieldName] &&
121 [viewRule.matchingRule.match.regex isEqualToString:@"^com\\.apple\\.safari\\.credit-cards$"]) {
122 // We can't match on any regex agrp match, because it might be some actually difficult regex. But, we know about this one!
124 (id)kSecAttrAccessGroup: @"com.apple.safari.credit-cards",
128 ckksnotice("ckksscan", self.deps.zoneID, "Policy view rule is not a match against viewhint: %@", viewRule);
131 ckksnotice("ckksscan", self.deps.zoneID, "Policy view rule is complex: %@", viewRule);
137 - (BOOL)executeQuery:(NSDictionary*)queryPredicates readWrite:(bool)readWrite error:(NSError**)error block:(void (^_Nonnull)(SecDbItemRef item))block
139 __block CFErrorRef cferror = NULL;
140 __block bool ok = false;
142 Query *q = query_create_with_limit((__bridge CFDictionaryRef)queryPredicates, NULL, kSecMatchUnlimited, NULL, &cferror);
145 ckkserror("ckksscan", self.deps.zoneID, "couldn't create query: %@", cferror);
146 SecTranslateError(error, cferror);
150 ok = kc_with_dbt(readWrite, &cferror, ^(SecDbConnectionRef dbt) {
151 return SecDbItemQuery(q, NULL, dbt, &cferror, ^(SecDbItemRef item, bool *stop) {
157 ok = query_notify_and_destroy(q, ok, &cferror);
159 ok = query_destroy(q, &cferror);
163 ckkserror("ckksscan", self.deps.zoneID, "couldn't execute query: %@", cferror);
164 SecTranslateError(error, cferror);
171 - (BOOL)onboardItemToCKKS:(SecDbItemRef)item error:(NSError**)error
173 NSError* itemSaveError = nil;
175 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry withItem:item
176 action:SecCKKSActionAdd
177 zoneID:self.deps.zoneID
178 error:&itemSaveError];
181 ckkserror("ckksscan", self.deps.zoneID, "Need to upload %@, but can't create outgoing entry: %@", item, itemSaveError);
183 *error = itemSaveError;
188 ckksnotice("ckksscan", self.deps.zoneID, "Syncing new item: %@", oqe);
190 [oqe saveToDatabase:&itemSaveError];
192 ckkserror("ckksscan", self.deps.zoneID, "Need to upload %@, but can't save to database: %@", oqe, itemSaveError);
193 self.error = itemSaveError;
197 self.newCKKSEntries = true;
198 self.recordsAdded += 1;
203 - (void)onboardItemsWithUUIDs:(NSSet<NSString*>*)uuids itemClass:(NSString*)itemClass databaseProvider:(id<CKKSDatabaseProviderProtocol>)databaseProvider
205 ckksnotice("ckksscan", self.deps.zoneID, "Found %d missing %@ items", (int)uuids.count, itemClass);
206 // Use one transaction for each item to allow for SecItem API calls to interleave
207 for(NSString* itemUUID in uuids) {
208 [databaseProvider dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult {
209 NSDictionary* queryAttributes = @{
210 (id)kSecClass: itemClass,
211 (id)kSecReturnRef: @(YES),
212 (id)kSecAttrSynchronizable: @(YES),
213 (id)kSecAttrTombstone: @(NO),
214 (id)kSecAttrUUID: itemUUID,
217 ckksnotice("ckksscan", self.deps.zoneID, "Onboarding %@ %@", itemClass, itemUUID);
219 __block NSError* itemSaveError = nil;
221 [self executeQuery:queryAttributes readWrite:false error:&itemSaveError block:^(SecDbItemRef itemToSave) {
222 [self onboardItemToCKKS:itemToSave error:&itemSaveError];
226 ckkserror("ckksscan", self.deps.zoneID, "Need to upload %@, but can't save to database: %@", itemUUID, itemSaveError);
227 self.error = itemSaveError;
228 return CKKSDatabaseTransactionRollback;
231 return CKKSDatabaseTransactionCommit;
236 - (void)fixUUIDlessItemsWithPrimaryKeys:(NSMutableSet<NSDictionary*>*)primaryKeys databaseProvider:(id<CKKSDatabaseProviderProtocol>)databaseProvider
238 ckksnotice("ckksscan", self.deps.zoneID, "Found %d items missing UUIDs", (int)primaryKeys.count);
240 if([primaryKeys count] == 0) {
244 [databaseProvider dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
245 __block NSError* itemError = nil;
247 for(NSDictionary* primaryKey in primaryKeys) {
248 ckksnotice("ckksscan", self.deps.zoneID, "Found item with no uuid: %@", primaryKey);
250 __block CFErrorRef cferror = NULL;
252 bool connectionSuccess = kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
253 Query *q = query_create_with_limit((__bridge CFDictionaryRef)primaryKey, NULL, kSecMatchUnlimited, NULL, &cferror);
256 ckkserror("ckksscan", self.deps.zoneID, "couldn't create query: %@", cferror);
260 __block bool ok = true;
262 ok &= SecDbItemQuery(q, NULL, dbt, &cferror, ^(SecDbItemRef uuidlessItem, bool *stop) {
263 NSString* uuid = [[NSUUID UUID] UUIDString];
264 NSDictionary* updates = @{(id)kSecAttrUUID: uuid};
266 ckksnotice("ckksscan", self.deps.zoneID, "Assigning new UUID %@ for item %@", uuid, uuidlessItem);
268 SecDbItemRef new_item = SecDbItemCopyWithUpdates(uuidlessItem, (__bridge CFDictionaryRef)updates, &cferror);
271 SecTranslateError(&itemError, cferror);
272 self.error = itemError;
273 ckksnotice("ckksscan", self.deps.zoneID, "Unable to copy item with new UUID: %@", cferror);
277 bool updateSuccess = kc_transaction_type(dbt, kSecDbExclusiveRemoteCKKSTransactionType, &cferror, ^{
278 return SecDbItemUpdate(uuidlessItem, new_item, dbt, kCFBooleanFalse, q->q_uuid_from_primary_key, &cferror);
282 [self onboardItemToCKKS:new_item error:&itemError];
284 ckksnotice("ckksscan", self.deps.zoneID, "Unable to update item with new UUID: %@", cferror);
290 ok &= query_notify_and_destroy(q, ok, &cferror);
295 if(!connectionSuccess) {
296 ckkserror("ckksscan", self.deps.zoneID, "couldn't execute query: %@", cferror);
297 SecTranslateError(&itemError, cferror);
298 self.error = itemError;
299 return CKKSDatabaseTransactionRollback;
303 return CKKSDatabaseTransactionCommit;
307 - (void)retriggerMissingMirrorEntires:(NSSet<NSString*>*)mirrorUUIDs
308 ckks:(CKKSKeychainView*)ckks
309 databaseProvider:(id<CKKSDatabaseProviderProtocol>)databaseProvider
311 if (mirrorUUIDs.count > 0) {
312 [databaseProvider dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
313 NSError* error = nil;
314 ckkserror("ckksscan", self.deps.zoneID, "BUG: keychain missing %lu items from mirror and/or queues: %@", (unsigned long)mirrorUUIDs.count, mirrorUUIDs);
315 self.missingLocalItemsFound = mirrorUUIDs.count;
317 [[CKKSAnalytics logger] logMetric:[NSNumber numberWithUnsignedInteger:mirrorUUIDs.count] withName:CKKSEventMissingLocalItemsFound];
319 for (NSString* uuid in mirrorUUIDs) {
320 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase:uuid zoneID:self.deps.zoneID error:&error];
323 ckkserror("ckksscan", self.deps.zoneID, "BUG: error fetching previously-extant CKME (uuid: %@) from database: %@", uuid, error);
326 [ckks _onqueueCKRecordChanged:ckme.item.storedCKRecord resync:true];
330 // And, if you're not in the tests, try to collect a sysdiagnose I guess?
331 // <rdar://problem/36166435> Re-enable IMCore autosysdiagnose capture to securityd
332 //if(SecIsInternalRelease() && !SecCKKSTestsEnabled()) {
333 // [[IMCloudKitHooks sharedInstance] tryToAutoCollectLogsWithErrorString:@"35810558" sendLogsTo:@"rowdy_bot@icloud.com"];
335 return CKKSDatabaseTransactionCommit;
338 ckksnotice("ckksscan", self.deps.zoneID,"No missing local items found");
344 if(SecCKKSTestsEnabled() && SecCKKSTestSkipScan()) {
345 ckksnotice("ckksscan", self.deps.zoneID, "Scan cancelled by test request");
349 // We need to not be jetsamed while running this
350 os_transaction_t transaction = os_transaction_create([[NSString stringWithFormat:@"com.apple.securityd.ckks.scan.%@", self.deps.zoneID] UTF8String]);
352 id<CKKSDatabaseProviderProtocol> databaseProvider = self.deps.databaseProvider;
353 CKKSKeychainView* ckks = self.ckks;
355 [self.deps.launch addEvent:@"scan-local-items"];
357 // A map of ItemClass -> Set of found UUIDs
358 NSMutableDictionary<NSString*, NSMutableSet<NSString*>*>* itemUUIDsNotYetInCKKS = [NSMutableDictionary dictionary];
360 // A list of primary keys of items that fit in this view, but have no UUIDs
361 NSMutableSet<NSDictionary*>* primaryKeysWithNoUUIDs = [NSMutableSet set];
363 // We want this set to be empty after scanning, or else the keychain (silently) dropped something on the floor
364 NSMutableSet<NSString*>* mirrorUUIDs = [NSMutableSet set];
366 [databaseProvider dispatchSyncWithReadOnlySQLTransaction:^{
367 // First, query for all synchronizable items
368 __block NSError* error = nil;
370 [mirrorUUIDs addObjectsFromArray:[CKKSMirrorEntry allUUIDs:self.deps.zoneID error:&error]];
372 // Must query per-class, so:
373 const SecDbSchema *newSchema = current_schema();
374 for (const SecDbClass *const *class = newSchema->classes; *class != NULL; class++) {
375 if(!((*class)->itemclass)) {
376 // Don't try to scan non-item 'classes'
380 NSString* itemClass = (__bridge NSString*)(*class)->name;
382 NSMutableDictionary* queryAttributes = [
383 @{(__bridge NSString*)kSecClass: itemClass,
384 (__bridge NSString*)kSecReturnRef: @(YES),
385 (__bridge NSString*)kSecAttrSynchronizable: @(YES),
386 (__bridge NSString*)kSecAttrTombstone: @(NO),
389 NSDictionary* extraQueryPredicates = [self queryPredicatesForViewMapping];
390 [queryAttributes addEntriesFromDictionary:extraQueryPredicates];
392 ckksnotice("ckksscan", self.deps.zoneID, "Scanning all synchronizable %@ items(%@) for: %@", itemClass, self.name, queryAttributes);
394 [self executeQuery:queryAttributes readWrite:false error:&error block:^(SecDbItemRef item) {
395 ckksnotice("ckksscan", self.deps.zoneID, "scanning item: %@", item);
397 self.processedItems += 1;
399 // First check: is this a tombstone? If so, skip with prejudice.
400 if(SecDbItemIsTombstone(item)) {
401 ckksinfo("ckksscan", self.deps.zoneID, "Skipping tombstone %@", item);
405 // Second check: is this item a CKKS key for a view? If so, skip.
406 if([CKKSKey isItemKeyForKeychainView:item] != nil) {
407 ckksinfo("ckksscan", self.deps.zoneID, "Scanned item is a CKKS internal key, skipping");
411 // Third check: What view is this for?
412 NSString* viewForItem = [[CKKSViewManager manager] viewNameForItem:item];
413 if(![viewForItem isEqualToString:self.deps.zoneID.zoneName]) {
414 ckksinfo("ckksscan", self.deps.zoneID, "Scanned item is for view %@, skipping", viewForItem);
418 // Fourth check: does this item have a UUID? If not, mark for later onboarding.
419 CFErrorRef cferror = NULL;
421 NSString* uuid = (__bridge_transfer NSString*) CFRetain(SecDbItemGetValue(item, &v10itemuuid, &cferror));
422 if(!uuid || [uuid isEqual: [NSNull null]]) {
423 ckksnotice("ckksscan", self.deps.zoneID, "making new UUID for item %@: %@", item, cferror);
425 NSMutableDictionary* primaryKey = [(NSDictionary*)CFBridgingRelease(SecDbItemCopyPListWithMask(item, kSecDbPrimaryKeyFlag, &cferror)) mutableCopy];
427 // Class is an important part of a primary key, SecDb
428 primaryKey[(id)kSecClass] = itemClass;
430 if(SecErrorGetOSStatus(cferror) != errSecSuccess) {
431 ckkserror("ckksscan", self.deps.zoneID, "couldn't copy UUID-less item's primary key: %@", cferror);
432 SecTranslateError(&error, cferror);
437 [primaryKeysWithNoUUIDs addObject:primaryKey];
441 // Is there a known sync item with this UUID?
442 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase:uuid
443 zoneID:self.deps.zoneID
446 [mirrorUUIDs removeObject:uuid];
447 ckksinfo("ckksscan", self.deps.zoneID, "Existing mirror entry with UUID %@", uuid);
449 if([self areEquivalent:item ckksItem:ckme.item]) {
453 ckksnotice("ckksscan", self.deps.zoneID, "Existing mirror entry with UUID %@ does not match local item", uuid);
457 // We don't care about the oqe state here, just that one exists
458 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry tryFromDatabase:uuid
459 zoneID:self.deps.zoneID
462 ckksnotice("ckksscan", self.deps.zoneID, "Existing outgoing queue entry with UUID %@", uuid);
463 // If its state is 'new', mark down that we've seen new entries that need processing
464 self.newCKKSEntries |= !![oqe.state isEqualToString:SecCKKSStateNew];
468 // Hurray, we can help!
469 ckksnotice("ckksscan", self.deps.zoneID, "Item(%@) is new; will attempt to add to CKKS", uuid);
470 self.recordsFound += 1;
472 NSMutableSet<NSString*>* classUUIDs = itemUUIDsNotYetInCKKS[itemClass];
474 classUUIDs = [NSMutableSet set];
475 itemUUIDsNotYetInCKKS[itemClass] = classUUIDs;
477 [classUUIDs addObject:uuid];
481 // We're done checking local keychain for extra items, now let's make sure the mirror doesn't have extra items that the keychain doesn't have, either
482 if (mirrorUUIDs.count > 0) {
483 ckksnotice("ckksscan", self.deps.zoneID, "keychain missing %lu items from mirror, proceeding with queue scanning", (unsigned long)mirrorUUIDs.count);
484 [mirrorUUIDs minusSet:[NSSet setWithArray:[CKKSIncomingQueueEntry allUUIDs:self.deps.zoneID error:&error]]];
486 ckkserror("ckksscan", self.deps.zoneID, "unable to inspect incoming queue: %@", error);
491 [mirrorUUIDs minusSet:[NSSet setWithArray:[CKKSOutgoingQueueEntry allUUIDs:self.deps.zoneID error:&error]]];
493 ckkserror("ckksscan", self.deps.zoneID, "unable to inspect outgoing queue: %@", error);
499 // Drop off of read-only transaction
503 ckksnotice("ckksscan", self.deps.zoneID, "Exiting due to previous error: %@", self.error);
507 ckksnotice("ckksscan", self.deps.zoneID, "Found %d item classes with missing items", (int)itemUUIDsNotYetInCKKS.count);
509 for(NSString* itemClass in [itemUUIDsNotYetInCKKS allKeys]) {
510 [self onboardItemsWithUUIDs:itemUUIDsNotYetInCKKS[itemClass] itemClass:itemClass databaseProvider:databaseProvider];
513 [self fixUUIDlessItemsWithPrimaryKeys:primaryKeysWithNoUUIDs databaseProvider:databaseProvider];
515 [self retriggerMissingMirrorEntires:mirrorUUIDs
517 databaseProvider:databaseProvider];
519 [CKKSPowerCollection CKKSPowerEvent:kCKKSPowerEventScanLocalItems zone:self.deps.zoneID.zoneName count:self.processedItems];
521 // Write down that a scan occurred
522 [databaseProvider dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
523 CKKSZoneStateEntry* zoneState = [CKKSZoneStateEntry state:self.deps.zoneID.zoneName];
525 zoneState.lastLocalKeychainScanTime = [NSDate now];
527 NSError* saveError = nil;
528 [zoneState saveToDatabase:&saveError];
531 ckkserror("ckksscan", self.deps.zoneID, "Unable to save 'scanned' bit: %@", saveError);
533 ckksnotice("ckksscan", self.deps.zoneID, "Saved scanned status.");
536 return CKKSDatabaseTransactionCommit;
539 if(self.newCKKSEntries) {
540 // Schedule a "view changed" notification
541 [self.deps.notifyViewChangedScheduler trigger];
543 // notify CKKS that it should process these new entries
544 [ckks processOutgoingQueue:self.ckoperationGroup];
545 // TODO: self.nextState = SecCKKSZoneKeyStateProcessOutgoingQueue;
547 self.nextState = self.intendedState;
550 if(self.missingLocalItemsFound > 0) {
551 [ckks processIncomingQueue:false];
552 // TODO [self.deps.flagHandler _onqueueHandleFlag:CKKSFlagProcessIncomingQueue];
555 ckksnotice("ckksscan", self.deps.zoneID, "Completed scan");
559 - (BOOL)areEquivalent:(SecDbItemRef)item ckksItem:(CKKSItem*)ckksItem
561 NSError* localerror = nil;
562 NSDictionary* attributes = [CKKSIncomingQueueOperation decryptCKKSItemToAttributes:ckksItem error:&localerror];
563 if(!attributes || localerror) {
564 ckksnotice("ckksscan", self.deps.zoneID, "Could not decrypt item for comparison: %@", localerror);
568 CFErrorRef cferror = NULL;
569 NSDictionary* objdict = (NSMutableDictionary*)CFBridgingRelease(SecDbItemCopyPListWithMask(item, kSecDbSyncFlag, &cferror));
570 localerror = (NSError*)CFBridgingRelease(cferror);
572 if(!objdict || localerror) {
573 ckksnotice("ckksscan", self.deps.zoneID, "Could not get item contents for comparison: %@", localerror);
575 // Fail open: assert that this item doesn't match
579 for(id key in objdict) {
580 // Okay, but seriously storing dates as floats was a mistake.
581 // Don't compare cdat and mdat, as they'll usually be different.
582 // Also don't compare the sha1, as it hashes that double.
583 if([key isEqual:(__bridge id)kSecAttrCreationDate] ||
584 [key isEqual:(__bridge id)kSecAttrModificationDate] ||
585 [key isEqual:(__bridge id)kSecAttrSHA1]) {
589 id value = objdict[key];
590 id attributesValue = attributes[key];
592 if(![value isEqual:attributesValue]) {