OTManager.m   [plain text]


/*
 * 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 "SecEntitlements.h"
#import <Foundation/NSXPCConnection.h>
#import <Foundation/NSXPCConnection_Private.h>


#if OCTAGON
#import "keychain/ot/OTControlProtocol.h"
#import "keychain/ot/OTControl.h"
#import "keychain/ot/OTContext.h"
#import "keychain/ot/OTManager.h"
#import "keychain/ot/OTDefines.h"
#import "keychain/ot/OTRamping.h"
#import "keychain/ot/SFPublicKey+SPKI.h"
#import "keychain/ot/OT.h"
#import "keychain/ot/OTConstants.h"

#import "keychain/categories/NSError+UsefulConstructors.h"
#import "keychain/ckks/CKKSAnalytics.h"
#import "keychain/ckks/CKKSNearFutureScheduler.h"
#import "keychain/ckks/CKKS.h"
#import "keychain/ckks/CKKSViewManager.h"
#import "keychain/ckks/CKKSLockStateTracker.h"

#import <CloudKit/CloudKit.h>
#import <CloudKit/CloudKit_Private.h>

#import <SecurityFoundation/SFKey.h>
#import <SecurityFoundation/SFKey_Private.h>

#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR
#import <Accounts/Accounts.h>
#import <Accounts/ACAccountStore_Private.h>
#import <Accounts/ACAccountType_Private.h>
#import <Accounts/ACAccountStore.h>
#import <AppleAccount/ACAccountStore+AppleAccount.h>
#import <AppleAccount/ACAccount+AppleAccount.h>
#else
#import <Accounts/Accounts.h>
#import <AOSAccounts/MobileMePrefsCoreAEPrivate.h>
#import <AOSAccounts/MobileMePrefsCore.h>
#import <AOSAccounts/ACAccountStore+iCloudAccount.h>
#import <AOSAccounts/ACAccount+iCloudAccount.h>
#import <AOSAccounts/iCloudAccount.h>
#endif

#import <Security/SecureObjectSync/SOSAccountTransaction.h>
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#import <Security/SecureObjectSync/SOSAccount.h>
#pragma clang diagnostic pop

static NSString* const kOTRampForEnrollmentRecordName = @"metadata_rampstate_enroll";
static NSString* const kOTRampForRestoreRecordName = @"metadata_rampstate_restore";
static NSString* const kOTRampForCFURecordName = @"metadata_rampstate_cfu";
static NSString* const kOTRampZoneName = @"metadata_zone";
#define NUM_NSECS_IN_24_HRS (86400 * NSEC_PER_SEC)

@interface OTManager () <NSXPCListenerDelegate, OTContextIdentityProvider>
@property NSXPCListener *listener;
@property (nonatomic, strong) OTContext* context;
@property (nonatomic, strong) OTLocalStore *localStore;
@property (nonatomic, strong) OTRamp *enrollRamp;
@property (nonatomic, strong) OTRamp *restoreRamp;
@property (nonatomic, strong) OTRamp *cfuRamp;
@property (nonatomic, strong) CKKSNearFutureScheduler *cfuScheduler;
@property (nonatomic, strong) NSDate *lastPostedCoreFollowUp;
@end

@implementation OTManager

-(instancetype)init
{
    OTLocalStore* localStore = nil;
    OTContext* context = nil;

    NSString* dsid = [self askAccountsForDSID];
    if(dsid){
        localStore = [[OTLocalStore alloc]initWithContextID:OTDefaultContext dsid:dsid path:nil error:nil];
        context = [[OTContext alloc]initWithContextID:OTDefaultContext dsid:dsid localStore:self.localStore cloudStore:nil identityProvider:self error:nil];
    }
    //initialize our scheduler
    CKKSNearFutureScheduler *cfuScheduler = [[CKKSNearFutureScheduler alloc] initWithName:@"scheduling-cfu" initialDelay:NUM_NSECS_IN_24_HRS  continuingDelay:NUM_NSECS_IN_24_HRS keepProcessAlive:true dependencyDescriptionCode:CKKSResultDescriptionNone block:^{
        secnotice("octagon", "running scheduled cfu block");
        NSError* error = nil;
        [self scheduledCloudKitRampCheck:&error];
    }];
    
    //initialize our ramp objects
    [self initRamps];
    
    return [self initWithContext:context
                      localStore:localStore
                          enroll:self.enrollRamp
                         restore:self.restoreRamp
                             cfu:self.cfuRamp
                    cfuScheduler:cfuScheduler];
}

-(instancetype) initWithContext:(OTContext*)context
                     localStore:(OTLocalStore*)localStore
                         enroll:(OTRamp*)enroll
                        restore:(OTRamp*)restore
                            cfu:(OTRamp*)cfu
                   cfuScheduler:(CKKSNearFutureScheduler*)cfuScheduler
{
    self = [super init];
    if(self){
        self.context = context;
        self.localStore = localStore;
        self.cfuRamp = cfu;
        self.enrollRamp = enroll;
        self.restoreRamp = restore;
        self.cfuScheduler = cfuScheduler;
        
        secnotice("octagon", "otmanager init");
    }
    return self;
}

-(NSString*) askAccountsForDSID
{
    NSString *dsid = nil;
    ACAccountStore *accountStore = [[ACAccountStore alloc] init];

#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR
    ACAccount *account = [accountStore aa_primaryAppleAccount];
    dsid = [account aa_personID];
#else
    ACAccount *primaryiCloudAccount = nil;
    if ([accountStore respondsToSelector:@selector(icaPrimaryAppleAccount)]){
        primaryiCloudAccount = [accountStore icaPrimaryAppleAccount];
    }
    dsid =  [primaryiCloudAccount icaPersonID];
#endif
    return dsid;
}

+ (instancetype _Nullable)manager {
    static OTManager* manager = nil;

    if(!SecOTIsEnabled()) {
        secerror("octagon: Attempt to fetch a manager while Octagon is disabled");
        return nil;
    }
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [[OTManager alloc]init];
    });

    return manager;
}


-(BOOL) initRamps
{
    BOOL initResult = NO;

    CKContainer* container = [CKKSViewManager manager].container;
    CKDatabase* database = [container privateCloudDatabase];
    CKRecordZoneID* zoneID = [[CKRecordZoneID alloc] initWithZoneName:kOTRampZoneName ownerName:CKCurrentUserDefaultName];

    CKKSCKAccountStateTracker *accountTracker = [CKKSViewManager manager].accountTracker;
    CKKSReachabilityTracker *reachabilityTracker = [CKKSViewManager manager].reachabilityTracker;
    CKKSLockStateTracker *lockStateTracker = [CKKSViewManager manager].lockStateTracker;

    self.cfuRamp = [[OTRamp alloc]initWithRecordName:kOTRampForCFURecordName
                                         featureName:@"cfu"
                                           container:container
                                            database:database
                                              zoneID:zoneID
                                      accountTracker:accountTracker
                                    lockStateTracker:lockStateTracker
                                 reachabilityTracker:reachabilityTracker
                    fetchRecordRecordsOperationClass:[CKFetchRecordsOperation class]];

    self.enrollRamp = [[OTRamp alloc]initWithRecordName:kOTRampForEnrollmentRecordName
                                            featureName:@"enroll"
                                              container:container
                                               database:database
                                                 zoneID:zoneID
                                         accountTracker:accountTracker
                                       lockStateTracker:lockStateTracker
                                    reachabilityTracker:reachabilityTracker
                       fetchRecordRecordsOperationClass:[CKFetchRecordsOperation class]];


    self.restoreRamp = [[OTRamp alloc]initWithRecordName:kOTRampForRestoreRecordName
                                             featureName:@"restore"
                                               container:container
                                                database:database
                                                  zoneID:zoneID
                                          accountTracker:accountTracker
                                        lockStateTracker:lockStateTracker
                                     reachabilityTracker:reachabilityTracker
                        fetchRecordRecordsOperationClass:[CKFetchRecordsOperation class]];

    if(self.cfuRamp && self.enrollRamp && self.restoreRamp){
        initResult = YES;
    }
    return initResult;
}

-(BOOL) initializeManagerPropertiesForContext:(NSString*)dsid error:(NSError**)error
{
    CKKSAnalytics* logger = [CKKSAnalytics logger];
    NSError *localError = nil;
    BOOL initialized = YES;

    if(dsid == nil){
        dsid = [self askAccountsForDSID];
    }

    //create local store
    self.localStore = [[OTLocalStore alloc] initWithContextID:OTDefaultContext dsid:dsid path:nil error:&localError];
    if(!self.localStore){
        secerror("octagon: could not create localStore: %@", localError);
        [logger logUnrecoverableError:localError forEvent:OctagonEventSignIn withAttributes:@{
                                                                                         OctagonEventAttributeFailureReason : @"creating local store",
                                                                                         }];
        initialized = NO;
    }
    
    //create context
    self.context = [[OTContext alloc]initWithContextID:OTDefaultContext dsid:dsid localStore:self.localStore cloudStore:nil identityProvider:self error:&localError];
    if(!self.context){
        secerror("octagon: could not create context: %@", localError);
        [logger logUnrecoverableError:localError forEvent:OctagonEventSignIn withAttributes:@{
                                                                                         OctagonEventAttributeFailureReason : @"creating context",
                                                                                         }];
        self.localStore = nil;
        initialized = NO;
    }

    //just in case, init the ramp objects
    [self initRamps];

    if(localError && error){
        *error = localError;
    }
    return initialized;
}

/*
 *       SPI routines
 */

