OctagonTrust.m   [plain text]


/*
* Copyright (c) 2020 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@
*/

#if __OBJC2__

#import <objc/runtime.h>

#import "OctagonTrust.h"
#import <Security/OTClique+Private.h>
#import <utilities/debugging.h>
#import "OTEscrowTranslation.h"

#import <SoftLinking/SoftLinking.h>
#import <CloudServices/SecureBackup.h>
#import <CloudServices/SecureBackupConstants.h>
#import "keychain/OctagonTrust/categories/OctagonTrustEscrowRecoverer.h"
#import "keychain/ot/OTDefines.h"
#import "keychain/ot/OTControl.h"
#import "keychain/ot/OTClique+Private.h"
#import <Security/OctagonSignPosts.h>
#include <utilities/SecCFRelease.h>

SOFT_LINK_FRAMEWORK(PrivateFrameworks, KeychainCircle);
SOFT_LINK_OPTIONAL_FRAMEWORK(PrivateFrameworks, CloudServices);

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wstrict-prototypes"
SOFT_LINK_CLASS(CloudServices, SecureBackup);
SOFT_LINK_CONSTANT(CloudServices, kSecureBackupErrorDomain, NSErrorDomain);
SOFT_LINK_CONSTANT(CloudServices, kSecureBackupiCDPRecordsKey, NSString*);
SOFT_LINK_CONSTANT(CloudServices, kSecureBackupMetadataKey, NSString*);
SOFT_LINK_CONSTANT(CloudServices, kSecureBackupRecordIDKey, NSString*);

SOFT_LINK_CONSTANT(CloudServices, kEscrowServiceRecordDataKey, NSString*);
SOFT_LINK_CONSTANT(CloudServices, kSecureBackupKeybagDigestKey, NSString*);
SOFT_LINK_CONSTANT(CloudServices, kSecureBackupBagPasswordKey, NSString*);
SOFT_LINK_CONSTANT(CloudServices, kSecureBackupRecordLabelKey, NSString*);

#pragma clang diagnostic pop

static NSString * const kOTEscrowAuthKey = @"kOTEscrowAuthKey";
NSString* OTCKContainerName = @"com.apple.security.keychain";

@implementation OTConfigurationContext(Framework)

@dynamic escrowAuth;

-(void) setEscrowAuth:(id)e
{
    objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kOTEscrowAuthKey), e, OBJC_ASSOCIATION_ASSIGN);

}
- (id) escrowAuth
{
    return objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kOTEscrowAuthKey));
}

@end

@implementation OTClique(Framework)

+ (NSArray<OTEscrowRecord*>*)filterViableSOSRecords:(NSArray<OTEscrowRecord*>*)nonViable
{
    NSMutableArray<OTEscrowRecord*>* viableSOS = [NSMutableArray array];
    for(OTEscrowRecord* record in nonViable) {
        if (record.viabilityStatus == OTEscrowRecord_SOSViability_SOS_VIABLE) {
            [viableSOS addObject:record];
        }
    }

    return viableSOS;
}

/*
 * desired outcome from filtering on an iOS/macOS platform:
 *              Escrow Record data validation outcome               Outcome of fetch records
                Octagon Viable, SOS Viable                          Record gets recommended, normal Octagon and SOS join
                Octagon Viable, SOS Doesn't work                    Record gets recommended, normal Octagon join, SOS reset
                Octagon Doesn't work, SOS Viable                    Record gets recommended, Octagon reset, normal SOS join
                Octagon Doesn't work, SOS Doesn't work              no records recommended.  Force user to reset all protected data

 * desired outcome from filtering on an watchOS/tvOS/etc:
 Escrow Record data validation outcome                           Outcome of fetch records
             Octagon Viable, SOS Viable                          Record gets recommended, normal Octagon and SOS noop
             Octagon Viable, SOS Doesn't work                    Record gets recommended, normal Octagon join, SOS noop
             Octagon Doesn't work, SOS Viable                    Record gets recommended, Octagon reset, SOS noop
             Octagon Doesn't work, SOS Doesn't work              no records recommended.  Force user to reset all protected data
*/

