/*
* Copyright (c) 2016 Apple Inc. All Rights Reserved.
*
* @APPLE_LICENSE_HEADER_START@
*
* This file contains Original Code and/or Modifications of Original Code
* as defined in and that are subject to the Apple Public Source License
* Version 2.0 (the 'License'). You may not use this file except in
* compliance with the License. Please obtain a copy of the License at
* http://www.opensource.apple.com/apsl/ and read it before using this
* file.
*
* The Original Code and all software distributed under the License are
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
* Please see the License for the specific language governing rights and
* limitations under the License.
*
* @APPLE_LICENSE_HEADER_END@
*/
#import "keychain/ckks/CKKSViewManager.h"
#import "keychain/ckks/CKKSKeychainView.h"
#import "keychain/ckks/CKKSSynchronizeOperation.h"
#import "keychain/ckks/CKKSKey.h"
#import "keychain/ckks/CKKSZoneStateEntry.h"
#import "keychain/ckks/CKKSNearFutureScheduler.h"
#import "keychain/ckks/CKKSNotifier.h"
#import "keychain/ckks/CKKSCondition.h"
#import "keychain/ckks/CloudKitCategories.h"
#import "CKKSAnalyticsLogger.h"
#import "SecEntitlements.h"
#include <securityd/SecDbItem.h>
#include <securityd/SecDbKeychainItem.h>
#include <securityd/SecItemSchema.h>
#include <Security/SecureObjectSync/SOSViews.h>
#import <Foundation/NSXPCConnection.h>
#import <Foundation/NSXPCConnection_Private.h>
#include <Security/SecureObjectSync/SOSAccount.h>
#include <Security/SecItemBackup.h>
#if OCTAGON
#import <CloudKit/CloudKit.h>
#import <CloudKit/CloudKit_Private.h>
#import <SecurityFoundation/SFKey.h>
#import <SecurityFoundation/SFKey_Private.h>
#endif
@interface CKKSViewManager () <NSXPCListenerDelegate>
#if OCTAGON
@property NSXPCListener *listener;
// Once you set these, all CKKSKeychainViews created will use them
@property (readonly) Class<CKKSFetchRecordZoneChangesOperation> fetchRecordZoneChangesOperationClass;
@property (readonly) Class<CKKSFetchRecordsOperation> fetchRecordsOperationClass;
@property (readonly) Class<CKKSQueryOperation> queryOperationClass;
@property (readonly) Class<CKKSModifySubscriptionsOperation> modifySubscriptionsOperationClass;
@property (readonly) Class<CKKSModifyRecordZonesOperation> modifyRecordZonesOperationClass;
@property (readonly) Class<CKKSAPSConnection> apsConnectionClass;
@property (readonly) Class<CKKSNotifier> notifierClass;
@property (readonly) Class<CKKSNSNotificationCenter> nsnotificationCenterClass;
@property NSMutableDictionary<NSString*, CKKSKeychainView*>* views;
@property NSMutableDictionary<NSString*, SecBoolNSErrorCallback>* pendingSyncCallbacks;
@property CKKSNearFutureScheduler* savedTLKNotifier;;
@property NSOperationQueue* operationQueue;
@property NSMapTable<dispatch_queue_t, id<CKKSPeerUpdateListener>>* peerChangeListeners;
#endif
@end
@implementation CKKSViewManager
#if OCTAGON
- (instancetype)initCloudKitWithContainerName: (NSString*) containerName usePCS:(bool)usePCS {
return [self initWithContainerName:containerName
usePCS:usePCS
fetchRecordZoneChangesOperationClass:[CKFetchRecordZoneChangesOperation class]
fetchRecordsOperationClass:[CKFetchRecordsOperation class]
queryOperationClass:[CKQueryOperation class]
modifySubscriptionsOperationClass:[CKModifySubscriptionsOperation class]
modifyRecordZonesOperationClass:[CKModifyRecordZonesOperation class]
apsConnectionClass:[APSConnection class]
nsnotificationCenterClass:[NSNotificationCenter class]
notifierClass:[CKKSNotifyPostNotifier class]
setupHold:nil];
}
- (instancetype)initWithContainerName: (NSString*) containerName
usePCS:(bool)usePCS
fetchRecordZoneChangesOperationClass: (Class<CKKSFetchRecordZoneChangesOperation>) fetchRecordZoneChangesOperationClass
fetchRecordsOperationClass: (Class<CKKSFetchRecordsOperation>)fetchRecordsOperationClass
queryOperationClass:(Class<CKKSQueryOperation>)queryOperationClass
modifySubscriptionsOperationClass: (Class<CKKSModifySubscriptionsOperation>) modifySubscriptionsOperationClass
modifyRecordZonesOperationClass: (Class<CKKSModifyRecordZonesOperation>) modifyRecordZonesOperationClass
apsConnectionClass: (Class<CKKSAPSConnection>) apsConnectionClass
nsnotificationCenterClass: (Class<CKKSNSNotificationCenter>) nsnotificationCenterClass
notifierClass: (Class<CKKSNotifier>) notifierClass
setupHold: (NSOperation*) setupHold {
if(self = [super init]) {
_fetchRecordZoneChangesOperationClass = fetchRecordZoneChangesOperationClass;
_fetchRecordsOperationClass = fetchRecordsOperationClass;
_queryOperationClass = queryOperationClass;
_modifySubscriptionsOperationClass = modifySubscriptionsOperationClass;
_modifyRecordZonesOperationClass = modifyRecordZonesOperationClass;
_apsConnectionClass = apsConnectionClass;
_nsnotificationCenterClass = nsnotificationCenterClass;
_notifierClass = notifierClass;
_container = [self makeCKContainer: containerName usePCS:usePCS];
_accountTracker = [[CKKSCKAccountStateTracker alloc] init:self.container nsnotificationCenterClass:nsnotificationCenterClass];
_lockStateTracker = [[CKKSLockStateTracker alloc] init];
_operationQueue = [[NSOperationQueue alloc] init];
// Backwards from how we'd like, but it's the best way to have weak pointers to CKKSPeerUpdateListener.
_peerChangeListeners = [NSMapTable strongToWeakObjectsMapTable];
_views = [[NSMutableDictionary alloc] init];
_pendingSyncCallbacks = [[NSMutableDictionary alloc] init];
_zoneStartupDependency = setupHold;
_initializeNewZones = false;
_completedSecCKKSInitialize = [[CKKSCondition alloc] init];
__weak __typeof(self) weakSelf = self;
_savedTLKNotifier = [[CKKSNearFutureScheduler alloc] initWithName: @"newtlks" delay:5*NSEC_PER_SEC keepProcessAlive:true block:^{
[weakSelf notifyNewTLKsInKeychain];
}];
_listener = [NSXPCListener anonymousListener];
_listener.delegate = self;
[_listener resume];
// If this is a live server, register with notify
if(!SecCKKSTestsEnabled()) {
int token = 0;
notify_register_dispatch(kSOSCCCircleOctagonKeysChangedNotification, &token, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^(int t) {
// Since SOS doesn't change the self peer, we can reliably just send "trusted peers changed"; it'll be mostly right
secnotice("ckksshare", "Received a notification that the SOS Octagon peer set changed");
[weakSelf sendTrustedPeerSetChangedUpdate];
});
}
}
return self;
}
-(CKContainer*)makeCKContainer:(NSString*)containerName usePCS:(bool)usePCS {
CKContainer* container = [CKContainer containerWithIdentifier:containerName];
if(!usePCS) {
CKContainerOptions* containerOptions = [[CKContainerOptions alloc] init];
containerOptions.bypassPCSEncryption = YES;
// We don't have a great way to set these, so replace the entire container object
container = [[CKContainer alloc] initWithContainerID: container.containerID options:containerOptions];
}
return container;
}
-(void)dealloc {
[self clearAllViews];
}
dispatch_queue_t globalZoneStateQueue = NULL;
dispatch_once_t globalZoneStateQueueOnce;
// 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).
// Lazy-load it here.
- (CKKSRateLimiter*)getGlobalRateLimiter {
dispatch_once(&globalZoneStateQueueOnce, ^{
globalZoneStateQueue = dispatch_queue_create("CKKS global zone state", DISPATCH_QUEUE_SERIAL);
});
if(_globalRateLimiter != nil) {
return _globalRateLimiter;
}
__block CKKSRateLimiter* blocklimit = nil;
dispatch_sync(globalZoneStateQueue, ^{
NSError* error = nil;
// Special object containing state for all zones. Currently, just the rate limiter.
CKKSZoneStateEntry* allEntry = [CKKSZoneStateEntry tryFromDatabase: @"all" error:&error];
if(error) {
secerror("CKKSViewManager: couldn't load global zone state: }
if(!error && allEntry.rateLimiter) {
blocklimit = allEntry.rateLimiter;
} else {
blocklimit = [[CKKSRateLimiter alloc] init];
}
});
_globalRateLimiter = blocklimit;
return _globalRateLimiter;
}
// Mostly exists to be mocked out.
-(NSSet*)viewList {
return CFBridgingRelease(SOSViewCopyViewSet(kViewSetCKKS));
}
- (void)setView: (CKKSKeychainView*) obj {
CKKSKeychainView* kcv = nil;
@synchronized(self.views) {
kcv = self.views[obj.zoneName];
self.views[obj.zoneName] = obj;
}
if(kcv) {
[kcv cancelAllOperations];
}
}
- (void)clearAllViews {
NSArray<CKKSKeychainView*>* tempviews = nil;
@synchronized(self.views) {
tempviews = [self.views.allValues copy];
[self.views removeAllObjects];
}
for(CKKSKeychainView* view in tempviews) {
[view cancelAllOperations];
}
}
- (void)clearView:(NSString*) viewName {
CKKSKeychainView* kcv = nil;
@synchronized(self.views) {
kcv = self.views[viewName];
self.views[viewName] = nil;
}
if(kcv) {
[kcv cancelAllOperations];
}
}
- (CKKSKeychainView*)findView:(NSString*)viewName {
if(!viewName) {
return nil;
}
@synchronized(self.views) {
return self.views[viewName];
}
}
- (CKKSKeychainView*)findOrCreateView:(NSString*)viewName {
@synchronized(self.views) {
CKKSKeychainView* kcv = self.views[viewName];
if(kcv) {
return kcv;
}
self.views[viewName] = [[CKKSKeychainView alloc] initWithContainer: self.container
zoneName: viewName
accountTracker: self.accountTracker
lockStateTracker: self.lockStateTracker
savedTLKNotifier: self.savedTLKNotifier
peerProvider:self
fetchRecordZoneChangesOperationClass: self.fetchRecordZoneChangesOperationClass
fetchRecordsOperationClass: self.fetchRecordsOperationClass
queryOperationClass:self.queryOperationClass
modifySubscriptionsOperationClass: self.modifySubscriptionsOperationClass
modifyRecordZonesOperationClass: self.modifyRecordZonesOperationClass
apsConnectionClass: self.apsConnectionClass
notifierClass: self.notifierClass];
if(self.zoneStartupDependency) {
[self.views[viewName].zoneSetupOperation addDependency: self.zoneStartupDependency];
}
if(self.initializeNewZones) {
[self.views[viewName] initializeZone];
}
return self.views[viewName];
}
}
+ (CKKSKeychainView*)findOrCreateView:(NSString*)viewName {
return [[CKKSViewManager manager] findOrCreateView: viewName];
}
- (NSDictionary<NSString *,NSString *> *)activeTLKs
{
NSMutableDictionary<NSString *,NSString *> *tlks = [NSMutableDictionary new];
@synchronized(self.views) {
for (NSString *name in self.views) {
CKKSKeychainView *view = self.views[name];
NSString *tlk = view.lastActiveTLKUUID;
if (tlk) {
tlks[name] = tlk;
}
}
}
return tlks;
}
- (CKKSKeychainView*)restartZone:(NSString*)viewName {
@synchronized(self.views) {
self.views[viewName] = nil;
}
return [self findOrCreateView: viewName];
}
// Allows all views to begin initializing, and opens the floodgates so that new views will be initalized immediately
- (void)initializeZones {
if(!SecCKKSIsEnabled()) {
secnotice("ckks", "Not initializing CKKS view set as CKKS is disabled");
return;
}
@synchronized(self.views) {
self.initializeNewZones = true;
NSSet* viewSet = [self viewList];
for(NSString* s in viewSet) {
[self findOrCreateView:s]; // initializes any newly-created views
}
}
}
- (NSString*)viewNameForViewHint: (NSString*) viewHint {
// For now, choose view based on viewhints.
if(viewHint && ![viewHint isEqual: [NSNull null]]) {
return viewHint;
}
// If there isn't a provided view hint, use the "keychain" view if we're testing. Otherwise, nil.
if(SecCKKSTestsEnabled()) {
return @"keychain";
} else {
return nil;
}
}
- (NSString*)viewNameForItem: (SecDbItemRef) item {
CFErrorRef cferror = NULL;
NSString* viewHint = (__bridge NSString*) SecDbItemGetValue(item, &v7vwht, &cferror);
if(cferror) {
secerror("ckks: Couldn't fetch the viewhint for some reason: CFReleaseNull(cferror);
viewHint = nil;
}
return [self viewNameForViewHint: viewHint];
}
- (NSString*)viewNameForAttributes: (NSDictionary*) item {
return [self viewNameForViewHint: item[(id)kSecAttrSyncViewHint]];
}
- (void)registerSyncStatusCallback: (NSString*) uuid callback: (SecBoolNSErrorCallback) callback {
// Someone is requesting future notification of this item.
@synchronized(self.pendingSyncCallbacks) {
self.pendingSyncCallbacks[uuid] = callback;
}
}
- (void) handleKeychainEventDbConnection: (SecDbConnectionRef) dbconn source:(SecDbTransactionSource)txionSource added: (SecDbItemRef) added deleted: (SecDbItemRef) deleted {
SecDbItemRef modified = added ? added : deleted;
NSString* viewName = [self viewNameForItem: modified];
NSString* keyViewName = [CKKSKey isItemKeyForKeychainView: modified];
if(keyViewName) {
// This might be some key material for this view! Poke it.
CKKSKeychainView* view = [self findView: keyViewName];
if(!SecCKKSTestDisableKeyNotifications()) {
ckksnotice("ckks", view, "Potential new key material from [view keyStateMachineRequestProcess];
} else {
ckksnotice("ckks", view, "Ignoring potential new key material from }
return;
}
// When SOS is in charge of a view, CKKS is not.
// Since this isn't a CKKS key item, we don't care about it.
if(txionSource == kSecDbSOSTransaction) {
secinfo("ckks", "Ignoring new non-CKKS item in kSecDbSOSTransaction notification");
}
// Looks like a normal item. Proceed!
CKKSKeychainView* view = [self findView:viewName];
NSString* uuid = (__bridge NSString*) SecDbItemGetValue(modified, &v10itemuuid, NULL);
SecBoolNSErrorCallback syncCallback = nil;
if(uuid) {
@synchronized(self.pendingSyncCallbacks) {
syncCallback = self.pendingSyncCallbacks[uuid];
self.pendingSyncCallbacks[uuid] = nil;
if(syncCallback) {
secinfo("ckks", "Have a pending callback for }
}
}
if(!view) {
secinfo("ckks", "No CKKS view for if(syncCallback) {
syncCallback(false, [NSError errorWithDomain:@"securityd"
code:kSOSCCNoSuchView
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"No syncing view for '%@'", viewName]}]);
}
return;
}
ckksnotice("ckks", view, "Routing item to zone [view handleKeychainEventDbConnection: dbconn added:added deleted:deleted rateLimiter:self.globalRateLimiter syncCallback: syncCallback];
}
-(void)setCurrentItemForAccessGroup:(SecDbItemRef)newItem
hash:(NSData*)newItemSHA1
accessGroup:(NSString*)accessGroup
identifier:(NSString*)identifier
viewHint:(NSString*)viewHint
replacing:(SecDbItemRef)oldItem
hash:(NSData*)oldItemSHA1
complete:(void (^) (NSError* operror)) complete
{
CKKSKeychainView* view = [self findView:viewHint];
if(!view) {
secnotice("ckks", "No CKKS view for complete([NSError errorWithDomain:CKKSErrorDomain
code:CKKSNoSuchView
description:[NSString stringWithFormat: @"No syncing view for view hint '%@'", viewHint]]);
return;
}
[view setCurrentItemForAccessGroup:newItem
hash:newItemSHA1
accessGroup:accessGroup
identifier:identifier
replacing:oldItem
hash:oldItemSHA1
complete:complete];
}
-(void)getCurrentItemForAccessGroup:(NSString*)accessGroup
identifier:(NSString*)identifier
viewHint:(NSString*)viewHint
fetchCloudValue:(bool)fetchCloudValue
complete:(void (^) (NSString* uuid, NSError* operror)) complete
{
CKKSKeychainView* view = [self findView:viewHint];
if(!view) {
secnotice("ckks", "No CKKS view for complete(NULL, [NSError errorWithDomain:CKKSErrorDomain
code:CKKSNoSuchView
description:[NSString stringWithFormat: @"No view for '%@'", viewHint]]);
return;
}
[view getCurrentItemForAccessGroup:accessGroup
identifier:identifier
fetchCloudValue:fetchCloudValue
complete:complete];
}
+ (instancetype) manager {
return [self resetManager: false setTo: nil];
}
+ (instancetype) resetManager: (bool) reset setTo: (CKKSViewManager*) obj {
static CKKSViewManager* manager = nil;
if([CKDatabase class] == nil) {
secerror("CKKS: CloudKit.framework appears to not be linked. Can't create CKKS objects.");
return nil;
}
if(!manager || reset || obj) {
@synchronized([self class]) {
if(obj != nil) {
[manager clearAllViews];
manager = obj;
} else {
if(reset) {
[manager clearAllViews];
manager = nil;
} else if (manager == nil) {
manager = [[CKKSViewManager alloc] initCloudKitWithContainerName:SecCKKSContainerName usePCS:SecCKKSContainerUsePCS];
}
}
}
}
return manager;
}
- (void)cancelPendingOperations {
[self.savedTLKNotifier cancel];
}
-(void)notifyNewTLKsInKeychain {
// Why two functions here? Limitation of OCMock, unfortunately: can't stub and expect the same method
secnotice("ckksbackup", "New TLKs have arrived");
[self syncBackupAndNotifyAboutSync];
}
- (void)syncBackupAndNotifyAboutSync {
SOSAccount* account = (__bridge SOSAccount*)SOSKeychainAccountGetSharedAccount();
[account performTransaction:^(SOSAccountTransaction * _Nonnull txn) {
CFErrorRef error = NULL;
NSSet* ignore = CFBridgingRelease(SOSAccountCopyBackupPeersAndForceSync(txn, &error));
(void)ignore;
if(error) {
secerror("ckksbackup: Couldn't process sync with backup peers: } else {
secnotice("ckksbackup", "telling CloudServices about TLK arrival");
notify_post(kSecItemBackupNotification);
};
}];
}
#pragma mark - XPC Endpoint
- (xpc_endpoint_t)xpcControlEndpoint {
return [_listener.endpoint _endpoint];
}
- (BOOL)listener:(__unused NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection {
NSNumber *num = [newConnection valueForEntitlement:(__bridge NSString *)kSecEntitlementPrivateCKKS];
if (![num isKindOfClass:[NSNumber class]] || ![num boolValue]) {
secinfo("ckks", "Client pid: [newConnection processIdentifier], kSecEntitlementPrivateCKKS);
return NO;
}
newConnection.exportedInterface = CKKSSetupControlProtocol([NSXPCInterface interfaceWithProtocol:@protocol(CKKSControlProtocol)]);
newConnection.exportedObject = self;
[newConnection resume];
return YES;
}
#pragma mark - RPCs to manage and report state
- (void)performanceCounters:(void(^)(NSDictionary <NSString *, NSNumber *> *counter))reply {
reply(@{});
}
- (void)rpcResetLocal:(NSString*)viewName reply: (void(^)(NSError* result)) reply {
NSArray* actualViews = nil;
if(viewName) {
secnotice("ckksreset", "Received a local reset RPC for zone CKKSKeychainView* view = self.views[viewName];
if(!view) {
secerror("ckks: Zone reply([NSError errorWithDomain:@"securityd"
code:kSOSCCNoSuchView
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"No view for '%@'", viewName]}]);
return;
}
actualViews = @[view];
} else {
secnotice("ckksreset", "Received a local reset RPC for all zones");
@synchronized(self.views) {
// Can't safely iterate a mutable collection, so copy it.
actualViews = [self.views.allValues copy];
}
}
CKKSResultOperation* op = [CKKSResultOperation named:@"local-reset-zones-waiter" withBlock:^{}];
for(CKKSKeychainView* view in actualViews) {
ckksnotice("ckksreset", view, "Beginning local reset for [op addSuccessDependency:[view resetLocalData]];
}
[op timeout:120*NSEC_PER_SEC];
[self.operationQueue addOperation: op];
[op waitUntilFinished];
if(op.error) {
secnotice("ckksreset", "Completed rpcResetLocal");
} else {
secnotice("ckksreset", "Completed rpcResetLocal with error: }
reply(CKXPCSuitableError(op.error));
}
- (void)rpcResetCloudKit:(NSString*)viewName reply: (void(^)(NSError* result)) reply {
NSArray* actualViews = nil;
if(viewName) {
secnotice("ckksreset", "Received a cloudkit reset RPC for zone CKKSKeychainView* view = self.views[viewName];
if(!view) {
secerror("ckks: Zone reply([NSError errorWithDomain:@"securityd"
code:kSOSCCNoSuchView
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"No view for '%@'", viewName]}]);
return;
}
actualViews = @[view];
} else {
secnotice("ckksreset", "Received a cloudkit reset RPC for all zones");
@synchronized(self.views) {
// Can't safely iterate a mutable collection, so copy it.
actualViews = [self.views.allValues copy];
}
}
CKKSResultOperation* op = [CKKSResultOperation named:@"cloudkit-reset-zones-waiter" withBlock:^{}];
for(CKKSKeychainView* view in actualViews) {
ckksnotice("ckksreset", view, "Beginning CloudKit reset for [op addSuccessDependency:[view resetCloudKitZone]];
}
[op timeout:120*NSEC_PER_SEC];
[self.operationQueue addOperation: op];
[op waitUntilFinished];
if(op.error) {
secnotice("ckksreset", "Completed rpcResetCloudKit");
} else {
secnotice("ckksreset", "Completed rpcResetCloudKit with error: }
reply(CKXPCSuitableError(op.error));
}
- (void)rpcResync:(NSString*)viewName reply: (void(^)(NSError* result)) reply {
secnotice("ckksresync", "Received a resync RPC for zone
NSArray* actualViews = nil;
if(viewName) {
secnotice("ckks", "Received a resync RPC for zone CKKSKeychainView* view = self.views[viewName];
if(!view) {
secerror("ckks: Zone reply(nil);
return;
}
actualViews = @[view];
} else {
@synchronized(self.views) {
// Can't safely iterate a mutable collection, so copy it.
actualViews = [self.views.allValues copy];
}
}
CKKSResultOperation* op = [[CKKSResultOperation alloc] init];
op.name = @"rpc-resync";
__weak __typeof(op) weakOp = op;
[op addExecutionBlock:^{
__strong __typeof(op) strongOp = weakOp;
secnotice("ckks", "Ending rsync rpc with }];
for(CKKSKeychainView* view in actualViews) {
ckksnotice("ckksresync", view, "Beginning resync for
CKKSSynchronizeOperation* resyncOp = [view resyncWithCloud];
[op addSuccessDependency:resyncOp];
}
[op timeout:120*NSEC_PER_SEC];
[self.operationQueue addOperation:op];
[op waitUntilFinished];
reply(CKXPCSuitableError(op.error));
}
- (void)rpcStatus: (NSString*)viewName reply: (void(^)(NSArray<NSDictionary*>* result, NSError* error)) reply {
NSMutableArray* a = [[NSMutableArray alloc] init];
// The first element is always the current global state (non-view-specific)
NSError* selfPeersError = nil;
CKKSSelves* selves = [self fetchSelfPeers:&selfPeersError];
NSError* trustedPeersError = nil;
NSSet<id<CKKSPeer>>* peers = [self fetchTrustedPeers:&trustedPeersError];
NSMutableArray<NSString*>* mutTrustedPeers = [[NSMutableArray alloc] init];
[peers enumerateObjectsUsingBlock:^(id<CKKSPeer> _Nonnull obj, BOOL * _Nonnull stop) {
[mutTrustedPeers addObject: [obj description]];
}];
#define stringify(obj) CKKSNilToNSNull([obj description])
NSDictionary* global = @{
@"view": @"global",
@"selfPeers": stringify(selves),
@"selfPeersError": CKKSNilToNSNull(selfPeersError),
@"trustedPeers": CKKSNilToNSNull(mutTrustedPeers),
@"trustedPeersError": CKKSNilToNSNull(trustedPeersError),
};
[a addObject: global];
// Now, query the views about their status
NSArray* actualViews = nil;
if(viewName) {
secnotice("ckks", "Received a status RPC for zone CKKSKeychainView* view = self.views[viewName];
if(!view) {
secerror("ckks: Zone reply(nil, nil);
return;
}
actualViews = @[view];
} else {
@synchronized(self.views) {
// Can't safely iterate a mutable collection, so copy it.
actualViews = self.views.allValues;
}
}
for(CKKSKeychainView* view in actualViews) {
ckksnotice("ckks", view, "Fetching status for NSDictionary* status = [view status];
ckksinfo("ckks", view, "Status is if(status) {
[a addObject: status];
}
}
reply(a, nil);
return;
}
- (void)rpcFetchAndProcessChanges:(NSString*)viewName reply: (void(^)(NSError* result))reply {
[self rpcFetchAndProcessChanges:viewName classA:false reply: (void(^)(NSError* result))reply];
}
- (void)rpcFetchAndProcessClassAChanges:(NSString*)viewName reply: (void(^)(NSError* result))reply {
[self rpcFetchAndProcessChanges:viewName classA:true reply:(void(^)(NSError* result))reply];
}
- (void)rpcFetchAndProcessChanges:(NSString*)viewName classA:(bool)classAError reply: (void(^)(NSError* result)) reply {
NSArray* actualViews = nil;
if(viewName) {
secnotice("ckks", "Received a fetch RPC for zone CKKSKeychainView* view = self.views[viewName];
if(!view) {
secerror("ckks: Zone reply([NSError errorWithDomain:@"securityd"
code:kSOSCCNoSuchView
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"No view for '%@'", viewName]}]);
return;
}
actualViews = @[view];
} else {
secnotice("ckks", "Received a fetch RPC for all zones");
@synchronized(self.views) {
// Can't safely iterate a mutable collection, so copy it.
actualViews = [self.views.allValues copy];
}
}
CKKSResultOperation* blockOp = [[CKKSResultOperation alloc] init];
blockOp.name = @"rpc-fetch-and-process-result";
__weak __typeof(blockOp) weakBlockOp = blockOp;
// Use the completion block instead of the operation block, so that it runs even if the cancel fires
[blockOp setCompletionBlock:^{
__strong __typeof(blockOp) strongBlockOp = weakBlockOp;
[strongBlockOp allDependentsSuccessful];
reply(CKXPCSuitableError(strongBlockOp.error));
}];
for(CKKSKeychainView* view in actualViews) {
ckksnotice("ckks", view, "Beginning fetch for
CKKSResultOperation* op = [view processIncomingQueue:classAError after:[view.zoneChangeFetcher requestSuccessfulFetch: CKKSFetchBecauseAPIFetchRequest]];
[blockOp addDependency:op];
}
[self.operationQueue addOperation: [blockOp timeout:60*NSEC_PER_SEC]];
}
- (void)rpcPushOutgoingChanges:(NSString*)viewName reply: (void(^)(NSError* result))reply {
NSArray* actualViews = nil;
if(viewName) {
secnotice("ckks", "Received a push RPC for zone CKKSKeychainView* view = self.views[viewName];
if(!view) {
secerror("ckks: Zone reply([NSError errorWithDomain:@"securityd"
code:kSOSCCNoSuchView
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"No view for '%@'", viewName]}]);
return;
}
actualViews = @[view];
} else {
secnotice("ckks", "Received a push RPC for all zones");
@synchronized(self.views) {
// Can't safely iterate a mutable collection, so copy it.
actualViews = [self.views.allValues copy];
}
}
CKKSResultOperation* blockOp = [[CKKSResultOperation alloc] init];
blockOp.name = @"rpc-push";
__weak __typeof(blockOp) weakBlockOp = blockOp;
// Use the completion block instead of the operation block, so that it runs even if the cancel fires
[blockOp setCompletionBlock:^{
__strong __typeof(blockOp) strongBlockOp = weakBlockOp;
[strongBlockOp allDependentsSuccessful];
reply(CKXPCSuitableError(strongBlockOp.error));
}];
for(CKKSKeychainView* view in actualViews) {
ckksnotice("ckks-rpc", view, "Beginning push for
CKKSResultOperation* op = [view processOutgoingQueue: [CKOperationGroup CKKSGroupWithName:@"rpc-push"]];
[blockOp addDependency:op];
}
[self.operationQueue addOperation: [blockOp timeout:60*NSEC_PER_SEC]];
}
- (void)rpcGetAnalyticsSysdiagnoseWithReply:(void (^)(NSString* sysdiagnose, NSError* error))reply
{
NSError* error = nil;
NSString* sysdiagnose = [[CKKSAnalyticsLogger logger] getSysdiagnoseDumpWithError:&error];
reply(sysdiagnose, CKXPCSuitableError(error));
}
- (void)rpcGetAnalyticsJSONWithReply:(void (^)(NSData* json, NSError* error))reply
{
NSError* error = nil;
NSData* json = [[CKKSAnalyticsLogger logger] getLoggingJSON:true error:&error];
reply(json, CKXPCSuitableError(error));
}
- (void)rpcForceUploadAnalyticsWithReply:(void (^)(BOOL success, NSError* error))reply
{
NSError* error = nil;
BOOL result = [[CKKSAnalyticsLogger logger] forceUploadWithError:&error];
reply(result, CKXPCSuitableError(error));
}
-(void)xpc24HrNotification {
// XPC has poked us and said we should do some cleanup!
// For now, poke the views and tell them to update their device states if they'd like
NSArray* actualViews = nil;
@synchronized(self.views) {
// Can't safely iterate a mutable collection, so copy it.
actualViews = self.views.allValues;
}
secnotice("ckks", "Received a 24hr notification from XPC");
CKOperationGroup* group = [CKOperationGroup CKKSGroupWithName:@"periodic-device-state-update"];
for(CKKSKeychainView* view in actualViews) {
ckksnotice("ckks", view, "Starting device state XPC update");
// Let the update know it should rate-limit itself
[view updateDeviceState:true waitForKeyHierarchyInitialization:30*NSEC_PER_SEC ckoperationGroup:group];
}
}
#pragma mark - CKKSPeerProvider implementation
- (CKKSSelves*)fetchSelfPeers:(NSError* __autoreleasing *)error {
__block SFECKeyPair* signingPrivateKey = nil;
__block SFECKeyPair* encryptionPrivateKey = nil;
__block NSError* localerror = nil;
// Wait for this to initialize, but don't worry if it isn't.
[self.accountTracker.accountCirclePeerIDInitialized wait:500*NSEC_PER_MSEC];
NSString* peerID = self.accountTracker.accountCirclePeerID;
if(!peerID || self.accountTracker.accountCirclePeerIDError) {
secerror("ckkspeer: Error fetching self peer : if(error) {
*error = self.accountTracker.accountCirclePeerIDError;
}
return nil;
}
SOSCCPerformWithOctagonSigningKey(^(SecKeyRef signingSecKey, CFErrorRef cferror) {
if(cferror) {
localerror = (__bridge NSError*)cferror;
return;
}
if (!cferror && signingSecKey) {
signingPrivateKey = [[SFECKeyPair alloc] initWithSecKey:signingSecKey];
}
});
SOSCCPerformWithOctagonEncryptionKey(^(SecKeyRef encryptionSecKey, CFErrorRef cferror) {
if(cferror) {
localerror = (__bridge NSError*)cferror;
return;
}
if (!cferror && encryptionSecKey) {
encryptionPrivateKey = [[SFECKeyPair alloc] initWithSecKey:encryptionSecKey];
}
});
if(localerror) {
secerror("ckkspeer: Error fetching self encryption keys: if(error) {
*error = localerror;
}
return nil;
}
CKKSSOSSelfPeer* selfPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:peerID
encryptionKey:encryptionPrivateKey
signingKey:signingPrivateKey];
CKKSSelves* selves = [[CKKSSelves alloc] initWithCurrent:selfPeer allSelves:nil];
return selves;
}
- (NSSet<id<CKKSPeer>>*)fetchTrustedPeers:(NSError* __autoreleasing *)error {
__block NSMutableSet<id<CKKSPeer>>* peerSet = [NSMutableSet set];
SOSCCPerformWithTrustedPeers(^(CFSetRef sosPeerInfoRefs, CFErrorRef cfTrustedPeersError) {
if(cfTrustedPeersError) {
secerror("ckks: Error fetching trusted peers: if(error) {
*error = (__bridge NSError*)cfTrustedPeersError;
}
}
CFSetForEach(sosPeerInfoRefs, ^(const void* voidPeer) {
CFErrorRef cfPeerError = NULL;
SOSPeerInfoRef sosPeerInfoRef = (SOSPeerInfoRef)voidPeer;
if(!sosPeerInfoRef) {
return;
}
CFStringRef cfpeerID = SOSPeerInfoGetPeerID(sosPeerInfoRef);
SecKeyRef cfOctagonSigningKey = SOSPeerInfoCopyOctagonSigningPublicKey(sosPeerInfoRef, &cfPeerError);
SecKeyRef cfOctagonEncryptionKey = SOSPeerInfoCopyOctagonEncryptionPublicKey(sosPeerInfoRef, &cfPeerError);
if(cfPeerError) {
secerror("ckkspeer: error fetching octagon keys for peer: } else {
SFECPublicKey* signingPublicKey = cfOctagonSigningKey ? [[SFECPublicKey alloc] initWithSecKey:cfOctagonSigningKey] : nil;
SFECPublicKey* encryptionPublicKey = cfOctagonEncryptionKey ? [[SFECPublicKey alloc] initWithSecKey:cfOctagonEncryptionKey] : nil;
CKKSSOSPeer* peer = [[CKKSSOSPeer alloc] initWithSOSPeerID:(__bridge NSString*)cfpeerID
encryptionPublicKey:encryptionPublicKey
signingPublicKey:signingPublicKey];
[peerSet addObject:peer];
}
CFReleaseNull(cfOctagonSigningKey);
CFReleaseNull(cfOctagonEncryptionKey);
});
});
return peerSet;
}
- (void)registerForPeerChangeUpdates:(id<CKKSPeerUpdateListener>)listener {
bool alreadyRegisteredListener = false;
NSEnumerator *enumerator = [self.peerChangeListeners objectEnumerator];
id<CKKSPeerUpdateListener> value;
while ((value = [enumerator nextObject])) {
// do pointer comparison
alreadyRegisteredListener |= (value == listener);
}
if(listener && !alreadyRegisteredListener) {
NSString* queueName = [NSString stringWithFormat: @"ck-peer-change-
dispatch_queue_t objQueue = dispatch_queue_create([queueName UTF8String], DISPATCH_QUEUE_SERIAL);
[self.peerChangeListeners setObject: listener forKey: objQueue];
}
}
- (void)iteratePeerListenersOnTheirQueue:(void (^)(id<CKKSPeerUpdateListener>))block {
NSEnumerator *enumerator = [self.peerChangeListeners keyEnumerator];
dispatch_queue_t dq;
// Queue up the changes for each listener.
while ((dq = [enumerator nextObject])) {
id<CKKSPeerUpdateListener> listener = [self.peerChangeListeners objectForKey: dq];
__weak id<CKKSPeerUpdateListener> weakListener = listener;
if(listener) {
dispatch_async(dq, ^{
__strong id<CKKSPeerUpdateListener> strongListener = weakListener;
block(strongListener);
});
}
}
}
- (void)sendSelfPeerChangedUpdate {
[self iteratePeerListenersOnTheirQueue: ^(id<CKKSPeerUpdateListener> listener) {
[listener selfPeerChanged];
}];
}
- (void)sendTrustedPeerSetChangedUpdate {
[self iteratePeerListenersOnTheirQueue: ^(id<CKKSPeerUpdateListener> listener) {
[listener trustedPeerSetChanged];
}];
}
#endif // OCTAGON
@end