- (void)signIn:(NSString*)dsid reply:(void (^)(BOOL result, NSError * _Nullable signedInError))reply
{
    CKKSAnalytics* logger = [CKKSAnalytics logger];
    SFAnalyticsActivityTracker *tracker = [logger logSystemMetricsForActivityNamed:CKKSActivityOctagonSignIn withAction:nil];
    [tracker start];

    NSError *error = nil;
    if(![self initializeManagerPropertiesForContext:dsid error:&error]){
        [tracker cancel];
        reply(NO, error);
        return;
    }

    [tracker stop];
    [logger logSuccessForEventNamed:OctagonEventSignIn];
    
    secnotice("octagon","created context and local store on manager for:%@", dsid);
    
    reply(YES, error);
}

- (void)signOut:(void (^)(BOOL result, NSError * _Nullable signedOutError))reply
{
    CKKSAnalytics* logger = [CKKSAnalytics logger];
    
    NSError* error = nil;
    NSError *bottledPeerError = nil;
    NSError *localContextError = nil;
    
    secnotice("octagon", "signing out of octagon trust: dsid: %@ contextID: %@",
              self.context.dsid,
              self.context.contextID);
    
    NSString* contextAndDSID = [NSString stringWithFormat:@"%@-%@", self.context.contextID, self.context.dsid];
    
    //remove all locally stored context
    BOOL result1 = [self.localStore deleteLocalContext:contextAndDSID error:&localContextError];
    if(!result1){
        secerror("octagon: could not delete local context: %@: %@", self.context.contextID, localContextError);
        [logger logUnrecoverableError:localContextError forEvent:OctagonEventSignOut withAttributes:@{
                                                                                                     OctagonEventAttributeFailureReason : @"deleting local context",
                                                                                                     }];
        error = localContextError;
    }
    
    BOOL result2 = [self.localStore deleteBottledPeersForContextAndDSID:contextAndDSID error:&bottledPeerError];
    if(!result2){
        secerror("octagon: could not delete bottle peer records: %@: %@", self.context.contextID, bottledPeerError);
        [logger logUnrecoverableError:bottledPeerError forEvent:OctagonEventSignOut withAttributes:@{
                                                                                                    OctagonEventAttributeFailureReason : @"deleting local bottled peers",
                                                                                                    }];
        error = bottledPeerError;
    }
    
    //free context & local store
    self.context = nil;
    self.localStore = nil;
    
    BOOL result = (result1 && result2);
    if (result) {
        [logger logSuccessForEventNamed:OctagonEventSignOut];
    }
    
    reply(result, error);
}
- (void)preflightBottledPeer:(NSString*)contextID
                        dsid:(NSString*)dsid
                       reply:(void (^)(NSData* _Nullable entropy,
                                       NSString* _Nullable bottleID,
                                       NSData* _Nullable signingPublicKey,
                                       NSError* _Nullable error))reply
{
    secnotice("octagon", "preflightBottledPeer: %@ %@", contextID, dsid);
    NSError* error = nil;
    CKKSAnalytics* logger = [CKKSAnalytics logger];
    SFAnalyticsActivityTracker *tracker = [logger logSystemMetricsForActivityNamed:CKKSActivityOctagonPreflightBottle withAction:nil];

    [tracker start];

    if(!self.context || !self.localStore){
        if(![self initializeManagerPropertiesForContext:dsid error:&error]){
            secerror("octagon:  could not init manager obejcts: %@", error);
            reply(nil,nil,nil,error);
            [tracker cancel];
            return;
        }
    }

    NSInteger retryDelayInSeconds = 0;
    BOOL isFeatureOn = [self.enrollRamp checkRampState:&retryDelayInSeconds networkBehavior:CKOperationDiscretionaryNetworkBehaviorNonDiscretionary error:&error];

    //got an error from ramp check, we should log it
    if(error){
        [logger logRecoverableError:error
                           forEvent:OctagonEventRamp
                           zoneName:kOTRampZoneName
                     withAttributes:@{
                                      OctagonEventAttributeFailureReason : @"ramp check for preflight bottle"
                                      }];
    }

    if(!isFeatureOn){           //cloud kit has not asked us to come back and the feature is off for this device
        secnotice("octagon", "bottled peers is not on");
        if(!error){
            error = [NSError errorWithDomain:octagonErrorDomain code:OTErrorFeatureNotEnabled userInfo:@{NSLocalizedDescriptionKey: @"Feature not enabled"}];
        }
        reply(nil, nil, nil, error);
        return;
    }
    
    NSData* entropy = [self.context makeMeSomeEntropy:OTMasterSecretLength];
    if(!entropy){
        secerror("octagon: entropy creation failed: %@", error);
        error = [NSError errorWithDomain:octagonErrorDomain code:OTErrorEntropyCreationFailure userInfo:@{NSLocalizedDescriptionKey: @"Failed to create entropy"}];
        [logger logUnrecoverableError:error forEvent:OctagonEventPreflightBottle withAttributes:@{
                                                                                                  OctagonEventAttributeFailureReason : @"preflight bottle, entropy failure"}
         ];
        [tracker stop];
        reply(nil, nil, nil, error);
        return;
    }
    
    OTPreflightInfo* result = [self.context preflightBottledPeer:contextID entropy:entropy error:&error];
    if(!result || error){
        secerror("octagon: preflight failed: %@", error);
        [logger logUnrecoverableError:error forEvent:OctagonEventPreflightBottle withAttributes:@{ OctagonEventAttributeFailureReason : @"preflight bottle"}];
        reply(nil, nil, nil, error);
        [tracker stop];
        return;
    }
    
    [tracker stop];
    [logger logSuccessForEventNamed:OctagonEventPreflightBottle];
    
    secnotice("octagon", "preflightBottledPeer completed, created: %@", result.bottleID);
    
    reply(entropy, result.bottleID, result.escrowedSigningSPKI, error);
}