+ (NSArray<OTEscrowRecord*>* _Nullable)filterRecords:(NSArray<OTEscrowRecord*>*)allRecords
{
    NSMutableArray<OTEscrowRecord*>* viable = [NSMutableArray array];
    NSMutableArray<OTEscrowRecord*>* partial = [NSMutableArray array];
    NSMutableArray<OTEscrowRecord*>* nonViable = [NSMutableArray array];

    for (OTEscrowRecord* record in allRecords) {
        if(record.recordViability == OTEscrowRecord_RecordViability_RECORD_VIABILITY_FULLY_VIABLE &&
           record.escrowInformationMetadata.bottleId != nil &&
           [record.escrowInformationMetadata.bottleId length] != 0) {
            [viable addObject:record];
        } else if(record.recordViability == OTEscrowRecord_RecordViability_RECORD_VIABILITY_PARTIALLY_VIABLE &&
                  record.escrowInformationMetadata.bottleId != nil &&
                  [record.escrowInformationMetadata.bottleId length] != 0) {
            [partial addObject:record];
        } else {
            [nonViable addObject:record];
        }
    }

    for(OTEscrowRecord* record in viable) {
        secnotice("octagontrust-fetchescrowrecords", "viable record: %@ serial:%@ bottleID:%@ silent allowed:%d",
                  record.label,
                  record.escrowInformationMetadata.serial,
                  record.escrowInformationMetadata.bottleId,
                  (int)record.silentAttemptAllowed);
    }
    for(OTEscrowRecord* record in partial) {
        secnotice("octagontrust-fetchescrowrecords", "partially viable record: %@ serial:%@ bottleID:%@ silent allowed:%d",
                  record.label,
                  record.escrowInformationMetadata.serial,
                  record.escrowInformationMetadata.bottleId,
                  (int)record.silentAttemptAllowed);
    }
    for(OTEscrowRecord* record in nonViable) {
        secnotice("octagontrust-fetchescrowrecords", "nonviable record: %@ serial:%@ bottleID:%@ silent allowed:%d",
                  record.label,
                  record.escrowInformationMetadata.serial,
                  record.escrowInformationMetadata.bottleId,
                  (int)record.silentAttemptAllowed);
    }

    if ([viable count] > 0) {
        secnotice("octagontrust-fetchescrowrecords", "Returning %d viable records", (int)[viable count]);
        return viable;
    }

    if ([partial count] > 0) {
        secnotice("octagontrust-fetchescrowrecords", "Returning %d partially viable records", (int)[partial count]);
        return partial;
    }

    if (OctagonPlatformSupportsSOS()) {
        NSArray<OTEscrowRecord*>* viableSOSRecords = [self filterViableSOSRecords:nonViable];
        secnotice("octagontrust-fetchescrowrecords", "Returning %d sos viable records", (int)[viableSOSRecords count]);
        return viableSOSRecords;
    }

    secnotice("octagontrust-fetchescrowrecords", "no viable records!");

    return nil;
}

