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