]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSScanLocalItemsOperation.m
Security-59306.11.20.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
26 #import "keychain/ckks/CKKSAnalytics.h"
27 #import "keychain/ckks/CKKSKeychainView.h"
28 #import "keychain/ckks/CKKSNearFutureScheduler.h"
29 #import "keychain/ckks/CKKSScanLocalItemsOperation.h"
30 #import "keychain/ckks/CKKSMirrorEntry.h"
31 #import "keychain/ckks/CKKSIncomingQueueEntry.h"
32 #import "keychain/ckks/CKKSOutgoingQueueEntry.h"
33 #import "keychain/ckks/CKKSGroupOperation.h"
34 #import "keychain/ckks/CKKSKey.h"
35 #import "keychain/ckks/CKKSViewManager.h"
36 #import "keychain/ckks/CKKSManifest.h"
37 #import "keychain/ckks/CKKSItemEncrypter.h"
38
39 #import "CKKSPowerCollection.h"
40
41 #include <securityd/SecItemSchema.h>
42 #include <securityd/SecItemServer.h>
43 #include <securityd/SecItemDb.h>
44 #include <Security/SecItemPriv.h>
45 #include <utilities/SecInternalReleasePriv.h>
46 #import <IMCore/IMCore_Private.h>
47 #import <IMCore/IMCloudKitHooks.h>
48
49 @interface CKKSScanLocalItemsOperation ()
50 @property CKOperationGroup* ckoperationGroup;
51 @property (assign) NSUInteger processedItems;
52 @end
53
54 @implementation CKKSScanLocalItemsOperation
55
56 - (instancetype)init {
57 return nil;
58 }
59 - (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks ckoperationGroup:(CKOperationGroup*)ckoperationGroup {
60 if(self = [super init]) {
61 _ckks = ckks;
62 _ckoperationGroup = ckoperationGroup;
63 _recordsFound = 0;
64 _recordsAdded = 0;
65 }
66 return self;
67 }
68
69 - (void) main {
70 // Take a strong reference.
71 CKKSKeychainView* ckks = self.ckks;
72 if(!ckks) {
73 ckkserror("ckksscan", ckks, "no CKKS object");
74 return;
75 }
76
77 [ckks.launch addEvent:@"scan-local-items"];
78
79 [ckks dispatchSyncWithAccountKeys: ^bool{
80 if(self.cancelled) {
81 ckksnotice("ckksscan", ckks, "CKKSScanLocalItemsOperation cancelled, quitting");
82 return false;
83 }
84 ckks.lastScanLocalItemsOperation = self;
85
86 NSMutableArray* itemsForManifest = [NSMutableArray array];
87
88 // First, query for all synchronizable items
89 __block CFErrorRef cferror = NULL;
90 __block NSError* error = nil;
91 __block bool newEntries = false;
92
93 // We want this set to be empty after scanning, or else the keychain (silently) dropped something on the floor
94 NSMutableSet<NSString*>* mirrorUUIDs = [NSMutableSet setWithArray:[CKKSMirrorEntry allUUIDs:ckks.zoneID error:&error]];
95
96 // Must query per-class, so:
97 const SecDbSchema *newSchema = current_schema();
98 for (const SecDbClass *const *class = newSchema->classes; *class != NULL; class++) {
99 cferror = NULL;
100
101 if(!((*class)->itemclass)) {
102 // Don't try to scan non-item 'classes'
103 continue;
104 }
105
106 NSDictionary* queryAttributes = @{(__bridge NSString*) kSecClass: (__bridge NSString*) (*class)->name,
107 (__bridge NSString*) kSecReturnRef: @(YES),
108 (__bridge NSString*) kSecAttrSynchronizable: @(YES),
109 (__bridge NSString*) kSecAttrTombstone: @(NO),
110 // This works ~as long as~ item views are chosen by view hint only. It's a significant perf win, though.
111 // <rdar://problem/32269541> SpinTracer: CKKSScanLocalItemsOperation expensive on M8 machines
112 (__bridge NSString*) kSecAttrSyncViewHint: ckks.zoneName,
113 };
114 ckksinfo("ckksscan", ckks, "Scanning all synchronizable items for: %@", queryAttributes);
115
116 Query *q = query_create_with_limit( (__bridge CFDictionaryRef) queryAttributes, NULL, kSecMatchUnlimited, &cferror);
117 bool ok = false;
118
119 if(cferror) {
120 ckkserror("ckksscan", ckks, "couldn't create query: %@", cferror);
121 SecTranslateError(&error, cferror);
122 self.error = error;
123 continue;
124 }
125
126 ok = kc_with_dbt(true, &cferror, ^(SecDbConnectionRef dbt) {
127 return SecDbItemQuery(q, NULL, dbt, &cferror, ^(SecDbItemRef item, bool *stop) {
128 ckksnotice("ckksscan", ckks, "scanning item: %@", item);
129
130 self.processedItems += 1;
131
132 SecDbItemRef itemToSave = NULL;
133
134 // First check: is this a tombstone? If so, skip with prejudice.
135 if(SecDbItemIsTombstone(item)) {
136 ckksinfo("ckksscan", ckks, "Skipping tombstone %@", item);
137 return;
138 }
139
140 // Second check: is this item even for this view? If not, skip.
141 NSString* viewForItem = [[CKKSViewManager manager] viewNameForItem:item];
142 if(![viewForItem isEqualToString: ckks.zoneName]) {
143 ckksinfo("ckksscan", ckks, "Scanned item is for view %@, skipping", viewForItem);
144 return;
145 }
146
147 // Third check: is this item one of our keys for a view? If not, skip.
148 if([CKKSKey isItemKeyForKeychainView: item] != nil) {
149 ckksinfo("ckksscan", ckks, "Scanned item is a CKKS internal key, skipping");
150 return;
151 }
152
153 // Fourth check: does this item have a UUID? If not, ONBOARD!
154 NSString* uuid = (__bridge_transfer NSString*) CFRetain(SecDbItemGetValue(item, &v10itemuuid, &cferror));
155 if(!uuid || [uuid isEqual: [NSNull null]]) {
156 ckksnotice("ckksscan", ckks, "making new UUID for item %@", item);
157
158 uuid = [[NSUUID UUID] UUIDString];
159 NSDictionary* updates = @{(id) kSecAttrUUID: uuid};
160
161 SecDbItemRef new_item = SecDbItemCopyWithUpdates(item, (__bridge CFDictionaryRef) updates, &cferror);
162 if(SecErrorGetOSStatus(cferror) != errSecSuccess) {
163 ckkserror("ckksscan", ckks, "couldn't update item with new UUID: %@", cferror);
164 SecTranslateError(&error, cferror);
165 self.error = error;
166 CFReleaseNull(new_item);
167 return;
168 }
169
170 if (new_item) {
171 bool ok = kc_transaction_type(dbt, kSecDbExclusiveRemoteCKKSTransactionType, &cferror, ^{
172 return SecDbItemUpdate(item, new_item, dbt, kCFBooleanFalse, q->q_uuid_from_primary_key, &cferror);
173 });
174
175 if(!ok || SecErrorGetOSStatus(cferror) != errSecSuccess) {
176 ckkserror("ckksscan", ckks, "couldn't update item with new UUID: %@", cferror);
177 SecTranslateError(&error, cferror);
178 self.error = error;
179 CFReleaseNull(new_item);
180 return;
181 }
182 }
183 itemToSave = CFRetainSafe(new_item);
184 CFReleaseNull(new_item);
185
186 } else {
187 // Is there a known sync item with this UUID?
188 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase: uuid zoneID:ckks.zoneID error: &error];
189 if(ckme != nil) {
190 if ([CKKSManifest shouldSyncManifests]) {
191 [itemsForManifest addObject:ckme.item];
192 }
193 [mirrorUUIDs removeObject:uuid];
194 ckksinfo("ckksscan", ckks, "Existing mirror entry with UUID %@", uuid);
195
196 if([self areEquivalent:item ckksItem:ckme.item]) {
197 // Fair enough.
198 return;
199 } else {
200 ckksnotice("ckksscan", ckks, "Existing mirror entry with UUID %@ does not match local item", uuid);
201 }
202 }
203
204 // We don't care about the oqe state here, just that one exists
205 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry tryFromDatabase: uuid zoneID:ckks.zoneID error: &error];
206 if(oqe != nil) {
207 ckksnotice("ckksscan", ckks, "Existing outgoing queue entry with UUID %@", uuid);
208 // If its state is 'new', mark down that we've seen new entries that need processing
209 newEntries |= !![oqe.state isEqualToString: SecCKKSStateNew];
210 return;
211 }
212
213 itemToSave = CFRetainSafe(item);
214 }
215
216 // Hurray, we can help!
217 self.recordsFound += 1;
218
219 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry withItem: itemToSave action: SecCKKSActionAdd ckks:ckks error: &error];
220
221 if(error) {
222 ckkserror("ckksscan", ckks, "Need to upload %@, but can't create outgoing entry: %@", item, error);
223 self.error = error;
224 CFReleaseNull(itemToSave);
225 return;
226 }
227
228 ckksnotice("ckksscan", ckks, "Syncing new item: %@", oqe);
229 CFReleaseNull(itemToSave);
230
231 [oqe saveToDatabase: &error];
232 if(error) {
233 ckkserror("ckksscan", ckks, "Need to upload %@, but can't save to database: %@", oqe, error);
234 self.error = error;
235 return;
236 }
237 newEntries = true;
238 if ([CKKSManifest shouldSyncManifests]) {
239 [itemsForManifest addObject:oqe.item];
240 }
241
242 self.recordsAdded += 1;
243 });
244 });
245
246 if(cferror || !ok) {
247 ckkserror("ckksscan", ckks, "error processing or finding items: %@", cferror);
248 SecTranslateError(&error, cferror);
249 self.error = error;
250 query_destroy(q, NULL);
251 continue;
252 }
253
254 ok = query_notify_and_destroy(q, ok, &cferror);
255
256 if(cferror || !ok) {
257 ckkserror("ckksscan", ckks, "couldn't delete query: %@", cferror);
258 SecTranslateError(&error, cferror);
259 self.error = error;
260 continue;
261 }
262 }
263
264 // We're done checking local keychain for extra items, now let's make sure the mirror doesn't have extra items, either
265 if (mirrorUUIDs.count > 0) {
266 ckksnotice("ckksscan", ckks, "keychain missing %lu items from mirror, proceeding with queue scanning", (unsigned long)mirrorUUIDs.count);
267 [mirrorUUIDs minusSet:[NSSet setWithArray:[CKKSIncomingQueueEntry allUUIDs:ckks.zoneID error:&error]]];
268 if (error) {
269 ckkserror("ckksscan", ckks, "unable to inspect incoming queue: %@", error);
270 self.error = error;
271 return false;
272 }
273
274 [mirrorUUIDs minusSet:[NSSet setWithArray:[CKKSOutgoingQueueEntry allUUIDs:ckks.zoneID error:&error]]];
275 if (error) {
276 ckkserror("ckksscan", ckks, "unable to inspect outgoing queue: %@", error);
277 self.error = error;
278 return false;
279 }
280
281 if (mirrorUUIDs.count > 0) {
282 ckkserror("ckksscan", ckks, "BUG: keychain missing %lu items from mirror and/or queues: %@", (unsigned long)mirrorUUIDs.count, mirrorUUIDs);
283 self.missingLocalItemsFound = mirrorUUIDs.count;
284
285 [[CKKSAnalytics logger] logMetric:[NSNumber numberWithUnsignedInteger:mirrorUUIDs.count] withName:CKKSEventMissingLocalItemsFound];
286
287 for (NSString* uuid in mirrorUUIDs) {
288 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase:uuid zoneID:ckks.zoneID error:&error];
289 [ckks _onqueueCKRecordChanged:ckme.item.storedCKRecord resync:true];
290 }
291
292 // And, if you're not in the tests, try to collect a sysdiagnose I guess?
293 // <rdar://problem/36166435> Re-enable IMCore autosysdiagnose capture to securityd
294 //if(SecIsInternalRelease() && !SecCKKSTestsEnabled()) {
295 // [[IMCloudKitHooks sharedInstance] tryToAutoCollectLogsWithErrorString:@"35810558" sendLogsTo:@"rowdy_bot@icloud.com"];
296 //}
297 } else {
298 ckksnotice("ckksscan", ckks, "No missing local items found");
299 }
300 }
301
302 [CKKSPowerCollection CKKSPowerEvent:kCKKSPowerEventScanLocalItems zone:ckks.zoneName count:self.processedItems];
303
304 if ([CKKSManifest shouldSyncManifests]) {
305 // TODO: this manifest needs to incorporate peer manifests
306 CKKSEgoManifest* manifest = [CKKSEgoManifest newManifestForZone:ckks.zoneName withItems:itemsForManifest peerManifestIDs:@[] currentItems:@{} error:&error];
307 if (!manifest || error) {
308 ckkserror("ckksscan", ckks, "could not create manifest: %@", error);
309 self.error = error;
310 return false;
311 }
312
313 [manifest saveToDatabase:&error];
314 if (error) {
315 ckkserror("ckksscan", ckks, "could not save manifest to database: %@", error);
316 self.error = error;
317 return false;
318 }
319
320 ckks.egoManifest = manifest;
321 }
322
323 if(newEntries) {
324 // Schedule a "view changed" notification
325 [ckks.notifyViewChangedScheduler trigger];
326
327 // notify CKKS that it should process these new entries
328 [ckks processOutgoingQueue:self.ckoperationGroup];
329 }
330
331 if(self.missingLocalItemsFound > 0) {
332 [ckks processIncomingQueue:false];
333 }
334
335 ckksnotice("ckksscan", ckks, "Completed scan");
336 ckks.droppedItems = false;
337 return true;
338 }];
339 }
340
341 - (BOOL)areEquivalent:(SecDbItemRef)item ckksItem:(CKKSItem*)ckksItem
342 {
343 CKKSKeychainView* ckks = self.ckks;
344
345 NSError* localerror = nil;
346 NSDictionary* attributes = [CKKSIncomingQueueOperation decryptCKKSItemToAttributes:ckksItem error:&localerror];
347 if(!attributes || localerror) {
348 ckksnotice("ckksscan", ckks, "Could not decrypt item for comparison: %@", localerror);
349 return YES;
350 }
351
352 CFErrorRef cferror = NULL;
353 NSDictionary* objdict = (NSMutableDictionary*)CFBridgingRelease(SecDbItemCopyPListWithMask(item, kSecDbSyncFlag, &cferror));
354 localerror = (NSError*)CFBridgingRelease(cferror);
355
356 if(!objdict || localerror) {
357 ckksnotice("ckksscan", ckks, "Could not get item contents for comparison: %@", localerror);
358
359 // Fail open: assert that this item doesn't match
360 return NO;
361 }
362
363 for(id key in objdict) {
364 // Okay, but seriously storing dates as floats was a mistake.
365 // Don't compare cdat and mdat, as they'll usually be different.
366 // Also don't compare the sha1, as it hashes that double.
367 if([key isEqual:(__bridge id)kSecAttrCreationDate] ||
368 [key isEqual:(__bridge id)kSecAttrModificationDate] ||
369 [key isEqual:(__bridge id)kSecAttrSHA1]) {
370 continue;
371 }
372
373 id value = objdict[key];
374 id attributesValue = attributes[key];
375
376 if(![value isEqual:attributesValue]) {
377 return NO;
378 }
379 }
380
381 return YES;
382 }
383
384 @end;
385
386 #endif