+ (NSArray<OTEscrowRecord*>* _Nullable)fetchAndHandleEscrowRecords:(OTConfigurationContext*)data shouldFilter:(BOOL)shouldFiler error:(NSError**)error
{
    OctagonSignpost signPost = OctagonSignpostBegin(OctagonSignpostNameFetchEscrowRecords);
    bool subTaskSuccess = false;

    NSError* localError = nil;
    NSArray* escrowRecordDatas = [OTClique fetchEscrowRecordsInternal:data error:&localError];
    if(localError) {
        secerror("octagontrust-fetchAndHandleEscrowRecords: failed to fetch escrow records: %@", localError);
        if(error){
            *error = localError;
        }
        OctagonSignpostEnd(signPost, OctagonSignpostNameFetchEscrowRecords, OctagonSignpostNumber1(OctagonSignpostNameFetchEscrowRecords), (int)subTaskSuccess);
        return nil;
    }

    NSMutableArray<OTEscrowRecord*>* escrowRecords = [NSMutableArray array];

    for(NSData* recordData in escrowRecordDatas){
        OTEscrowRecord* translated = [[OTEscrowRecord alloc] initWithData:recordData];
        if(translated.escrowInformationMetadata.bottleValidity == nil || [translated.escrowInformationMetadata.bottleValidity isEqualToString:@""]){
            switch(translated.recordViability) {
                case OTEscrowRecord_RecordViability_RECORD_VIABILITY_FULLY_VIABLE:
                    translated.escrowInformationMetadata.bottleValidity = @"valid";
                    break;
                case OTEscrowRecord_RecordViability_RECORD_VIABILITY_PARTIALLY_VIABLE:
                    translated.escrowInformationMetadata.bottleValidity = @"valid";
                    break;
                case OTEscrowRecord_RecordViability_RECORD_VIABILITY_LEGACY:
                    translated.escrowInformationMetadata.bottleValidity = @"invalid";
                    break;
            }
        }
        if(translated.recordId == nil || [translated.recordId isEqualToString:@""]){
            translated.recordId = [[translated.label stringByReplacingOccurrencesOfString:OTEscrowRecordPrefix withString:@""] mutableCopy];
        }
        if((translated.serialNumber == nil || [translated.serialNumber isEqualToString:@""]) && translated.escrowInformationMetadata.peerInfo != nil && [translated.escrowInformationMetadata.peerInfo length] > 0) {
            CFErrorRef cfError = NULL;
            SOSPeerInfoRef peer = SOSPeerInfoCreateFromData(kCFAllocatorDefault, &cfError, (__bridge CFDataRef)translated.escrowInformationMetadata.peerInfo);
            if(cfError != NULL){
                secnotice("octagontrust-handleEscrowRecords", "failed to create sos peer info: %@", cfError);
            } else {
                translated.serialNumber = (NSString*)CFBridgingRelease(SOSPeerInfoCopySerialNumber(peer));
            }
        }

        [escrowRecords addObject:translated];
    }
    subTaskSuccess = true;
    OctagonSignpostEnd(signPost, OctagonSignpostNameFetchEscrowRecords, OctagonSignpostNumber1(OctagonSignpostNameFetchEscrowRecords), (int)subTaskSuccess);

    if (shouldFiler == YES) {
        return [OTClique filterRecords:escrowRecords];
    }

    return escrowRecords;
}

+ (NSArray<OTEscrowRecord*>* _Nullable)fetchAllEscrowRecords:(OTConfigurationContext*)data error:(NSError**)error
{
#if OCTAGON
    secnotice("octagontrust-fetchallescrowrecords", "fetching all escrow records for context :%@, altdsid:%@", data.context, data.altDSID);
    return [OTClique fetchAndHandleEscrowRecords:data shouldFilter:NO error:error];
#else
    if (error) {
        *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecUnimplemented userInfo:nil];
    }
    return nil;
#endif
}