- (void)launchBottledPeer:(NSString*)contextID
                 bottleID:(NSString*)bottleID
                    reply:(void (^ _Nullable)(NSError* _Nullable error))reply
{
    secnotice("octagon", "launchBottledPeer");
    NSError* error = nil;
    CKKSAnalytics* logger = [CKKSAnalytics logger];
    SFAnalyticsActivityTracker *tracker = [logger logSystemMetricsForActivityNamed:CKKSActivityOctagonLaunchBottle withAction:nil];

    [tracker start];
    
    if(!self.context || !self.localStore){
        if(![self initializeManagerPropertiesForContext:nil error:&error]){
            [tracker cancel];
            reply(error);
            return;
        }
    }
    
    NSInteger retryDelayInSeconds = 0;
    BOOL isFeatureOn = [self.enrollRamp checkRampState:&retryDelayInSeconds networkBehavior:CKOperationDiscretionaryNetworkBehaviorNonDiscretionary error:&error];

    //got an error from ramp check, we should log it
    if(error){
        [logger logRecoverableError:error
                           forEvent:OctagonEventRamp
                           zoneName:kOTRampZoneName
                     withAttributes:@{
                                      OctagonEventAttributeFailureReason : @"ramp state check for launch bottle"
                                      }];
    }

    if(!isFeatureOn){
        secnotice("octagon", "bottled peers is not on");
        if(!error){
            error = [NSError errorWithDomain:octagonErrorDomain code:OTErrorFeatureNotEnabled userInfo:@{NSLocalizedDescriptionKey: @"Feature not enabled"}];
        }
        reply(error);
        return;
    }

    OTBottledPeerRecord* bprecord = [self.localStore readLocalBottledPeerRecordWithRecordID:bottleID error:&error];
    if(!bprecord || error){
        secerror("octagon: could not retrieve record for: %@, error: %@", bottleID, error);
        [logger logUnrecoverableError:error forEvent:OctagonEventLaunchBottle withAttributes:@{
                                                                                               OctagonEventAttributeFailureReason : @"reading bottle from local store"
                                                                                               }];
        [tracker stop];
        reply(error);
        return;
    }
    BOOL result = [self.context.cloudStore uploadBottledPeerRecord:bprecord escrowRecordID:bprecord.escrowRecordID error:&error];
    if(!result || error){
        secerror("octagon: could not upload record for bottleID %@, error: %@", bottleID, error);
        [logger logUnrecoverableError:error forEvent:OctagonEventLaunchBottle withAttributes:@{
                                                                                               OctagonEventAttributeFailureReason : @"upload bottle to cloud kit"
                                                                                               }];
        [tracker stop];
        reply(error);
        return;
    }
    
    [tracker stop];
    [logger logSuccessForEventNamed:OctagonEventLaunchBottle];
    
    secnotice("octagon", "successfully launched: %@", bprecord.recordName);
    
    reply(error);
}

