]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSViewManager.m
Security-59306.41.2.tar.gz
[apple/security.git] / keychain / ckks / CKKSViewManager.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 #import <os/feature_private.h>
25
26 #import "keychain/ckks/CKKSViewManager.h"
27 #import "keychain/ckks/CKKSKeychainView.h"
28 #import "keychain/ckks/CKKSSynchronizeOperation.h"
29 #import "keychain/ckks/CKKSKey.h"
30 #import "keychain/ckks/CKKSZoneStateEntry.h"
31 #import "keychain/ckks/CKKSNearFutureScheduler.h"
32 #import "keychain/ckks/CKKSNotifier.h"
33 #import "keychain/ckks/CKKSCondition.h"
34 #import "keychain/ckks/CKKSListenerCollection.h"
35 #import "keychain/ckks/CloudKitCategories.h"
36 #import "keychain/categories/NSError+UsefulConstructors.h"
37 #import "keychain/analytics/SecEventMetric.h"
38 #import "keychain/analytics/SecMetrics.h"
39
40 #import "keychain/ot/OTManager.h"
41 #import "keychain/ot/OTDefines.h"
42 #import "keychain/ot/OTConstants.h"
43 #import "keychain/ot/ObjCImprovements.h"
44
45 #import "TPPolicy.h"
46
47 #import "SecEntitlements.h"
48
49 #include "keychain/securityd/SecDbItem.h"
50 #include "keychain/securityd/SecDbKeychainItem.h"
51 #include "keychain/securityd/SecItemSchema.h"
52 #include <Security/SecureObjectSync/SOSViews.h>
53
54 #import <Foundation/NSXPCConnection.h>
55 #import <Foundation/NSXPCConnection_Private.h>
56
57 #include "keychain/SecureObjectSync/SOSAccount.h"
58 #include <Security/SecItemBackup.h>
59
60 #if OCTAGON
61 #import <CloudKit/CloudKit.h>
62 #import <CloudKit/CloudKit_Private.h>
63
64 #import <SecurityFoundation/SFKey.h>
65 #import <SecurityFoundation/SFKey_Private.h>
66
67 #import "CKKSAnalytics.h"
68 #endif
69
70 @interface CKKSViewManager () <NSXPCListenerDelegate>
71 #if OCTAGON
72 @property NSXPCListener *listener;
73
74 // Once you set these, all CKKSKeychainViews created will use them
75 @property CKKSCloudKitClassDependencies* cloudKitClassDependencies;
76
77 @property NSMutableDictionary<NSString*, SecBoolNSErrorCallback>* pendingSyncCallbacks;
78 @property CKKSNearFutureScheduler* savedTLKNotifier;;
79 @property NSOperationQueue* operationQueue;
80
81 @property CKKSListenerCollection<id<CKKSPeerUpdateListener>>* peerChangeListenerCollection;
82
83 @property (nonatomic) BOOL overrideCKKSViewsFromPolicy;
84 @property (nonatomic) BOOL valueCKKSViewsFromPolicy;
85
86 #endif
87 @end
88
89 #if OCTAGON
90 @interface CKKSViewManager (lockstateTracker) <CKKSLockStateNotification>
91 @end
92 #endif
93
94 @implementation CKKSViewManager
95 #if OCTAGON
96
97 NSSet<NSString*>* _viewList;
98
99 - (instancetype)initWithContainerName: (NSString*) containerName
100 usePCS: (bool)usePCS
101 sosAdapter:(id<OTSOSAdapter> _Nullable)sosAdapter
102 cloudKitClassDependencies:(CKKSCloudKitClassDependencies*)cloudKitClassDependencies
103 {
104 if(self = [super init]) {
105 _cloudKitClassDependencies = cloudKitClassDependencies;
106 _sosPeerAdapter = sosAdapter;
107 _viewList = nil;
108
109 _container = [self makeCKContainer: containerName usePCS:usePCS];
110 _accountTracker = [[CKKSAccountStateTracker alloc] init:self.container nsnotificationCenterClass:cloudKitClassDependencies.nsnotificationCenterClass];
111 _lockStateTracker = [[CKKSLockStateTracker alloc] init];
112 [_lockStateTracker addLockStateObserver:self];
113 _reachabilityTracker = [[CKKSReachabilityTracker alloc] init];
114
115 _zoneChangeFetcher = [[CKKSZoneChangeFetcher alloc] initWithContainer:_container
116 fetchClass:cloudKitClassDependencies.fetchRecordZoneChangesOperationClass
117 reachabilityTracker:_reachabilityTracker];
118
119 _zoneModifier = [[CKKSZoneModifier alloc] initWithContainer:_container
120 reachabilityTracker:_reachabilityTracker
121 cloudkitDependencies:cloudKitClassDependencies];
122
123 _operationQueue = [[NSOperationQueue alloc] init];
124
125 _peerChangeListenerCollection = [[CKKSListenerCollection alloc] initWithName:@"sos-peer-set"];
126
127 _views = [[NSMutableDictionary alloc] init];
128 _pendingSyncCallbacks = [[NSMutableDictionary alloc] init];
129
130 _completedSecCKKSInitialize = [[CKKSCondition alloc] init];
131
132 WEAKIFY(self);
133 _savedTLKNotifier = [[CKKSNearFutureScheduler alloc] initWithName:@"newtlks"
134 delay:5*NSEC_PER_SEC
135 keepProcessAlive:true
136 dependencyDescriptionCode:CKKSResultDescriptionNone
137 block:^{
138 STRONGIFY(self);
139 [self notifyNewTLKsInKeychain];
140 }];
141
142 _listener = [NSXPCListener anonymousListener];
143 _listener.delegate = self;
144 [_listener resume];
145 }
146 return self;
147 }
148
149 -(CKContainer*)makeCKContainer:(NSString*)containerName usePCS:(bool)usePCS {
150 CKContainer* container = [CKContainer containerWithIdentifier:containerName];
151 if(!usePCS) {
152 CKContainerOptions* containerOptions = [[CKContainerOptions alloc] init];
153 containerOptions.bypassPCSEncryption = YES;
154
155 // We don't have a great way to set these, so replace the entire container object
156 container = [[CKContainer alloc] initWithContainerID: container.containerID options:containerOptions];
157 }
158 return container;
159 }
160
161 - (BOOL)waitForTrustReady {
162 static dispatch_once_t onceToken;
163 __block BOOL success = YES;
164 dispatch_once(&onceToken, ^{
165 OTManager* manager = [OTManager manager];
166 if (![manager waitForReady:OTCKContainerName context:OTDefaultContext wait:3*NSEC_PER_SEC]) {
167 success = NO;
168 }
169 });
170 return success;
171 }
172
173 - (void)setupAnalytics
174 {
175 WEAKIFY(self);
176
177 // Tests shouldn't continue here; it leads to entitlement crashes with CloudKit if the mocks aren't enabled when this function runs
178 if(SecCKKSTestsEnabled()) {
179 return;
180 }
181
182 [[CKKSAnalytics logger] AddMultiSamplerForName:@"CKKS-healthSummary" withTimeInterval:SFAnalyticsSamplerIntervalOncePerReport block:^NSDictionary<NSString *,NSNumber *> *{
183 STRONGIFY(self);
184 if(!self) {
185 return nil;
186 }
187
188 NSError* sosCircleError = nil;
189 SOSCCStatus sosStatus = [self.sosPeerAdapter circleStatus:&sosCircleError];
190 if(sosCircleError) {
191 secerror("CKKSViewManager: couldn't fetch sos status for SF report: %@", sosCircleError);
192 }
193
194 NSMutableDictionary* values = [NSMutableDictionary dictionary];
195 BOOL inCircle = (sosStatus == kSOSCCInCircle);
196 if (inCircle) {
197 [[CKKSAnalytics logger] setDateProperty:[NSDate date] forKey:CKKSAnalyticsLastInCircle];
198 }
199 values[CKKSAnalyticsInCircle] = @(inCircle);
200
201 BOOL validCredentials = self.accountTracker.currentCKAccountInfo.hasValidCredentials;
202 if (!validCredentials) {
203 values[CKKSAnalyticsValidCredentials] = @(validCredentials);
204 }
205
206 NSArray<NSString *>* keys = @[ CKKSAnalyticsLastUnlock, CKKSAnalyticsLastInCircle];
207 for (NSString * key in keys) {
208 NSDate *date = [[CKKSAnalytics logger] datePropertyForKey:key];
209 values[key] = @([CKKSAnalytics fuzzyDaysSinceDate:date]);
210 }
211 return values;
212 }];
213
214 for (NSString* viewName in [self viewList]) {
215 [[CKKSAnalytics logger] AddMultiSamplerForName:[NSString stringWithFormat:@"CKKS-%@-healthSummary", viewName] withTimeInterval:SFAnalyticsSamplerIntervalOncePerReport block:^NSDictionary<NSString *,NSNumber *> *{
216 STRONGIFY(self);
217 if(!self) {
218 return nil;
219 }
220
221 NSError* sosCircleError = nil;
222 SOSCCStatus sosStatus = [self.sosPeerAdapter circleStatus:&sosCircleError];
223 if(sosCircleError) {
224 secerror("CKKSViewManager: couldn't fetch sos status for SF report: %@", sosCircleError);
225 }
226 BOOL inCircle = (sosStatus == kSOSCCInCircle);
227 NSMutableDictionary* values = [NSMutableDictionary dictionary];
228 CKKSKeychainView* view = [self findOrCreateView:viewName];
229 NSDate* dateOfLastSyncClassA = [[CKKSAnalytics logger] dateOfLastSuccessForEvent:CKKSEventProcessIncomingQueueClassA inView:view];
230 NSDate* dateOfLastSyncClassC = [[CKKSAnalytics logger] dateOfLastSuccessForEvent:CKKSEventProcessIncomingQueueClassC inView:view];
231 NSDate* dateOfLastKSR = [[CKKSAnalytics logger] datePropertyForKey:CKKSAnalyticsLastKeystateReady inView:view];
232
233 NSInteger fuzzyDaysSinceClassASync = [CKKSAnalytics fuzzyDaysSinceDate:dateOfLastSyncClassA];
234 NSInteger fuzzyDaysSinceClassCSync = [CKKSAnalytics fuzzyDaysSinceDate:dateOfLastSyncClassC];
235 NSInteger fuzzyDaysSinceKSR = [CKKSAnalytics fuzzyDaysSinceDate:dateOfLastKSR];
236 [values setValue:@(fuzzyDaysSinceClassASync) forKey:[NSString stringWithFormat:@"%@-daysSinceClassASync", viewName]];
237 [values setValue:@(fuzzyDaysSinceClassCSync) forKey:[NSString stringWithFormat:@"%@-daysSinceClassCSync", viewName]];
238 [values setValue:@(fuzzyDaysSinceKSR) forKey:[NSString stringWithFormat:@"%@-daysSinceLastKeystateReady", viewName]];
239
240 BOOL hasTLKs = [view.keyHierarchyState isEqualToString:SecCKKSZoneKeyStateReady] || [view.keyHierarchyState isEqualToString:SecCKKSZoneKeyStateReadyPendingUnlock];
241 /* only synced recently if between [0...7, ie withing 7 days */
242 BOOL syncedClassARecently = fuzzyDaysSinceClassASync >= 0 && fuzzyDaysSinceClassASync < 7;
243 BOOL syncedClassCRecently = fuzzyDaysSinceClassCSync >= 0 && fuzzyDaysSinceClassCSync < 7;
244 BOOL incomingQueueIsErrorFree = view.lastIncomingQueueOperation.error == nil;
245 BOOL outgoingQueueIsErrorFree = view.lastOutgoingQueueOperation.error == nil;
246
247 NSString* hasTLKsKey = [NSString stringWithFormat:@"%@-%@", viewName, CKKSAnalyticsHasTLKs];
248 NSString* syncedClassARecentlyKey = [NSString stringWithFormat:@"%@-%@", viewName, CKKSAnalyticsSyncedClassARecently];
249 NSString* syncedClassCRecentlyKey = [NSString stringWithFormat:@"%@-%@", viewName, CKKSAnalyticsSyncedClassCRecently];
250 NSString* incomingQueueIsErrorFreeKey = [NSString stringWithFormat:@"%@-%@", viewName, CKKSAnalyticsIncomingQueueIsErrorFree];
251 NSString* outgoingQueueIsErrorFreeKey = [NSString stringWithFormat:@"%@-%@", viewName, CKKSAnalyticsOutgoingQueueIsErrorFree];
252
253 values[hasTLKsKey] = @(hasTLKs);
254 values[syncedClassARecentlyKey] = @(syncedClassARecently);
255 values[syncedClassCRecentlyKey] = @(syncedClassCRecently);
256 values[incomingQueueIsErrorFreeKey] = @(incomingQueueIsErrorFree);
257 values[outgoingQueueIsErrorFreeKey] = @(outgoingQueueIsErrorFree);
258
259 BOOL weThinkWeAreInSync = inCircle && hasTLKs && syncedClassARecently && syncedClassCRecently && incomingQueueIsErrorFree && outgoingQueueIsErrorFree;
260 NSString* inSyncKey = [NSString stringWithFormat:@"%@-%@", viewName, CKKSAnalyticsInSync];
261 values[inSyncKey] = @(weThinkWeAreInSync);
262
263 return values;
264 }];
265 }
266 }
267
268 -(void)dealloc {
269 [self clearAllViews];
270 }
271
272 dispatch_queue_t globalZoneStateQueue = NULL;
273 dispatch_once_t globalZoneStateQueueOnce;
274
275 // We can't load the rate limiter in an init method, as the method might end up calling itself (if the database layer isn't yet initialized).
276 // Lazy-load it here.
277 - (CKKSRateLimiter*)getGlobalRateLimiter {
278 dispatch_once(&globalZoneStateQueueOnce, ^{
279 globalZoneStateQueue = dispatch_queue_create("CKKS global zone state", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
280 });
281
282 if(_globalRateLimiter != nil) {
283 return _globalRateLimiter;
284 }
285
286 __block CKKSRateLimiter* blocklimit = nil;
287
288 dispatch_sync(globalZoneStateQueue, ^{
289 NSError* error = nil;
290
291 // Special object containing state for all zones. Currently, just the rate limiter.
292 CKKSZoneStateEntry* allEntry = [CKKSZoneStateEntry tryFromDatabase: @"all" error:&error];
293
294 if(error) {
295 secerror("CKKSViewManager: couldn't load global zone state: %@", error);
296 }
297
298 if(!error && allEntry.rateLimiter) {
299 blocklimit = allEntry.rateLimiter;
300 } else {
301 blocklimit = [[CKKSRateLimiter alloc] init];
302 }
303 });
304 _globalRateLimiter = blocklimit;
305 return _globalRateLimiter;
306 }
307
308 - (void)lockStateChangeNotification:(bool)unlocked
309 {
310 if (unlocked) {
311 [[CKKSAnalytics logger] setDateProperty:[NSDate date] forKey:CKKSAnalyticsLastUnlock];
312 }
313 }
314
315 - (NSSet<NSString*>*)defaultViewList {
316 NSSet<NSString*>* fullList = [OTSOSActualAdapter sosCKKSViewList];
317
318 // Not a great hack: if this platform is an aTV or a HomePod, then filter its list of views
319 bool filter = false;
320
321 #if TARGET_OS_TV
322 filter = true;
323 #elif TARGET_OS_WATCH
324 filter = false;
325 #elif TARGET_OS_IOS
326 filter = !OctagonPlatformSupportsSOS();
327 #elif TARGET_OS_OSX
328 filter = false;
329 #endif
330
331 if(filter) {
332 // For now, use a hardcoded allow list for TV/HomePod
333 NSMutableSet<NSString*>* allowList = [NSMutableSet setWithArray:@[@"Home", @"LimitedPeersAllowed"]];
334 [allowList intersectSet:fullList];
335 return allowList;
336 }
337
338 return fullList;
339 }
340
341 -(NSSet<NSString*>*)viewList {
342 if (_viewList) {
343 return _viewList;
344 } else {
345 return [self defaultViewList];
346 }
347 }
348
349 - (void)setViewList:(NSSet<NSString*>* _Nullable)newViewList {
350 _viewList = newViewList;
351 }
352
353 - (void)setView: (CKKSKeychainView*) obj {
354 CKKSKeychainView* kcv = nil;
355
356 @synchronized(self.views) {
357 kcv = self.views[obj.zoneName];
358 self.views[obj.zoneName] = obj;
359 }
360
361 if(kcv) {
362 [kcv cancelAllOperations];
363 }
364 }
365
366 - (void)clearAllViews {
367 NSArray<CKKSKeychainView*>* tempviews = nil;
368 @synchronized(self.views) {
369 tempviews = [self.views.allValues copy];
370 [self.views removeAllObjects];
371 }
372
373 for(CKKSKeychainView* view in tempviews) {
374 [view cancelAllOperations];
375 }
376 }
377
378 - (void)clearView:(NSString*) viewName {
379 CKKSKeychainView* kcv = nil;
380 @synchronized(self.views) {
381 kcv = self.views[viewName];
382 self.views[viewName] = nil;
383 }
384
385 if(kcv) {
386 [kcv cancelAllOperations];
387 }
388 }
389
390 - (CKKSKeychainView*)findView:(NSString*)viewName {
391 if(!viewName) {
392 return nil;
393 }
394 @synchronized(self.views) {
395 return self.views[viewName];
396 }
397 }
398
399 - (CKKSKeychainView*)findOrCreateView:(NSString*)viewName {
400 @synchronized(self.views) {
401 CKKSKeychainView* kcv = self.views[viewName];
402 if(kcv) {
403 return kcv;
404 }
405
406 self.views[viewName] = [[CKKSKeychainView alloc] initWithContainer: self.container
407 zoneName: viewName
408 accountTracker: self.accountTracker
409 lockStateTracker: self.lockStateTracker
410 reachabilityTracker: self.reachabilityTracker
411 changeFetcher:self.zoneChangeFetcher
412 zoneModifier:self.zoneModifier
413 savedTLKNotifier: self.savedTLKNotifier
414 cloudKitClassDependencies:self.cloudKitClassDependencies];
415 return self.views[viewName];
416 }
417 }
418
419 - (NSSet<CKKSKeychainView*>*)currentViews
420 {
421 @synchronized (self.views) {
422 NSMutableSet<CKKSKeychainView*>* viewObjects = [NSMutableSet set];
423 for(NSString* viewName in self.views) {
424 [viewObjects addObject:self.views[viewName]];
425 }
426 return viewObjects;
427 }
428 }
429
430 - (void)createViews
431 {
432 // In the future, the CKKSViewManager needs to persist its policy property through daemon restarts
433 // and load it here, before creating whatever views it was told to (in a previous daemon lifetime)
434 for (NSString* viewName in self.viewList) {
435 CKKSKeychainView* view = [self findOrCreateView:viewName];
436 (void)view;
437 }
438 }
439
440 - (void)beginCloudKitOperationOfAllViews
441 {
442 for (NSString* viewName in self.viewList) {
443 CKKSKeychainView* view = [self findView:viewName];
444 [view beginCloudKitOperation];
445 }
446 }
447
448 - (NSDictionary<NSString *,NSString *> *)activeTLKs
449 {
450 NSMutableDictionary<NSString *,NSString *> *tlks = [NSMutableDictionary new];
451 @synchronized(self.views) {
452 for (NSString *name in self.views) {
453 CKKSKeychainView *view = self.views[name];
454 NSString *tlk = view.lastActiveTLKUUID;
455 if (tlk) {
456 tlks[name] = tlk;
457 }
458 }
459 }
460 return tlks;
461 }
462
463 - (CKKSKeychainView*)restartZone:(NSString*)viewName {
464 @synchronized(self.views) {
465 [self.views[viewName] halt];
466 self.views[viewName] = nil;
467 }
468 return [self findOrCreateView: viewName];
469 }
470
471 - (NSString*)viewNameForViewHint: (NSString*) viewHint {
472 // For now, choose view based on viewhints.
473 if(viewHint && ![viewHint isEqual: [NSNull null]]) {
474 return viewHint;
475 }
476
477 // If there isn't a provided view hint, use the "keychain" view if we're testing. Otherwise, nil.
478 if(SecCKKSTestsEnabled()) {
479 return @"keychain";
480 } else {
481 return nil;
482 }
483 }
484
485 - (void) setOverrideCKKSViewsFromPolicy:(BOOL)value {
486 _overrideCKKSViewsFromPolicy = YES;
487 _valueCKKSViewsFromPolicy = value;
488 }
489
490 - (BOOL)useCKKSViewsFromPolicy {
491 if (self.overrideCKKSViewsFromPolicy) {
492 return self.valueCKKSViewsFromPolicy;
493 } else {
494 return os_feature_enabled(Security, CKKSViewsFromPolicy);
495 }
496 }
497
498 - (NSString*)viewNameForItem: (SecDbItemRef) item {
499 if ([self useCKKSViewsFromPolicy]) {
500 CFErrorRef cferror = NULL;
501 NSMutableDictionary *dict = (__bridge_transfer NSMutableDictionary*) SecDbItemCopyPListWithMask(item, ~0, &cferror);
502
503 if(cferror) {
504 secerror("ckks: Couldn't fetch attributes from item: %@", cferror);
505 CFReleaseNull(cferror);
506 return [self viewNameForViewHint: nil];
507 }
508 TPPolicy* policy = self.policy;
509 if (policy == nil) {
510 return [self viewNameForViewHint: nil];
511 }
512 NSString* view = [policy mapKeyToView:dict];
513 if (view == nil) {
514 return [self viewNameForViewHint: nil];
515 }
516 return view;
517 } else {
518 CFErrorRef cferror = NULL;
519 NSString* viewHint = (__bridge NSString*) SecDbItemGetValue(item, &v7vwht, &cferror);
520
521 if(cferror) {
522 secerror("ckks: Couldn't fetch the viewhint for some reason: %@", cferror);
523 CFReleaseNull(cferror);
524 viewHint = nil;
525 }
526
527 return [self viewNameForViewHint: viewHint];
528 }
529 }
530
531 - (void)registerSyncStatusCallback: (NSString*) uuid callback: (SecBoolNSErrorCallback) callback {
532 // Someone is requesting future notification of this item.
533 @synchronized(self.pendingSyncCallbacks) {
534 self.pendingSyncCallbacks[uuid] = callback;
535 }
536 }
537
538 - (void) handleKeychainEventDbConnection: (SecDbConnectionRef) dbconn source:(SecDbTransactionSource)txionSource added: (SecDbItemRef) added deleted: (SecDbItemRef) deleted {
539
540 SecDbItemRef modified = added ? added : deleted;
541
542 NSString* viewName = [self viewNameForItem: modified];
543 NSString* keyViewName = [CKKSKey isItemKeyForKeychainView: modified];
544
545 if(keyViewName) {
546 // This might be some key material for this view! Poke it.
547 CKKSKeychainView* view = [self findView: keyViewName];
548
549 if(!SecCKKSTestDisableKeyNotifications()) {
550 ckksnotice("ckks", view, "Potential new key material from %@ (source %lu)",
551 keyViewName, (unsigned long)txionSource);
552 [view keyStateMachineRequestProcess];
553 } else {
554 ckksnotice("ckks", view, "Ignoring potential new key material from %@ (source %lu)",
555 keyViewName, (unsigned long)txionSource);
556 }
557 return;
558 }
559
560 // When SOS is in charge of a view, CKKS is not.
561 // Since this isn't a CKKS key item, we don't care about it.
562 if(txionSource == kSecDbSOSTransaction) {
563 secinfo("ckks", "Ignoring new non-CKKS item in kSecDbSOSTransaction notification");
564 }
565
566 // Looks like a normal item. Proceed!
567 CKKSKeychainView* view = [self findView:viewName];
568
569 NSString* uuid = (__bridge NSString*) SecDbItemGetValue(modified, &v10itemuuid, NULL);
570 SecBoolNSErrorCallback syncCallback = nil;
571 if(uuid) {
572 @synchronized(self.pendingSyncCallbacks) {
573 syncCallback = self.pendingSyncCallbacks[uuid];
574 self.pendingSyncCallbacks[uuid] = nil;
575
576 if(syncCallback) {
577 secinfo("ckks", "Have a pending callback for %@; passing along", uuid);
578 }
579 }
580 }
581
582 if(!view) {
583 if(viewName) {
584 secnotice("ckks", "No CKKS view for %@, skipping: %@", viewName, modified);
585 } else {
586 secinfo("ckks", "No CKKS view for %@, skipping: %@", viewName, modified);
587 }
588 if(syncCallback) {
589 syncCallback(false, [NSError errorWithDomain:@"securityd"
590 code:kSOSCCNoSuchView
591 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"No syncing view for '%@'", viewName]}]);
592 }
593 return;
594 }
595
596 ckksnotice("ckks", view, "Routing item to zone %@: %@", viewName, modified);
597 [view handleKeychainEventDbConnection: dbconn added:added deleted:deleted rateLimiter:self.globalRateLimiter syncCallback: syncCallback];
598 }
599
600 -(void)setCurrentItemForAccessGroup:(NSData* _Nonnull)newItemPersistentRef
601 hash:(NSData*)newItemSHA1
602 accessGroup:(NSString*)accessGroup
603 identifier:(NSString*)identifier
604 viewHint:(NSString*)viewHint
605 replacing:(NSData* _Nullable)oldCurrentItemPersistentRef
606 hash:(NSData*)oldItemSHA1
607 complete:(void (^) (NSError* operror)) complete
608 {
609 CKKSKeychainView* view = [self findView:viewHint];
610
611 if(!view) {
612 secnotice("ckks", "No CKKS view for %@, skipping current request", viewHint);
613 complete([NSError errorWithDomain:CKKSErrorDomain
614 code:CKKSNoSuchView
615 description:[NSString stringWithFormat: @"No syncing view for view hint '%@'", viewHint]]);
616 return;
617 }
618
619 [view setCurrentItemForAccessGroup:newItemPersistentRef
620 hash:newItemSHA1
621 accessGroup:accessGroup
622 identifier:identifier
623 replacing:oldCurrentItemPersistentRef
624 hash:oldItemSHA1
625 complete:complete];
626 }
627
628 -(void)getCurrentItemForAccessGroup:(NSString*)accessGroup
629 identifier:(NSString*)identifier
630 viewHint:(NSString*)viewHint
631 fetchCloudValue:(bool)fetchCloudValue
632 complete:(void (^) (NSString* uuid, NSError* operror)) complete
633 {
634 CKKSKeychainView* view = [self findView:viewHint];
635 if(!view) {
636 secnotice("ckks", "No CKKS view for %@, skipping current fetch request", viewHint);
637 complete(NULL, [NSError errorWithDomain:CKKSErrorDomain
638 code:CKKSNoSuchView
639 description:[NSString stringWithFormat: @"No view for '%@'", viewHint]]);
640 return;
641 }
642
643 [view getCurrentItemForAccessGroup:accessGroup
644 identifier:identifier
645 fetchCloudValue:fetchCloudValue
646 complete:complete];
647 }
648
649
650 + (instancetype) manager {
651 return [self resetManager: false setTo: nil];
652 }
653
654 + (instancetype) resetManager: (bool) reset setTo: (CKKSViewManager*) obj {
655 static CKKSViewManager* manager = nil;
656
657 if([CKDatabase class] == nil) {
658 secerror("CKKS: CloudKit.framework appears to not be linked. Can't create CKKS objects.");
659 return nil;
660 }
661
662 if(!manager || reset || obj) {
663 @synchronized([self class]) {
664 if(obj != nil) {
665 [manager clearAllViews];
666 manager = obj;
667 } else {
668 if(reset) {
669 [manager clearAllViews];
670 manager = nil;
671 } else if (manager == nil && SecCKKSIsEnabled()) {
672 // The CKKSViewManager doesn't do much with its adapter, so leave as nonessentialq
673 manager = [[CKKSViewManager alloc] initWithContainerName:SecCKKSContainerName
674 usePCS:SecCKKSContainerUsePCS
675 sosAdapter:[[OTSOSActualAdapter alloc] initAsEssential:NO]
676 cloudKitClassDependencies:[CKKSCloudKitClassDependencies forLiveCloudKit]];
677 }
678 }
679 }
680 }
681
682 return manager;
683 }
684
685 - (void)cancelPendingOperations {
686 [self.savedTLKNotifier cancel];
687 }
688
689 -(void)notifyNewTLKsInKeychain {
690 // Why two functions here? Limitation of OCMock, unfortunately: can't stub and expect the same method
691 secnotice("ckksbackup", "New TLKs have arrived");
692 [self syncBackupAndNotifyAboutSync];
693 }
694
695 - (void)syncBackupAndNotifyAboutSync {
696 SOSAccount* account = (__bridge SOSAccount*)SOSKeychainAccountGetSharedAccount();
697
698 if(!account) {
699 secnotice("ckks", "Failed to get account object");
700 return;
701 }
702
703 [account performTransaction:^(SOSAccountTransaction * _Nonnull txn) {
704 CFErrorRef error = NULL;
705 NSSet* ignore = CFBridgingRelease(SOSAccountCopyBackupPeersAndForceSync(txn, &error));
706 (void)ignore;
707
708 if(error) {
709 secerror("ckksbackup: Couldn't process sync with backup peers: %@", error);
710 } else {
711 secnotice("ckksbackup", "telling CloudServices about TLK arrival");
712 notify_post(kSecItemBackupNotification);
713 };
714 }];
715 }
716
717 #pragma mark - RPCs to manage and report state
718
719 - (void)performanceCounters:(void(^)(NSDictionary <NSString *, NSNumber *> *counter))reply {
720 reply(@{});
721 }
722
723 - (NSArray<CKKSKeychainView*>*)views:(NSString*)viewName operation:(NSString*)opName error:(NSError**)error
724 {
725 NSArray* actualViews = nil;
726
727 // Ensure we've actually set up, but don't wait too long. Clients get impatient.
728 if([self.completedSecCKKSInitialize wait:5*NSEC_PER_SEC]) {
729 secerror("ckks: Haven't yet initialized zones; expect failure fetching views");
730 }
731
732 @synchronized(self.views) {
733 if(viewName) {
734 CKKSKeychainView* view = self.views[viewName];
735 secnotice("ckks", "Received a %@ request for zone %@ (%@)", opName, viewName, view);
736
737 if(!view) {
738 if(error) {
739 *error = [NSError errorWithDomain:CKKSErrorDomain
740 code:CKKSNoSuchView
741 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"No view for '%@'", viewName]}];
742 }
743 return nil;
744 }
745
746 actualViews = @[view];
747 } else {
748 actualViews = [self.views.allValues copy];
749 secnotice("ckks", "Received a %@ request for all zones: %@", opName, actualViews);
750 }
751 }
752 actualViews = [actualViews sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"zoneName" ascending:YES]]];
753 return actualViews;
754 }
755
756 - (void)rpcResetLocal:(NSString*)viewName reply: (void(^)(NSError* result)) reply {
757 NSError* localError = nil;
758 NSArray* actualViews = [self views:viewName operation:@"local reset" error:&localError];
759 if(localError) {
760 secerror("ckks: Error getting view %@: %@", viewName, localError);
761 reply(localError);
762 return;
763 }
764
765 CKKSResultOperation* op = [CKKSResultOperation named:@"local-reset-zones-waiter" withBlockTakingSelf:^(CKKSResultOperation * _Nonnull strongOp) {
766 if(!strongOp.error) {
767 secnotice("ckksreset", "Completed rpcResetLocal");
768 } else {
769 secnotice("ckks", "Completed rpcResetLocal with error: %@", strongOp.error);
770 }
771 reply(CKXPCSuitableError(strongOp.error));
772 }];
773
774 for(CKKSKeychainView* view in actualViews) {
775 ckksnotice("ckksreset", view, "Beginning local reset for %@", view);
776 [op addSuccessDependency:[view resetLocalData]];
777 }
778
779 [op timeout:120*NSEC_PER_SEC];
780 [self.operationQueue addOperation: op];
781 }
782
783 - (void)rpcResetCloudKit:(NSString*)viewName reason:(NSString *)reason reply:(void(^)(NSError* result)) reply {
784 NSError* localError = nil;
785 NSArray* actualViews = [self views:viewName operation:@"CloudKit reset" error:&localError];
786 if(localError) {
787 secerror("ckks: Error getting view %@: %@", viewName, localError);
788 reply(localError);
789 return;
790 }
791
792 CKKSResultOperation* op = [CKKSResultOperation named:@"cloudkit-reset-zones-waiter" withBlock:^() {}];
793
794 // Use the completion block instead of the operation block, so that it runs even if the cancel fires
795 __weak __typeof(op) weakOp = op;
796 [op setCompletionBlock:^{
797 __strong __typeof(op) strongOp = weakOp;
798 if(!strongOp.error) {
799 secnotice("ckksreset", "Completed rpcResetCloudKit");
800 } else {
801 secnotice("ckksreset", "Completed rpcResetCloudKit with error: %@", strongOp.error);
802 }
803 reply(CKXPCSuitableError(strongOp.error));
804 }];
805
806 for(CKKSKeychainView* view in actualViews) {
807 NSString *operationGroupName = [NSString stringWithFormat:@"api-reset-%@", reason];
808 ckksnotice("ckksreset", view, "Beginning CloudKit reset for %@: %@", view, reason);
809 [op addSuccessDependency:[view resetCloudKitZone:[CKOperationGroup CKKSGroupWithName:operationGroupName]]];
810 }
811
812 [op timeout:120*NSEC_PER_SEC];
813 [self.operationQueue addOperation: op];
814 }
815
816 - (void)rpcResync:(NSString*)viewName reply: (void(^)(NSError* result)) reply {
817 NSError* localError = nil;
818 NSArray* actualViews = [self views:viewName operation:@"CloudKit resync" error:&localError];
819 if(localError) {
820 secerror("ckks: Error getting view %@: %@", viewName, localError);
821 reply(localError);
822 return;
823 }
824
825 CKKSResultOperation* op = [[CKKSResultOperation alloc] init];
826 op.name = @"rpc-resync-cloudkit";
827 __weak __typeof(op) weakOp = op;
828
829 // Use the completion block instead of the operation block, so that it runs even if the cancel fires
830 [op setCompletionBlock:^{
831 __strong __typeof(op) strongOp = weakOp;
832 secnotice("ckks", "Ending rsync-CloudKit rpc with %@", strongOp.error);
833 reply(CKXPCSuitableError(strongOp.error));
834 }];
835
836 for(CKKSKeychainView* view in actualViews) {
837 ckksnotice("ckksresync", view, "Beginning resync (CloudKit) for %@", view);
838
839 CKKSSynchronizeOperation* resyncOp = [view resyncWithCloud];
840 [op addSuccessDependency:resyncOp];
841 }
842
843 [op timeout:120*NSEC_PER_SEC];
844 [self.operationQueue addOperation:op];
845 }
846
847 - (void)rpcResyncLocal:(NSString*)viewName reply:(void(^)(NSError* result))reply {
848 NSError* localError = nil;
849 NSArray* actualViews = [self views:viewName operation:@"local resync" error:&localError];
850 if(localError) {
851 secerror("ckks: Error getting view %@: %@", viewName, localError);
852 reply(localError);
853 return;
854 }
855
856 CKKSResultOperation* op = [[CKKSResultOperation alloc] init];
857 op.name = @"rpc-resync-local";
858 __weak __typeof(op) weakOp = op;
859 // Use the completion block instead of the operation block, so that it runs even if the cancel fires
860 [op setCompletionBlock:^{
861 __strong __typeof(op) strongOp = weakOp;
862 secnotice("ckks", "Ending rsync-local rpc with %@", strongOp.error);
863 reply(CKXPCSuitableError(strongOp.error));
864 }];
865
866 for(CKKSKeychainView* view in actualViews) {
867 ckksnotice("ckksresync", view, "Beginning resync (local) for %@", view);
868
869 CKKSLocalSynchronizeOperation* resyncOp = [view resyncLocal];
870 [op addSuccessDependency:resyncOp];
871 }
872
873 [op timeout:120*NSEC_PER_SEC];
874 }
875
876 - (void)rpcStatus: (NSString*)viewName
877 fast:(bool)fast
878 reply:(void(^)(NSArray<NSDictionary*>* result, NSError* error)) reply
879 {
880 NSMutableArray* a = [[NSMutableArray alloc] init];
881
882 // Now, query the views about their status
883 NSError* error = nil;
884 NSArray* actualViews = [self views:viewName operation:@"status" error:&error];
885 if(!actualViews || error) {
886 reply(nil, error);
887 return;
888 }
889
890 WEAKIFY(self);
891 CKKSResultOperation* statusOp = [CKKSResultOperation named:@"status-rpc" withBlock:^{
892 STRONGIFY(self);
893
894 if (fast == false) {
895 // Get account state, even wait for it a little
896 [self.accountTracker.ckdeviceIDInitialized wait:1*NSEC_PER_SEC];
897 NSString *deviceID = self.accountTracker.ckdeviceID;
898 NSError *deviceIDError = self.accountTracker.ckdeviceIDError;
899 NSDate *lastCKKSPush = [[CKKSAnalytics logger] datePropertyForKey:CKKSAnalyticsLastCKKSPush];
900
901 #define stringify(obj) CKKSNilToNSNull([obj description])
902 NSDictionary* global = @{
903 @"view": @"global",
904 @"reachability": self.reachabilityTracker.currentReachability ? @"network" : @"no-network",
905 @"ckdeviceID": CKKSNilToNSNull(deviceID),
906 @"ckdeviceIDError": CKKSNilToNSNull(deviceIDError),
907 @"lockstatetracker": stringify(self.lockStateTracker),
908 @"cloudkitRetryAfter": stringify(self.zoneModifier.cloudkitRetryAfter),
909 @"lastCKKSPush": CKKSNilToNSNull(lastCKKSPush),
910 };
911 [a addObject: global];
912 }
913
914 for(CKKSKeychainView* view in actualViews) {
915 NSDictionary* status = nil;
916 ckksnotice("ckks", view, "Fetching status for %@", view.zoneName);
917 if (fast) {
918 status = [view fastStatus];
919 } else {
920 status = [view status];
921 }
922 ckksinfo("ckks", view, "Status is %@", status);
923 if(status) {
924 [a addObject: status];
925 }
926 }
927 reply(a, nil);
928 }];
929
930 // If we're signed in, give the views a few seconds to enter what they consider to be a non-transient state (in case this daemon just launched)
931 if([self.accountTracker.ckAccountInfoInitialized wait:5*NSEC_PER_SEC]) {
932 secerror("ckks status: Haven't yet figured out cloudkit account state");
933 }
934
935 if(self.accountTracker.currentCKAccountInfo.accountStatus == CKAccountStatusAvailable) {
936 if (![self waitForTrustReady]) {
937 secerror("ckks status: Haven't yet figured out trust status");
938 }
939
940 CKKSResultOperation* blockOp = [CKKSResultOperation named:@"wait-for-status" withBlock:^{}];
941 [blockOp timeout:8*NSEC_PER_SEC];
942 for(CKKSKeychainView* view in actualViews) {
943 [blockOp addNullableDependency:view.keyStateNonTransientDependency];
944 }
945 [statusOp addDependency:blockOp];
946 [self.operationQueue addOperation:blockOp];
947 }
948 [self.operationQueue addOperation:statusOp];
949
950 return;
951 }
952
953 - (void)rpcStatus:(NSString*)viewName reply:(void (^)(NSArray<NSDictionary*>* result, NSError* error))reply
954 {
955 [self rpcStatus:viewName fast:false reply:reply];
956 }
957
958 - (void)rpcFastStatus:(NSString*)viewName reply:(void (^)(NSArray<NSDictionary*>* result, NSError* error))reply
959 {
960 [self rpcStatus:viewName fast:true reply:reply];
961 }
962
963 - (void)rpcFetchAndProcessChanges:(NSString*)viewName reply: (void(^)(NSError* result))reply {
964 [self rpcFetchAndProcessChanges:viewName classA:false reply: (void(^)(NSError* result))reply];
965 }
966
967 - (void)rpcFetchAndProcessClassAChanges:(NSString*)viewName reply: (void(^)(NSError* result))reply {
968 [self rpcFetchAndProcessChanges:viewName classA:true reply:(void(^)(NSError* result))reply];
969 }
970
971 - (void)rpcFetchAndProcessChanges:(NSString*)viewName classA:(bool)classAError reply: (void(^)(NSError* result)) reply {
972 NSError* error = nil;
973 NSArray* actualViews = [self views:viewName operation:@"fetch" error:&error];
974 if(!actualViews || error) {
975 reply(error);
976 return;
977 }
978
979 CKKSResultOperation* blockOp = [[CKKSResultOperation alloc] init];
980 blockOp.name = @"rpc-fetch-and-process-result";
981 __weak __typeof(blockOp) weakBlockOp = blockOp;
982 // Use the completion block instead of the operation block, so that it runs even if the cancel fires
983 [blockOp setCompletionBlock:^{
984 __strong __typeof(blockOp) strongBlockOp = weakBlockOp;
985 [strongBlockOp allDependentsSuccessful];
986 reply(CKXPCSuitableError(strongBlockOp.error));
987 }];
988
989 for(CKKSKeychainView* view in actualViews) {
990 ckksnotice("ckks", view, "Beginning fetch for %@", view);
991
992 CKKSResultOperation* op = [view processIncomingQueue:classAError after:[view.zoneChangeFetcher requestSuccessfulFetch: CKKSFetchBecauseAPIFetchRequest]];
993 [blockOp addDependency:op];
994 }
995
996 [self.operationQueue addOperation: [blockOp timeout:(SecCKKSTestsEnabled() ? NSEC_PER_SEC * 5 : NSEC_PER_SEC * 120)]];
997 }
998
999 - (void)rpcPushOutgoingChanges:(NSString*)viewName reply: (void(^)(NSError* result))reply {
1000 NSError* error = nil;
1001 NSArray* actualViews = [self views:viewName operation:@"push" error:&error];
1002 if(!actualViews || error) {
1003 reply(error);
1004 return;
1005 }
1006
1007 CKKSResultOperation* blockOp = [[CKKSResultOperation alloc] init];
1008 blockOp.name = @"rpc-push";
1009 __weak __typeof(blockOp) weakBlockOp = blockOp;
1010 // Use the completion block instead of the operation block, so that it runs even if the cancel fires
1011 [blockOp setCompletionBlock:^{
1012 __strong __typeof(blockOp) strongBlockOp = weakBlockOp;
1013 [strongBlockOp allDependentsSuccessful];
1014 reply(CKXPCSuitableError(strongBlockOp.error));
1015 }];
1016
1017 for(CKKSKeychainView* view in actualViews) {
1018 ckksnotice("ckks-rpc", view, "Beginning push for %@", view);
1019
1020 CKKSResultOperation* op = [view processOutgoingQueue: [CKOperationGroup CKKSGroupWithName:@"rpc-push"]];
1021 [blockOp addDependency:op];
1022 }
1023
1024 [self.operationQueue addOperation: [blockOp timeout:(SecCKKSTestsEnabled() ? NSEC_PER_SEC * 2 : NSEC_PER_SEC * 120)]];
1025 }
1026
1027 - (void)rpcGetCKDeviceIDWithReply:(void (^)(NSString *))reply {
1028 reply(self.accountTracker.ckdeviceID);
1029 }
1030
1031 - (void)rpcCKMetric:(NSString *)eventName attributes:(NSDictionary *)attributes reply:(void (^)(NSError *))reply
1032 {
1033 if (eventName == NULL) {
1034 reply([NSError errorWithDomain:CKKSErrorDomain
1035 code:CKKSNoMetric
1036 description:@"No metric name"]);
1037 return;
1038 }
1039 SecEventMetric *metric = [[SecEventMetric alloc] initWithEventName:eventName];
1040 [attributes enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull __unused stop) {
1041 metric[key] = obj;
1042 }];
1043 [[SecMetrics managerObject] submitEvent:metric];
1044 reply(NULL);
1045
1046 }
1047
1048 -(void)xpc24HrNotification {
1049 // XPC has poked us and said we should do some cleanup!
1050 secnotice("ckks", "Received a 24hr notification from XPC");
1051
1052 if (![self waitForTrustReady]) {
1053 secnotice("ckks", "Trust not ready, still going ahead");
1054 }
1055
1056 [[CKKSAnalytics logger] dailyCoreAnalyticsMetrics:@"com.apple.security.CKKSHealthSummary"];
1057
1058 // For now, poke the views and tell them to update their device states if they'd like
1059 NSArray* actualViews = nil;
1060 @synchronized(self.views) {
1061 // Can't safely iterate a mutable collection, so copy it.
1062 actualViews = self.views.allValues;
1063 }
1064
1065 CKOperationGroup* group = [CKOperationGroup CKKSGroupWithName:@"periodic-device-state-update"];
1066 for(CKKSKeychainView* view in actualViews) {
1067 ckksnotice("ckks", view, "Starting device state XPC update");
1068 // Let the update know it should rate-limit itself
1069 [view updateDeviceState:true waitForKeyHierarchyInitialization:30*NSEC_PER_SEC ckoperationGroup:group];
1070 }
1071 }
1072
1073 - (void)haltAll
1074 {
1075 [self.zoneModifier halt];
1076
1077 @synchronized(self.views) {
1078 for(CKKSKeychainView* view in self.views.allValues) {
1079 [view halt];
1080 }
1081 }
1082
1083 }
1084
1085 #endif // OCTAGON
1086 @end