+ (NSArray<OTEscrowRecord*>* _Nullable)fetchEscrowRecords:(OTConfigurationContext*)data error:(NSError**)error
{
#if OCTAGON
    if (OctagonIsOptimizationEnabled() && OctagonIsEscrowRecordFetchEnabled()) {
        secnotice("octagontrust-fetchescrowrecords", "fetching filtered escrow records for context with feature flag enabled:%@, altdsid:%@", data.context, data.altDSID);
        return [OTClique fetchAndHandleEscrowRecords:data shouldFilter:YES error:error];
    } else {
        if ([OTClique isCloudServicesAvailable] == NO) {
            if (error) {
                *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecUnimplemented userInfo:nil];
            }
            return NULL;
        }

        OctagonSignpost signPost = OctagonSignpostBegin(OctagonSignpostNameGetAccountInfo);
        secnotice("octagontrust-fetchescrowrecords", "fetching escrow records for context with feature flag disabled:%@, altdsid:%@", data.context, data.altDSID);
        id<OctagonEscrowRecovererPrococol> sb = data.sbd ?: [[getSecureBackupClass() alloc] init];

        OTEscrowAuthenticationInformation *escrowAuth = nil;
        NSDictionary* escrowAuthDictionary = nil;
        if(data.escrowAuth != nil) {
            escrowAuth = data.escrowAuth;
            escrowAuthDictionary = [OTEscrowTranslation escrowAuthenticationInfoToDictionary:escrowAuth];
        }
        NSDictionary* results = nil;
        NSError* recoverError = [sb getAccountInfoWithInfo:escrowAuthDictionary results:&results];
        bool subTaskSuccess = false;
        if(recoverError || results == nil) {
            secerror("octagontrust-fetchescrowrecords: error fetching escrow records: %@", recoverError);
            if(error){
                *error = recoverError;
            }
            OctagonSignpostEnd(signPost, OctagonSignpostNameGetAccountInfo, OctagonSignpostNumber1(OctagonSignpostNameGetAccountInfo), (int)subTaskSuccess);
            return nil;
        } else {
            secnotice("octagontrust-fetchescrowrecords", "recovered accountWithInfo results: %@", results);
            NSArray *icdpRecords = results[getkSecureBackupiCDPRecordsKey()];
            secnotice("octagontrust-fetchescrowrecords", "recovered iCDP records: %@", icdpRecords);

            NSMutableArray<OTEscrowRecord*>* records = [NSMutableArray array];
            for (NSDictionary *dictionaryRecord in icdpRecords) {
                OTEscrowRecord* otEscrowRecord = [OTEscrowTranslation dictionaryToEscrowRecord:dictionaryRecord];
                [records addObject:otEscrowRecord];
            }
            secnotice("octagontrust-fetchescrowrecords", "translated dictionary records to escrow record protos: %@", records);

            subTaskSuccess = true;
            OctagonSignpostEnd(signPost, OctagonSignpostNameGetAccountInfo, OctagonSignpostNumber1(OctagonSignpostNameGetAccountInfo), (int)subTaskSuccess);

            return records;
        }
    }
#else
    if (error) {
        *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecUnimplemented userInfo:nil];
    }
    return nil;
#endif
}