- (void)restore:(NSString *)contextID dsid:(NSString *)dsid secret:(NSData*)secret escrowRecordID:(NSString*)escrowRecordID reply:(void (^)(NSData* signingKeyData, NSData* encryptionKeyData, NSError *))reply
{
    //check if configuration zone allows restore
    NSError* error = nil;
    CKKSAnalytics* logger = [CKKSAnalytics logger];
    SFAnalyticsActivityTracker *tracker = [logger logSystemMetricsForActivityNamed:CKKSActivityOctagonRestore withAction:nil];

    [tracker start];

    if(!self.context || !self.localStore){
        if(![self initializeManagerPropertiesForContext:dsid error:&error]){
            secerror("octagon:  could not init manager obejcts: %@", error);
            reply(nil,nil,error);
            [tracker cancel];
            return;
        }
    }

    NSInteger retryDelayInSeconds = 0;
    BOOL isFeatureOn = [self.restoreRamp checkRampState:&retryDelayInSeconds networkBehavior:CKOperationDiscretionaryNetworkBehaviorNonDiscretionary error:&error];

    //got an error from ramp check, we should log it
    if(error){
        [logger logRecoverableError:error
                           forEvent:OctagonEventRamp
                           zoneName:kOTRampZoneName
                     withAttributes:@{
                                      OctagonEventAttributeFailureReason : @"checking ramp state for restore"
                                      }];
    }

    if(!isFeatureOn){
        secnotice("octagon", "bottled peers is not on");
        if(!error){
            error = [NSError errorWithDomain:octagonErrorDomain code:OTErrorFeatureNotEnabled userInfo:@{NSLocalizedDescriptionKey: @"Feature not enabled"}];
        }
        [tracker stop];
        reply(nil, nil, error);
        return;
    }

    if(!escrowRecordID || [escrowRecordID length] == 0){
        secerror("octagon: missing escrowRecordID");
        error = [NSError errorWithDomain:octagonErrorDomain code:OTErrorEmptyEscrowRecordID userInfo:@{NSLocalizedDescriptionKey: @"Escrow Record ID is empty or missing"}];
        
        [logger logUnrecoverableError:error forEvent:OctagonEventRestoreBottle withAttributes:@{
                                                                                                OctagonEventAttributeFailureReason : @"escrow record id missing",
                                                                                                }];
        
        [tracker stop];
        reply(nil, nil, error);
        return;
    }
    if(!dsid || [dsid length] == 0){
        secerror("octagon: missing dsid");
        error = [NSError errorWithDomain:octagonErrorDomain code:OTErrorEmptyDSID userInfo:@{NSLocalizedDescriptionKey: @"DSID is empty or missing"}];
        
        [logger logUnrecoverableError:error forEvent:OctagonEventRestoreBottle withAttributes:@{
                                                                                                OctagonEventAttributeFailureReason : @"dsid missing",
                                                                                                }];
        [tracker stop];
        reply(nil, nil, error);
        return;
    }
    if(!secret || [secret length] == 0){
        secerror("octagon: missing secret");
        error = [NSError errorWithDomain:octagonErrorDomain code:OTErrorEmptySecret userInfo:@{NSLocalizedDescriptionKey: @"Secret is empty or missing"}];
        
        
        [logger logUnrecoverableError:error forEvent:OctagonEventRestoreBottle withAttributes:@{
                                                                                                OctagonEventAttributeFailureReason : @"secret missing",
                                                                                                }];
        
        [tracker stop];
        reply(nil, nil, error);
        return;
    }

    OTBottledPeerSigned *bps = [_context restoreFromEscrowRecordID:escrowRecordID secret:secret error:&error];
    if(!bps || error != nil){
        secerror("octagon: failed to restore bottled peer: %@", error);
        
        [logger logUnrecoverableError:error forEvent:OctagonEventRestoreBottle withAttributes:@{
                                                                                                OctagonEventAttributeFailureReason : @"restore failed",
                                                                                                }];
        [tracker stop];
        reply(nil, nil, error);
        return;
    }
    
    NSData *encryptionKeyData = bps.bp.peerEncryptionKey.publicKey.keyData;  // FIXME
    if(!encryptionKeyData){
        secerror("octagon: restored octagon encryption key is nil: %@", error);
        error = [NSError errorWithDomain:octagonErrorDomain code:OTErrorRestoredPeerEncryptionKeyFailure userInfo:@{NSLocalizedDescriptionKey: @"Failed to retrieve restored Octagon Peer Encryption Key"}];
        
        [logger logUnrecoverableError:error forEvent:OctagonEventRestoreBottle withAttributes:@{
                                                                                                OctagonEventAttributeFailureReason : @"restored octagon encryption key"
                                                                                                }];
        [tracker stop];
        reply(nil,nil,error);
        return;
    }
    
    NSData *signingKeyData = bps.bp.peerSigningKey.publicKey.keyData;  // FIXME
    if(!signingKeyData){
        secerror("octagon: restored octagon signing key is nil: %@", error);
        error = [NSError errorWithDomain:octagonErrorDomain code:OTErrorRestoredPeerSigningKeyFailure userInfo:@{NSLocalizedDescriptionKey: @"Failed to retrieve restored Octagon Peer Signing Key"}];
        
        [logger logUnrecoverableError:error forEvent:OctagonEventRestoreBottle withAttributes:@{
                                                                                                OctagonEventAttributeFailureReason : @"restored octagon signing key"
                                                                                                }];
        [tracker stop];
        reply(nil,nil,error);
        return;
    }
    [tracker stop];
    
    [logger logSuccessForEventNamed:OctagonEventRestoreBottle];
    
    secnotice("octagon", "restored bottled peer: %@", escrowRecordID);
    
    reply(signingKeyData, encryptionKeyData, error);
}

