]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSScanLocalItemsOperation.m
Security-59306.101.1.tar.gz
[apple/security.git] / keychain / ckks / CKKSScanLocalItemsOperation.m
1 /*
2 * Copyright (c) 2016 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
11 * file.
12 *
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 #if OCTAGON
25 #import <TrustedPeers/TPPolicy.h>
26 #import <TrustedPeers/TPPBPolicyKeyViewMapping.h>
27 #import <TrustedPeers/TPDictionaryMatchingRules.h>
28
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/CKKSManifest.h"
40 #import "keychain/ckks/CKKSItemEncrypter.h"
41
42 #import "CKKSPowerCollection.h"
43
44 #include "keychain/securityd/SecItemSchema.h"
45 #include "keychain/securityd/SecItemServer.h"
46 #include "keychain/securityd/SecItemDb.h"
47 #include <Security/SecItemPriv.h>
48 #include <utilities/SecInternalReleasePriv.h>
49 #import <IMCore/IMCore_Private.h>
50 #import <IMCore/IMCloudKitHooks.h>
51
52 @interface CKKSScanLocalItemsOperation ()
53 @property (assign) NSUInteger processedItems;
54 @end
55
56 @implementation CKKSScanLocalItemsOperation
57
58 - (instancetype)init {
59 return nil;
60 }
61 - (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks ckoperationGroup:(CKOperationGroup*)ckoperationGroup {
62 if(self = [super init]) {
63 _ckks = ckks;
64 _ckoperationGroup = ckoperationGroup;
65 _recordsFound = 0;
66 _recordsAdded = 0;
67 }
68 return self;
69 }
70
71 // returns true if the currently loaded TPPolicy in the view manager says that only items with a VewHint
72 // matching the viewName belong to this view.
73 - (BOOL)policyRecommendsOnlyViewHintItems:(CKKSKeychainView*)ckks
74 {
75 // Early-exit for when feature is not on:
76 if(![[CKKSViewManager manager] useCKKSViewsFromPolicy]) {
77 return YES;
78 }
79
80 TPPBPolicyKeyViewMapping* viewRule = nil;
81
82 // If there's more than one rule matching this view, then exit with NO.
83 for(TPPBPolicyKeyViewMapping* mapping in [CKKSViewManager manager].policy.keyViewMapping) {
84 if([mapping.view isEqualToString:ckks.zoneName]) {
85 if(viewRule == nil) {
86 viewRule = mapping;
87 } else {
88 // Too many rules for this view! Don't perform optimization.
89 ckksnotice("ckksscan", ckks, "Too many policy rules for view %@", ckks.zoneName);
90 return NO;
91 }
92 }
93 }
94
95 if(viewRule.hasMatchingRule &&
96 viewRule.matchingRule.andsCount == 0 &&
97 viewRule.matchingRule.orsCount == 0 &&
98 !viewRule.matchingRule.hasNot &&
99 !viewRule.matchingRule.hasExists &&
100 viewRule.matchingRule.hasMatch) {
101 if([@"vwht" isEqualToString:viewRule.matchingRule.match.fieldName] &&
102 [viewRule.matchingRule.match.regex isEqualToString:[NSString stringWithFormat:@"^%@$", ckks.zoneName]]) {
103 return YES;
104 } else {
105 ckksnotice("ckksscan", ckks, "Policy view rule is not a match against viewhint: %@", viewRule);
106 }
107 } else {
108 ckksnotice("ckksscan", ckks, "Policy view rule is of unknown type: %@", viewRule);
109 }
110
111 return NO;
112 }
113
114 - (void) main {
115 // Take a strong reference.
116 CKKSKeychainView* ckks = self.ckks;
117 if(!ckks) {
118 ckkserror("ckksscan", ckks, "no CKKS object");
119 return;
120 }
121
122 [ckks.launch addEvent:@"scan-local-items"];
123
124 [ckks dispatchSyncWithAccountKeys: ^bool{
125 if(self.cancelled) {
126 ckksnotice("ckksscan", ckks, "CKKSScanLocalItemsOperation cancelled, quitting");
127 return false;
128 }
129 ckks.lastScanLocalItemsOperation = self;
130
131 NSMutableArray* itemsForManifest = [NSMutableArray array];
132
133 // First, query for all synchronizable items
134 __block CFErrorRef cferror = NULL;
135 __block NSError* error = nil;
136 __block bool newEntries = false;
137
138 // We want this set to be empty after scanning, or else the keychain (silently) dropped something on the floor
139 NSMutableSet<NSString*>* mirrorUUIDs = [NSMutableSet setWithArray:[CKKSMirrorEntry allUUIDs:ckks.zoneID error:&error]];
140
141 // Must query per-class, so:
142 const SecDbSchema *newSchema = current_schema();
143 for (const SecDbClass *const *class = newSchema->classes; *class != NULL; class++) {
144 cferror = NULL;
145
146 if(!((*class)->itemclass)) {
147 // Don't try to scan non-item 'classes'
148 continue;
149 }
150
151 // As a performance optimization, if the current policy says that this view only includes items by viewhint,
152 // add that to the query.
153 BOOL limitToViewHint = [self policyRecommendsOnlyViewHintItems:ckks];
154
155 NSMutableDictionary* queryAttributes = [
156 @{(__bridge NSString*) kSecClass: (__bridge NSString*) (*class)->name,
157 (__bridge NSString*) kSecReturnRef: @(YES),
158 (__bridge NSString*) kSecAttrSynchronizable: @(YES),
159 (__bridge NSString*) kSecAttrTombstone: @(NO),
160 } mutableCopy];
161
162 if(limitToViewHint) {
163 queryAttributes[(__bridge NSString*)kSecAttrSyncViewHint] = ckks.zoneName;
164 }
165
166 ckksnotice("ckksscan", ckks, "Scanning all synchronizable %@ items(%@) for: %@", (__bridge NSString*)(*class)->name, self.name, queryAttributes);
167
168 Query *q = query_create_with_limit( (__bridge CFDictionaryRef) queryAttributes, NULL, kSecMatchUnlimited, &cferror);
169 bool ok = false;
170
171 if(cferror) {
172 ckkserror("ckksscan", ckks, "couldn't create query: %@", cferror);
173 SecTranslateError(&error, cferror);
174 self.error = error;
175 continue;
176 }
177
178 ok = kc_with_dbt(true, &cferror, ^(SecDbConnectionRef dbt) {
179 return SecDbItemQuery(q, NULL, dbt, &cferror, ^(SecDbItemRef item, bool *stop) {
180 ckksnotice("ckksscan", ckks, "scanning item: %@", item);
181
182 self.processedItems += 1;
183
184 SecDbItemRef itemToSave = NULL;
185
186 // First check: is this a tombstone? If so, skip with prejudice.
187 if(SecDbItemIsTombstone(item)) {
188 ckksinfo("ckksscan", ckks, "Skipping tombstone %@", item);
189 return;
190 }
191
192 // Second check: is this item even for this view? If not, skip.
193 NSString* viewForItem = [[CKKSViewManager manager] viewNameForItem:item];
194 if(![viewForItem isEqualToString: ckks.zoneName]) {
195 ckksinfo("ckksscan", ckks, "Scanned item is for view %@, skipping", viewForItem);
196 return;
197 }
198
199 // Third check: is this item one of our keys for a view? If not, skip.
200 if([CKKSKey isItemKeyForKeychainView: item] != nil) {
201 ckksinfo("ckksscan", ckks, "Scanned item is a CKKS internal key, skipping");
202 return;
203 }
204
205 // Fourth check: does this item have a UUID? If not, ONBOARD!
206 NSString* uuid = (__bridge_transfer NSString*) CFRetain(SecDbItemGetValue(item, &v10itemuuid, &cferror));
207 if(!uuid || [uuid isEqual: [NSNull null]]) {
208 ckksnotice("ckksscan", ckks, "making new UUID for item %@", item);
209
210 uuid = [[NSUUID UUID] UUIDString];
211 NSDictionary* updates = @{(id) kSecAttrUUID: uuid};
212
213 SecDbItemRef new_item = SecDbItemCopyWithUpdates(item, (__bridge CFDictionaryRef) updates, &cferror);
214 if(SecErrorGetOSStatus(cferror) != errSecSuccess) {
215 ckkserror("ckksscan", ckks, "couldn't update item with new UUID: %@", cferror);
216 SecTranslateError(&error, cferror);
217 self.error = error;
218 CFReleaseNull(new_item);
219 return;
220 }
221
222 if (new_item) {
223 bool ok = kc_transaction_type(dbt, kSecDbExclusiveRemoteCKKSTransactionType, &cferror, ^{
224 return SecDbItemUpdate(item, new_item, dbt, kCFBooleanFalse, q->q_uuid_from_primary_key, &cferror);
225 });
226
227 if(!ok || SecErrorGetOSStatus(cferror) != errSecSuccess) {
228 ckkserror("ckksscan", ckks, "couldn't update item with new UUID: %@", cferror);
229 SecTranslateError(&error, cferror);
230 self.error = error;
231 CFReleaseNull(new_item);
232 return;
233 }
234 }
235 itemToSave = CFRetainSafe(new_item);
236 CFReleaseNull(new_item);
237
238 } else {
239 // Is there a known sync item with this UUID?
240 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase: uuid zoneID:ckks.zoneID error: &error];
241 if(ckme != nil) {
242 if ([CKKSManifest shouldSyncManifests]) {
243 [itemsForManifest addObject:ckme.item];
244 }
245 [mirrorUUIDs removeObject:uuid];
246 ckksinfo("ckksscan", ckks, "Existing mirror entry with UUID %@", uuid);
247
248 if([self areEquivalent:item ckksItem:ckme.item]) {
249 // Fair enough.
250 return;
251 } else {
252 ckksnotice("ckksscan", ckks, "Existing mirror entry with UUID %@ does not match local item", uuid);
253 }
254 }
255
256 // We don't care about the oqe state here, just that one exists
257 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry tryFromDatabase: uuid zoneID:ckks.zoneID error: &error];
258 if(oqe != nil) {
259 ckksnotice("ckksscan", ckks, "Existing outgoing queue entry with UUID %@", uuid);
260 // If its state is 'new', mark down that we've seen new entries that need processing
261 newEntries |= !![oqe.state isEqualToString: SecCKKSStateNew];
262 return;
263 }
264
265 itemToSave = CFRetainSafe(item);
266 }
267
268 // Hurray, we can help!
269 self.recordsFound += 1;
270
271 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry withItem: itemToSave action: SecCKKSActionAdd ckks:ckks error: &error];
272
273 if(error) {
274 ckkserror("ckksscan", ckks, "Need to upload %@, but can't create outgoing entry: %@", item, error);
275 self.error = error;
276 CFReleaseNull(itemToSave);
277 return;
278 }
279
280 ckksnotice("ckksscan", ckks, "Syncing new item: %@", oqe);
281 CFReleaseNull(itemToSave);
282
283 [oqe saveToDatabase: &error];
284 if(error) {
285 ckkserror("ckksscan", ckks, "Need to upload %@, but can't save to database: %@", oqe, error);
286 self.error = error;
287 return;
288 }
289 newEntries = true;
290 if ([CKKSManifest shouldSyncManifests]) {
291 [itemsForManifest addObject:oqe.item];
292 }
293
294 self.recordsAdded += 1;
295 });
296 });
297
298 if(cferror || !ok) {
299 ckkserror("ckksscan", ckks, "error processing or finding items: %@", cferror);
300 SecTranslateError(&error, cferror);
301 self.error = error;
302 query_destroy(q, NULL);
303 continue;
304 }
305
306 ok = query_notify_and_destroy(q, ok, &cferror);
307
308 if(cferror || !ok) {
309 ckkserror("ckksscan", ckks, "couldn't delete query: %@", cferror);
310 SecTranslateError(&error, cferror);
311 self.error = error;
312 continue;
313 }
314 }
315
316 // We're done checking local keychain for extra items, now let's make sure the mirror doesn't have extra items, either
317 if (mirrorUUIDs.count > 0) {
318 ckksnotice("ckksscan", ckks, "keychain missing %lu items from mirror, proceeding with queue scanning", (unsigned long)mirrorUUIDs.count);
319 [mirrorUUIDs minusSet:[NSSet setWithArray:[CKKSIncomingQueueEntry allUUIDs:ckks.zoneID error:&error]]];
320 if (error) {
321 ckkserror("ckksscan", ckks, "unable to inspect incoming queue: %@", error);
322 self.error = error;
323 return false;
324 }
325
326 [mirrorUUIDs minusSet:[NSSet setWithArray:[CKKSOutgoingQueueEntry allUUIDs:ckks.zoneID error:&error]]];
327 if (error) {
328 ckkserror("ckksscan", ckks, "unable to inspect outgoing queue: %@", error);
329 self.error = error;
330 return false;
331 }
332
333 if (mirrorUUIDs.count > 0) {
334 ckkserror("ckksscan", ckks, "BUG: keychain missing %lu items from mirror and/or queues: %@", (unsigned long)mirrorUUIDs.count, mirrorUUIDs);
335 self.missingLocalItemsFound = mirrorUUIDs.count;
336
337 [[CKKSAnalytics logger] logMetric:[NSNumber numberWithUnsignedInteger:mirrorUUIDs.count] withName:CKKSEventMissingLocalItemsFound];
338
339 for (NSString* uuid in mirrorUUIDs) {
340 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase:uuid zoneID:ckks.zoneID error:&error];
341 [ckks _onqueueCKRecordChanged:ckme.item.storedCKRecord resync:true];
342 }
343
344 // And, if you're not in the tests, try to collect a sysdiagnose I guess?
345 // <rdar://problem/36166435> Re-enable IMCore autosysdiagnose capture to securityd
346 //if(SecIsInternalRelease() && !SecCKKSTestsEnabled()) {
347 // [[IMCloudKitHooks sharedInstance] tryToAutoCollectLogsWithErrorString:@"35810558" sendLogsTo:@"rowdy_bot@icloud.com"];
348 //}
349 } else {
350 ckksnotice("ckksscan", ckks, "No missing local items found");
351 }
352 }
353
354 [CKKSPowerCollection CKKSPowerEvent:kCKKSPowerEventScanLocalItems zone:ckks.zoneName count:self.processedItems];
355
356 if ([CKKSManifest shouldSyncManifests]) {
357 // TODO: this manifest needs to incorporate peer manifests
358 CKKSEgoManifest* manifest = [CKKSEgoManifest newManifestForZone:ckks.zoneName withItems:itemsForManifest peerManifestIDs:@[] currentItems:@{} error:&error];
359 if (!manifest || error) {
360 ckkserror("ckksscan", ckks, "could not create manifest: %@", error);
361 self.error = error;
362 return false;
363 }
364
365 [manifest saveToDatabase:&error];
366 if (error) {
367 ckkserror("ckksscan", ckks, "could not save manifest to database: %@", error);
368 self.error = error;
369 return false;
370 }
371
372 ckks.egoManifest = manifest;
373 }
374
375 if(newEntries) {
376 // Schedule a "view changed" notification
377 [ckks.notifyViewChangedScheduler trigger];
378
379 // notify CKKS that it should process these new entries
380 [ckks processOutgoingQueue:self.ckoperationGroup];
381 }
382
383 if(self.missingLocalItemsFound > 0) {
384 [ckks processIncomingQueue:false];
385 }
386
387 ckksnotice("ckksscan", ckks, "Completed scan");
388 ckks.droppedItems = false;
389 return true;
390 }];
391 }
392
393 - (BOOL)areEquivalent:(SecDbItemRef)item ckksItem:(CKKSItem*)ckksItem
394 {
395 CKKSKeychainView* ckks = self.ckks;
396
397 NSError* localerror = nil;
398 NSDictionary* attributes = [CKKSIncomingQueueOperation decryptCKKSItemToAttributes:ckksItem error:&localerror];
399 if(!attributes || localerror) {
400 ckksnotice("ckksscan", ckks, "Could not decrypt item for comparison: %@", localerror);
401 return YES;
402 }
403
404 CFErrorRef cferror = NULL;
405 NSDictionary* objdict = (NSMutableDictionary*)CFBridgingRelease(SecDbItemCopyPListWithMask(item, kSecDbSyncFlag, &cferror));
406 localerror = (NSError*)CFBridgingRelease(cferror);
407
408 if(!objdict || localerror) {
409 ckksnotice("ckksscan", ckks, "Could not get item contents for comparison: %@", localerror);
410
411 // Fail open: assert that this item doesn't match
412 return NO;
413 }
414
415 for(id key in objdict) {
416 // Okay, but seriously storing dates as floats was a mistake.
417 // Don't compare cdat and mdat, as they'll usually be different.
418 // Also don't compare the sha1, as it hashes that double.
419 if([key isEqual:(__bridge id)kSecAttrCreationDate] ||
420 [key isEqual:(__bridge id)kSecAttrModificationDate] ||
421 [key isEqual:(__bridge id)kSecAttrSHA1]) {
422 continue;
423 }
424
425 id value = objdict[key];
426 id attributesValue = attributes[key];
427
428 if(![value isEqual:attributesValue]) {
429 return NO;
430 }
431 }
432
433 return YES;
434 }
435
436 @end;
437
438 #endif