+ (OTClique* _Nullable)handleRecoveryResults:(OTConfigurationContext*)data recoveredInformation:(NSDictionary*)recoveredInformation sosViability:(OTEscrowRecord_SOSViability)sosViability performedSilentBurn:(BOOL)performedSilentBurn recoverError:(NSError*)recoverError error:(NSError**)error
{
    if ([self isCloudServicesAvailable] == NO) {
        if (error) {
            *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecUnimplemented userInfo:nil];
        }
        return nil;
    }

    OTClique* clique = [[OTClique alloc] initWithContextData:data];
    BOOL resetToOfferingOccured = NO;

    if(recoverError) {
        secnotice("octagontrust-handleRecoveryResults", "sbd escrow recovery failed: %@", recoverError);
        if(recoverError.code == 40 /* kSecureBackupRestoringLegacyBackupKeychainError */ && [recoverError.domain isEqualToString:getkSecureBackupErrorDomain()]) {
            if([OTClique platformSupportsSOS]) {
                secnotice("octagontrust-handleRecoveryResults", "Can't restore legacy backup with no keybag. Resetting SOS to offering");
                CFErrorRef resetToOfferingError = NULL;
                bool successfulReset = SOSCCResetToOffering(&resetToOfferingError);
                resetToOfferingOccured = YES;
                if(!successfulReset || resetToOfferingError) {
                    secerror("octagontrust-handleRecoveryResults: failed to reset to offering:%@", resetToOfferingError);
                } else {
                    secnotice("octagontrust-handleRecoveryResults", "resetting SOS circle successful");
                }
                CFReleaseNull(resetToOfferingError);
            } else {
                secnotice("octagontrust-handleRecoveryResults", "Legacy restore failed on a non-SOS platform");
            }
        } else {
            if(error) {
                *error = recoverError;
            }
            return nil;
        }
    }

    NSError* localError = nil;
    OTControl* control = nil;

    if (data.otControl) {
        control = data.otControl;
    } else {
        control = [data makeOTControl:&localError];
    }
    if (!control) {
        secerror("octagontrust-handleRecoveryResults: unable to create otcontrol: %@", localError);
        if (error) {
            *error = localError;
        }
        return nil;
    }

    // look for OT Bottles
    NSString *bottleID = recoveredInformation[@"bottleID"];
    NSString *isValid = recoveredInformation[@"bottleValid"];
    NSData *bottledPeerEntropy = recoveredInformation[@"EscrowServiceEscrowData"][@"BottledPeerEntropy"];
    bool shouldResetOctagon = false;

    if(bottledPeerEntropy && bottleID && [isValid isEqualToString:@"valid"]){
        secnotice("octagontrust-handleRecoveryResults", "recovering from bottle: %@", bottleID);
        __block NSError* restoreBottleError = nil;
        OctagonSignpost bottleRestoreSignPost = performedSilentBurn ? OctagonSignpostBegin(OctagonSignpostNamePerformOctagonJoinForSilent) : OctagonSignpostBegin(OctagonSignpostNamePerformOctagonJoinForNonSilent);

        //restore bottle!
        [control restore:OTCKContainerName
               contextID:data.context
              bottleSalt:data.altDSID
                 entropy:bottledPeerEntropy
                bottleID:bottleID
                   reply:^(NSError * _Nullable restoreError) {
            if(restoreError) {
                secnotice("octagontrust-handleRecoveryResults", "restore bottle errored: %@", restoreError);
            } else {
                secnotice("octagontrust-handleRecoveryResults", "restoring bottle succeeded");
            }
            restoreBottleError = restoreError;
            if (performedSilentBurn) {
                OctagonSignpostEnd(bottleRestoreSignPost, OctagonSignpostNamePerformOctagonJoinForSilent, OctagonSignpostNumber1(OctagonSignpostNamePerformOctagonJoinForSilent), (int)true);
            } else {
                OctagonSignpostEnd(bottleRestoreSignPost, OctagonSignpostNamePerformOctagonJoinForNonSilent, OctagonSignpostNumber1(OctagonSignpostNamePerformOctagonJoinForNonSilent), (int)true);
            }
        }];

        if(restoreBottleError) {
            if(error){
                *error = restoreBottleError;
            }
            return nil;
        }
    } else {
        shouldResetOctagon = true;
    }

    if(shouldResetOctagon) {
        secnotice("octagontrust-handleRecoveryResults", "bottle %@ is not valid, resetting octagon", bottleID);
        NSError* resetError = nil;
        [clique resetAndEstablish:CuttlefishResetReasonNoBottleDuringEscrowRecovery error:&resetError];
        if(resetError) {
            secerror("octagontrust-handleRecoveryResults: failed to reset octagon: %@", resetError);
            if(error){
                *error = resetError;
            }
            return nil;
        } else{
            secnotice("octagontrust-handleRecoveryResults", "reset octagon succeeded");
        }
    }

    // Join SOS circle if platform is supported and we didn't previously reset SOS
    if (OctagonPlatformSupportsSOS() && resetToOfferingOccured == NO) {
        if (sosViability == OTEscrowRecord_SOSViability_SOS_NOT_VIABLE) {
            secnotice("octagontrust-handleRecoveryResults", "Record will not allow device to join SOS.  Invoking reset to offering");
            CFErrorRef resetToOfferingError = NULL;
            bool successfulReset = SOSCCResetToOffering(&resetToOfferingError);
            if(!successfulReset || resetToOfferingError) {
                secerror("octagontrust-handleRecoveryResults: failed to reset to offering:%@", resetToOfferingError);
            } else {
                secnotice("octagontrust-handleRecoveryResults", "resetting SOS circle successful");
            }
            CFReleaseNull(resetToOfferingError);
        } else {
            NSError* joinAfterRestoreError = nil;
            secnotice("octagontrust-handleRecoveryResults", "attempting joinAfterRestore");
            BOOL joinAfterRestoreResult = [clique joinAfterRestore:&joinAfterRestoreError];
            if(joinAfterRestoreError || joinAfterRestoreResult == NO) {
                secnotice("octagontrust-handleRecoveryResults", "failed to join after restore: %@", joinAfterRestoreError);
            } else {
                secnotice("octagontrust-handleRecoveryResults", "joinAfterRestore succeeded");
            }
        }
    }


    // call SBD to kick off keychain data restore
    id<OctagonEscrowRecovererPrococol> sb = data.sbd ?: [[getSecureBackupClass() alloc] init];
    NSError* restoreError = nil;

    NSMutableSet <NSString *> *viewsNotToBeRestored = [NSMutableSet set];
    [viewsNotToBeRestored addObject:@"iCloudIdentity"];
    [viewsNotToBeRestored addObject:@"PCS-MasterKey"];
    [viewsNotToBeRestored addObject:@"KeychainV0"];

    NSDictionary *escrowRecords = recoveredInformation[getkEscrowServiceRecordDataKey()];
    if (escrowRecords == nil) {
        secnotice("octagontrust-handleRecoveryResults", "unable to request keychain restore, record data missing");
        return clique;
    }


    NSData *keybagDigest = escrowRecords[getkSecureBackupKeybagDigestKey()];
    NSData *password = escrowRecords[getkSecureBackupBagPasswordKey()];
    if (keybagDigest == nil || password == nil) {
        secnotice("octagontrust-handleRecoveryResults", "unable to request keychain restore, digest or password missing");
        return clique;
    }

    BOOL haveBottledPeer = (bottledPeerEntropy && bottleID && [isValid isEqualToString:@"valid"]) || shouldResetOctagon;
    [sb restoreKeychainAsyncWithPassword:password
                            keybagDigest:keybagDigest
                         haveBottledPeer:haveBottledPeer
                    viewsNotToBeRestored:viewsNotToBeRestored
                                   error:&restoreError];

    if (restoreError) {
        secerror("octagontrust-handleRecoveryResults: error restoring keychain items: %@", restoreError);
    }

    return clique;
}