- (void)scrubBottledPeer:(NSString*)contextID
                bottleID:(NSString*)bottleID
                   reply:(void (^ _Nullable)(NSError* _Nullable error))reply
{
    NSError* error = nil;

    CKKSAnalytics* logger = [CKKSAnalytics logger];
    SFAnalyticsActivityTracker *tracker = [logger logSystemMetricsForActivityNamed:CKKSActivityScrubBottle withAction:nil];
  
    if(!self.context || !self.localStore){
        if(![self initializeManagerPropertiesForContext:nil error:&error]){
            [tracker cancel];
            reply(error);
            return;
        }
    }
    [tracker start];

    NSInteger retryDelayInSeconds = 0;
    BOOL isFeatureOn = [self.enrollRamp checkRampState:&retryDelayInSeconds networkBehavior:CKOperationDiscretionaryNetworkBehaviorNonDiscretionary error:&error];

    //got an error from ramp check, we should log it
    if(error){
        [logger logRecoverableError:error
                           forEvent:OctagonEventRamp
                           zoneName:kOTRampZoneName
                     withAttributes:@{
                                      OctagonEventAttributeFailureReason : @"ramp check for scrubbing bottled peer"
                                      }];
    }

    if(!isFeatureOn){
        secnotice("octagon", "bottled peers is not on");
        if(!error){
            error = [NSError errorWithDomain:octagonErrorDomain code:OTErrorFeatureNotEnabled userInfo:@{NSLocalizedDescriptionKey: @"Feature not enabled"}];
        }
        [tracker stop];
        reply(error);
        return;
    }
    
    BOOL result = [self.context scrubBottledPeer:contextID bottleID:bottleID error:&error];
    if(!result || error){
        secerror("octagon: could not scrub record for bottleID %@, error: %@", bottleID, error);
        [logger logUnrecoverableError:error forEvent:OctagonEventScrubBottle withAttributes:@{
                                                                                              OctagonEventAttributeFailureReason : @"could not scrub bottle",
                                                                                              }];
        [tracker stop];
        reply(error);
        return;
    }
    [logger logSuccessForEventNamed:OctagonEventScrubBottle];
    
    secnotice("octagon", "scrubbed bottled peer: %@", bottleID);
    
    reply(error);
}

/*
 *  OTCTL tool routines
 */

-(void) reset:(void (^)(BOOL result, NSError *))reply
{
    NSError* error = nil;

    if(!self.context || !self.localStore){
        if(![self initializeManagerPropertiesForContext:nil error:&error]){
            secerror("octagon:  could not init manager obejcts: %@", error);
            reply(NO,error);
            return;
        }
    }

    if(self.context.lockStateTracker.isLocked){
        secnotice("octagon","device is locked! can't check ramp state");
        error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain
                                    code:errSecInteractionNotAllowed
                                userInfo:@{NSLocalizedDescriptionKey: @"device is locked"}];

        reply(NO,error);
        return;
    }
    if(self.context.accountTracker.currentCKAccountInfo.accountStatus != CKAccountStatusAvailable){
        secnotice("octagon","not signed in! can't check ramp state");
        error = [NSError errorWithDomain:octagonErrorDomain
                                    code:OTErrorNotSignedIn
                                userInfo:@{NSLocalizedDescriptionKey: @"not signed in"}];
        reply(NO,error);
        return;

    }
    if(!self.context.reachabilityTracker.currentReachability){
        secnotice("octagon","no network! can't check ramp state");
        error = [NSError errorWithDomain:octagonErrorDomain
                                    code:OTErrorNoNetwork
                                userInfo:@{NSLocalizedDescriptionKey: @"no network"}];
        reply(NO,error);
        return;
    }

    NSError* bottledPeerError = nil;

    BOOL result = [_context.cloudStore performReset:&bottledPeerError];
    if(!result || bottledPeerError != nil){
        secerror("octagon: resetting octagon trust zone failed: %@", bottledPeerError);
    }
    
    NSString* contextAndDSID = [NSString stringWithFormat:@"%@-%@", self.context.contextID, self.context.dsid];

    result = [self.localStore deleteBottledPeersForContextAndDSID:contextAndDSID error:&bottledPeerError];
    if(!result){
        secerror("octagon: could not delete bottle peer records: %@: %@", self.context.contextID, bottledPeerError);
    }
    
    reply(result, bottledPeerError);
}