+ (instancetype _Nullable)performEscrowRecovery:(OTConfigurationContext*)data
                                     cdpContext:(OTICDPRecordContext*)cdpContext
                                   escrowRecord:(OTEscrowRecord*)escrowRecord
                                          error:(NSError**)error
{
#if OCTAGON
    secnotice("octagontrust-performEscrowRecovery", "performEscrowRecovery invoked for context:%@, altdsid:%@", data.context, data.altDSID);

    if ([self isCloudServicesAvailable] == NO) {
        if (error) {
            *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecUnimplemented userInfo:nil];
        }
        return nil;
    }

    OctagonSignpost performEscrowRecoverySignpost = OctagonSignpostBegin(OctagonSignpostNamePerformEscrowRecovery);
    bool subTaskSuccess = true;

    id<OctagonEscrowRecovererPrococol> sb = data.sbd ?: [[getSecureBackupClass() alloc] init];
    NSDictionary* recoveredInformation = nil;
    NSError* recoverError = nil;

    BOOL supportedRestorePath = [OTEscrowTranslation supportedRestorePath:cdpContext];
    secnotice("octagontrust-performEscrowRecovery", "restore path is supported? %@", supportedRestorePath ? @"YES" : @"NO");

    if (OctagonIsOptimizationEnabled() && supportedRestorePath) {
        secnotice("octagontrust-performEscrowRecovery", "optimization flag turned on");
        OctagonSignpost recoverFromSBDSignPost = OctagonSignpostBegin(OctagonSignpostNameRecoverWithCDPContext);
        recoveredInformation = [sb recoverWithCDPContext:cdpContext escrowRecord:escrowRecord error:&recoverError];
        subTaskSuccess = (recoverError == nil) ? true : false;
        OctagonSignpostEnd(recoverFromSBDSignPost, OctagonSignpostNameRecoverWithCDPContext, OctagonSignpostNumber1(OctagonSignpostNameRecoverWithCDPContext), (int)subTaskSuccess);
    } else {
        secnotice("octagontrust-performEscrowRecovery", "optimization flag turned off");
        NSMutableDictionary* sbdRecoveryArguments = [[OTEscrowTranslation CDPRecordContextToDictionary:cdpContext] mutableCopy];
        NSDictionary* metadata = [OTEscrowTranslation metadataToDictionary:escrowRecord.escrowInformationMetadata];
        sbdRecoveryArguments[getkSecureBackupMetadataKey()] = metadata;
        sbdRecoveryArguments[getkSecureBackupRecordIDKey()] = escrowRecord.recordId;
        secnotice("octagontrust-performEscrowRecovery", "using sbdRecoveryArguments: %@", sbdRecoveryArguments);

        OctagonSignpost recoverFromSBDSignPost = OctagonSignpostBegin(OctagonSignpostNamePerformRecoveryFromSBD);
        recoverError = [sb recoverWithInfo:sbdRecoveryArguments results:&recoveredInformation];
        subTaskSuccess = (recoverError == nil) ? true : false;
        OctagonSignpostEnd(recoverFromSBDSignPost, OctagonSignpostNamePerformRecoveryFromSBD, OctagonSignpostNumber1(OctagonSignpostNamePerformRecoveryFromSBD), (int)subTaskSuccess);
    }

    OTEscrowRecord_SOSViability viableForSOS = escrowRecord.viabilityStatus;

    OTClique* clique = [OTClique handleRecoveryResults:data recoveredInformation:recoveredInformation sosViability:viableForSOS performedSilentBurn:NO recoverError:recoverError error:error];

    if(recoverError) {
        subTaskSuccess = false;
        OctagonSignpostEnd(performEscrowRecoverySignpost, OctagonSignpostNamePerformEscrowRecovery, OctagonSignpostNumber1(OctagonSignpostNamePerformEscrowRecovery), (int)subTaskSuccess);
    } else {
        OctagonSignpostEnd(performEscrowRecoverySignpost, OctagonSignpostNamePerformEscrowRecovery, OctagonSignpostNumber1(OctagonSignpostNamePerformEscrowRecovery), (int)subTaskSuccess);
    }
    return clique;