- (void)listOfEligibleBottledPeerRecords:(void (^)(NSArray* listOfRecords, NSError *))reply
{
    NSError* error = nil;
    if(!self.context || !self.localStore){
        if(![self initializeManagerPropertiesForContext:nil error:&error]){
            secerror("octagon:  could not init manager obejcts: %@", error);
            reply(nil,error);
            return;
        }
    }

    if(self.context.lockStateTracker.isLocked){
        secnotice("octagon","device is locked! can't check ramp state");
        error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain
                                         code:errSecInteractionNotAllowed
                                     userInfo:@{NSLocalizedDescriptionKey: @"device is locked"}];

        reply(nil,error);
        return;
    }
    if(self.context.accountTracker.currentCKAccountInfo.accountStatus != CKAccountStatusAvailable){
        secnotice("octagon","not signed in! can't check ramp state");
        error = [NSError errorWithDomain:octagonErrorDomain
                                         code:OTErrorNotSignedIn
                                     userInfo:@{NSLocalizedDescriptionKey: @"not signed in"}];
        reply(nil,error);
        return;
    }
    if(!self.context.reachabilityTracker.currentReachability){
        secnotice("octagon","no network! can't check ramp state");
        error = [NSError errorWithDomain:octagonErrorDomain
                                         code:OTErrorNoNetwork
                                     userInfo:@{NSLocalizedDescriptionKey: @"no network"}];
        reply(nil,error);
        return;
    }

    NSArray* list = [_context.cloudStore retrieveListOfEligibleEscrowRecordIDs:&error];
    if(!list || error !=nil){
        secerror("octagon: there are not eligible bottle peer records: %@", error);
        reply(nil,error);
        return;
    }
    reply(list, error);
}

- (void)octagonEncryptionPublicKey:(void (^)(NSData* encryptionKey, NSError *))reply
{
    __block NSData *encryptionKey = NULL;
    __block NSError* localError = nil;

    SOSCCPerformWithOctagonEncryptionPublicKey(^(SecKeyRef octagonPrivKey, CFErrorRef error) {
        CFDataRef key;
        SecKeyCopyPublicBytes(octagonPrivKey, &key);
        encryptionKey = CFBridgingRelease(key);
        if(error){
            localError = (__bridge NSError*)error;
        }
    });
    if(!encryptionKey || localError != nil){
        reply(nil, localError);
        secerror("octagon: retrieving the octagon encryption public key failed: %@", localError);
        return;
    }
    reply(encryptionKey, localError);
}