#else
    if (error) {
        *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecUnimplemented userInfo:nil];
    }
    return nil;
#endif
}

+ (OTEscrowRecord* _Nullable) recordMatchingLabel:(NSString*)label allRecords:(NSArray<OTEscrowRecord*>*)allRecords
{
    for(OTEscrowRecord* record in allRecords) {
        if ([record.label isEqualToString:label]) {
            return record;
        }
    }
    return nil;
}

+ (instancetype _Nullable)performSilentEscrowRecovery:(OTConfigurationContext*)data
                                           cdpContext:(OTICDPRecordContext*)cdpContext
                                           allRecords:(NSArray<OTEscrowRecord*>*)allRecords
                                                error:(NSError**)error
{
#if OCTAGON
    secnotice("octagontrust-performSilentEscrowRecovery", "performSilentEscrowRecovery invoked for context:%@, altdsid:%@", data.context, data.altDSID);

    if ([self isCloudServicesAvailable] == NO) {
        if (error) {
            *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecUnimplemented userInfo:nil];
        }
        return nil;
    }

    OctagonSignpost performSilentEscrowRecoverySignpost = OctagonSignpostBegin(OctagonSignpostNamePerformSilentEscrowRecovery);
    bool subTaskSuccess = true;

    id<OctagonEscrowRecovererPrococol> sb = data.sbd ?: [[getSecureBackupClass() alloc] init];
    NSDictionary* recoveredInformation = nil;
    NSError* recoverError = nil;

    BOOL supportedRestorePath = [OTEscrowTranslation supportedRestorePath:cdpContext];
    secnotice("octagontrust-performSilentEscrowRecovery", "restore path is supported? %@", supportedRestorePath ? @"YES" : @"NO");

    if (OctagonIsOptimizationEnabled() && supportedRestorePath) {
        secnotice("octagontrust-performSilentEscrowRecovery", "optimization flag turned on");
        OctagonSignpost recoverFromSBDSignPost = OctagonSignpostBegin(OctagonSignpostNameRecoverSilentWithCDPContext);
        recoveredInformation = [sb recoverSilentWithCDPContext:cdpContext allRecords:allRecords error:&recoverError];
        subTaskSuccess = (recoverError == nil) ? true : false;
        OctagonSignpostEnd(recoverFromSBDSignPost, OctagonSignpostNameRecoverSilentWithCDPContext, OctagonSignpostNumber1(OctagonSignpostNameRecoverSilentWithCDPContext), (int)subTaskSuccess);
    } else {
        secnotice("octagontrust-performSilentEscrowRecovery", "optimization flag turned off");
        NSDictionary* sbdRecoveryArguments = [OTEscrowTranslation CDPRecordContextToDictionary:cdpContext];

        OctagonSignpost recoverFromSBDSignPost = OctagonSignpostBegin(OctagonSignpostNamePerformRecoveryFromSBD);
        recoverError = [sb recoverWithInfo:sbdRecoveryArguments results:&recoveredInformation];
        subTaskSuccess = (recoverError == nil) ? true : false;
        OctagonSignpostEnd(recoverFromSBDSignPost, OctagonSignpostNamePerformRecoveryFromSBD, OctagonSignpostNumber1(OctagonSignpostNamePerformRecoveryFromSBD), (int)subTaskSuccess);
    }

    NSString* label = recoveredInformation[getkSecureBackupRecordLabelKey()];
    OTEscrowRecord* chosenRecord = [OTClique recordMatchingLabel:label allRecords:allRecords];
    OTEscrowRecord_SOSViability viableForSOS = chosenRecord ? chosenRecord.viabilityStatus : OTEscrowRecord_SOSViability_SOS_VIABLE_UNKNOWN;

    OTClique *clique = [OTClique handleRecoveryResults:data recoveredInformation:recoveredInformation sosViability:viableForSOS performedSilentBurn:YES recoverError:recoverError error:error];

    if(recoverError) {
        subTaskSuccess = false;
        OctagonSignpostEnd(performSilentEscrowRecoverySignpost, OctagonSignpostNamePerformSilentEscrowRecovery, OctagonSignpostNumber1(OctagonSignpostNamePerformSilentEscrowRecovery), (int)subTaskSuccess);
    } else {
        OctagonSignpostEnd(performSilentEscrowRecoverySignpost, OctagonSignpostNamePerformSilentEscrowRecovery, OctagonSignpostNumber1(OctagonSignpostNamePerformSilentEscrowRecovery), (int)subTaskSuccess);
    }

    return clique;

#else
    if (error) {
        *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecUnimplemented userInfo:nil];
    }
    return nil;
#endif
}

+ (BOOL) invalidateEscrowCache:(OTConfigurationContext*)configurationContext error:(NSError**)error
{
#if OCTAGON
    secnotice("octagontrust-invalidateEscrowCache", "invalidateEscrowCache invoked for context:%@, altdsid:%@", configurationContext.context, configurationContext.altDSID);
    __block NSError* localError = nil;
    __block BOOL invalidatedCache = NO;

    OTControl *control = [configurationContext makeOTControl:&localError];
    if (!control) {
        secnotice("clique-invalidateEscrowCache", "unable to create otcontrol: %@", localError);
        if (error) {
            *error = localError;
        }
        return invalidatedCache;
    }

    [control invalidateEscrowCache:OTCKContainerName
                         contextID:configurationContext.context
                             reply:^(NSError * _Nullable invalidateError) {
        if(invalidateError) {
            secnotice("clique-invalidateEscrowCache", "invalidateEscrowCache errored: %@", invalidateError);
        } else {
            secnotice("clique-invalidateEscrowCache", "invalidateEscrowCache succeeded");
            invalidatedCache = YES;
        }
        localError = invalidateError;
    }];

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

    secnotice("clique-invalidateEscrowCache", "invalidateEscrowCache complete");

    return invalidatedCache;
#else
    if (error) {
        *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecUnimplemented userInfo:nil];
    }
    return NO;
#endif
}

@end

#endif