-(void)octagonSigningPublicKey:(void (^)(NSData* encryptionKey, NSError *))reply
{
    __block NSData *signingKey = NULL;
    __block NSError* localError = nil;

    SOSCCPerformWithOctagonSigningPublicKey(^(SecKeyRef octagonPrivKey, CFErrorRef error) {
        CFDataRef key;
        SecKeyCopyPublicBytes(octagonPrivKey, &key);
        signingKey = CFBridgingRelease(key);
        if(error){
            localError = (__bridge NSError*)error;
        }
    });
    if(!signingKey || localError != nil){
        reply(nil, localError);
        secerror("octagon: retrieving the octagon signing public key failed: %@", localError);
        return;
    }
    reply(signingKey, localError);
}

/*
 *  OT Helpers
 */

-(BOOL)scheduledCloudKitRampCheck:(NSError**)error
{
    secnotice("octagon", "scheduling a CloudKit ramping check");
    NSInteger retryAfterInSeconds = 0;
    NSError* localError = nil;
    BOOL cancelScheduler = YES;

    CKKSAnalytics* logger = [CKKSAnalytics logger];

    if(self.cfuRamp){
        BOOL canCFU = [self.cfuRamp checkRampState:&retryAfterInSeconds networkBehavior:CKOperationDiscretionaryNetworkBehaviorNonDiscretionary error:&localError];

        if(localError){
            secerror("octagon: checking ramp state for CFU error'd: %@", localError);
            [logger logUnrecoverableError:localError forEvent:OctagonEventRamp withAttributes:@{
                                                                                                OctagonEventAttributeFailureReason : @"ramp check failed",
                                                                                                }];
        }

        if(canCFU){
            secnotice("octagon", "CFU is enabled, checking if this device has a bottle");
            OctagonBottleCheckState bottleStatus = [self.context doesThisDeviceHaveABottle:&localError];

            if(bottleStatus == NOBOTTLE){
                //time to post a follow up!
                secnotice("octagon", "device does not have a bottle, posting a follow up");
                if(!SecCKKSTestsEnabled()){
                    //40347954 removing cfu invocations until we can add CDP without creating a cycle.
                    //[self.context postFollowUp];
                    secnotice("octagon", "would have posted a follow up");
                }
                NSInteger timeDiff = -1;

                NSDate *currentDate = [NSDate date];
                if(self.lastPostedCoreFollowUp){
                    timeDiff = [currentDate timeIntervalSinceDate:self.lastPostedCoreFollowUp];
                }

                //log how long we last posted a followup, if any
                [logger logRecoverableError:localError
                                   forEvent:OctagonEventCoreFollowUp
                                   zoneName:kOTRampZoneName
                             withAttributes:@{
                                              OctagonEventAttributeFailureReason : @"No bottle for peer",
                                              OctagonEventAttributeTimeSinceLastPostedFollowUp: [NSNumber numberWithInteger:timeDiff],
                                              }];

                self.lastPostedCoreFollowUp = currentDate;
                //if the followup failed or succeeded, we should continue the scheduler until we have a bottle.
                cancelScheduler = NO;
            }else if(bottleStatus == BOTTLE){
                secnotice("octagon", "device has a bottle");
                [logger logSuccessForEventNamed:OctagonEventBottleCheck];
            }

            if(localError){
                [logger logRecoverableError:localError
                                   forEvent:OctagonEventBottleCheck
                                   zoneName:kOTRampZoneName
                             withAttributes:@{
                                              OctagonEventAttributeFailureReason : @"bottle check",
                                              }];
            }
        }
    }
    if(cancelScheduler == NO){
        secnotice("octagon", "requesting bottle check again");
        [self.cfuScheduler trigger];
    }

    if(error && localError){
        *error = localError;
    }
    return cancelScheduler;
}

-(void)scheduleCFUForFuture
{
    secnotice("octagon", "scheduling a query to cloudkit to see if this device can post a core follow up");
    
    [self.cfuScheduler trigger];
}

- (nullable OTIdentity *) currentIdentity:(NSError**)error
{
    return [OTIdentity currentIdentityFromSOS:error];
}

@end

#endif