OTContext.m   [plain text]


/*
 * Copyright (c) 2017 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 OCTAGON

#import "OTContext.h"
#import "SFPublicKey+SPKI.h"

#include <utilities/SecFileLocations.h>
#include <Security/SecRandomP.h>

#import "keychain/ckks/CKKS.h"
#import "keychain/ckks/CKKSViewManager.h"
#import "keychain/ckks/CKKSAnalytics.h"

#import <CoreCDP/CDPAccount.h>

NSString* OTCKContainerName = @"com.apple.security.keychain";
NSString* OTCKZoneName = @"OctagonTrust";
static NSString* const kOTRampZoneName = @"metadata_zone";

@interface OTContext (lockstateTracker) <CKKSLockStateNotification>
@end

@interface OTContext ()

@property (nonatomic, strong) NSString* contextID;
@property (nonatomic, strong) NSString* contextName;
@property (nonatomic, strong) NSString* dsid;

@property (nonatomic, strong) OTLocalStore* localStore;
@property (nonatomic, strong) OTCloudStore* cloudStore;
@property (nonatomic, strong) NSData* changeToken;
@property (nonatomic, strong) NSString* egoPeerID;
@property (nonatomic, strong) NSDate*   egoPeerCreationDate;
@property (nonatomic, strong) dispatch_queue_t queue;
@property (nonatomic, weak) id <OTContextIdentityProvider> identityProvider;

@property (nonatomic, strong) CKKSCKAccountStateTracker* accountTracker;
@property (nonatomic, strong) CKKSLockStateTracker* lockStateTracker;
@property (nonatomic, strong) CKKSReachabilityTracker *reachabilityTracker;

@end

@implementation OTContext

-(CKContainer*)makeCKContainer:(NSString*)containerName {
    CKContainer* container = [CKContainer containerWithIdentifier:containerName];
    container = [[CKContainer alloc] initWithContainerID: container.containerID];
    return container;
}

-(BOOL) isPrequeliteEnabled
{
    BOOL result = YES;
    if([PQLConnection class] == nil) {
        secerror("OT: prequelite appears to not be linked. Can't create OT objects.");
        result = NO;
    }
    return result;
}

- (nullable instancetype) initWithContextID:(NSString*)contextID
                                       dsid:(NSString*)dsid
                                 localStore:(OTLocalStore*)localStore
                                 cloudStore:(nullable OTCloudStore*)cloudStore
                           identityProvider:(id <OTContextIdentityProvider>)identityProvider
                                      error:(NSError**)error
{
    if(![self isPrequeliteEnabled]){
        // We're running in the base build environment, which lacks a bunch of libraries.
        // We don't support doing anything in this environment. Bye.
        return nil;
    }

    self = [super init];
    if (self) {
        NSError* localError = nil;
        _contextID = contextID;
        _dsid = dsid;
        _identityProvider = identityProvider;
        _localStore = localStore;
        
        NSString* contextAndDSID = [NSString stringWithFormat:@"%@-%@", contextID, dsid];

        CKContainer* container = [self makeCKContainer:OTCKContainerName];

        _accountTracker = [CKKSViewManager manager].accountTracker;
        _lockStateTracker = [CKKSViewManager manager].lockStateTracker;
        _reachabilityTracker = [CKKSViewManager manager].reachabilityTracker;

        if(!cloudStore) {
            _cloudStore = [[OTCloudStore alloc]initWithContainer:container
                                                        zoneName:OTCKZoneName
                                                  accountTracker:_accountTracker
                                             reachabilityTracker:_reachabilityTracker
                                                      localStore:_localStore
                                                       contextID:contextID
                                                            dsid:dsid
                            fetchRecordZoneChangesOperationClass:[CKFetchRecordZoneChangesOperation class]
                                      fetchRecordsOperationClass:[CKFetchRecordsOperation class]
                                             queryOperationClass:[CKQueryOperation class]
                               modifySubscriptionsOperationClass:[CKModifySubscriptionsOperation class]
                                 modifyRecordZonesOperationClass:[CKModifyRecordZonesOperation class]
                                              apsConnectionClass:[APSConnection class]
                                                  operationQueue:nil];
        } else{
            _cloudStore = cloudStore;
        }

        OTContextRecord* localContextRecord = [_localStore readLocalContextRecordForContextIDAndDSID:contextAndDSID error:&localError];

        if(localContextRecord == nil || localContextRecord.contextID == nil){
            localError = nil;
            BOOL result = [_localStore initializeContextTable:contextID dsid:dsid error:&localError];
            if(!result || localError != nil){
                secerror("octagon: reading from database failed with error: %@", localError);
                if (error) {
                    *error = localError;
                }
                return nil;
            }
            localContextRecord = [_localStore readLocalContextRecordForContextIDAndDSID:contextAndDSID error:&localError];
            if(localContextRecord == nil || localError !=nil){
                secerror("octagon: reading from database failed with error: %@", localError);
                if (error) {
                    *error = localError;
                }
                return nil;
            }
        }
        
        _contextID = localContextRecord.contextID;
        _contextName = localContextRecord.contextName;
        _changeToken = localContextRecord.changeToken;
        _egoPeerID = localContextRecord.egoPeerID;
        _egoPeerCreationDate = localContextRecord.egoPeerCreationDate;
        
        _queue = dispatch_queue_create("com.apple.security.otcontext", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

- (nullable OTBottledPeerSigned *) createBottledPeerRecordForIdentity:(OTIdentity *)identity
                                                               secret:(NSData*)secret
                                                                error:(NSError**)error
{
    NSError* localError = nil;
    if(self.lockStateTracker.isLocked){
        secnotice("octagon", "device is locked");
        if(error){
            *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:nil];
        }
        return nil;
    }

    OTEscrowKeys *escrowKeys = [[OTEscrowKeys alloc] initWithSecret:secret dsid:self.dsid error:&localError];
    if (!escrowKeys || localError != nil) {
        secerror("octagon: unable to derive escrow keys: %@", localError);
        if (error) {
            *error = localError;
        }
        return nil;
    }
    
    OTBottledPeer *bp = [[OTBottledPeer alloc] initWithPeerID:identity.peerID
                                                         spID:identity.spID
                                               peerSigningKey:identity.peerSigningKey
                                            peerEncryptionKey:identity.peerEncryptionKey
                                                   escrowKeys:escrowKeys
                                                        error:&localError];
    if (!bp || localError !=nil) {
        secerror("octagon: unable to create a bottled peer: %@", localError);
        if (error) {
            *error = localError;
        }
        return nil;
    }
    return [[OTBottledPeerSigned alloc] initWithBottledPeer:bp
                                         escrowedSigningKey:escrowKeys.signingKey
                                             peerSigningKey:identity.peerSigningKey
                                                      error:error];
}

- (NSData* _Nullable) makeMeSomeEntropy:(int)requiredLength
{
    NSMutableData* salt = [NSMutableData dataWithLength:requiredLength];
    if (salt == nil){
        return nil;
    }
    if (SecRandomCopyBytes(kSecRandomDefault, [salt length], [salt mutableBytes]) != 0){
        return nil;
    }
    return salt;
}

- (nullable OTPreflightInfo*) preflightBottledPeer:(NSString*)contextID
                                           entropy:(NSData*)entropy
                                             error:(NSError**)error
{
    NSError* localError = nil;
    if(self.lockStateTracker.isLocked){
        secnotice("octagon", "device is locked");
        if(error){
            *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:nil];
        }
        return nil;
    }

    OTIdentity *identity = [self.identityProvider currentIdentity:&localError];
    if (!identity || localError != nil) {
        secerror("octagon: unable to get current identity:%@", localError);
        if (error) {
            *error = localError;
        }
        return nil;
    }

    OTBottledPeerSigned* bps = [self createBottledPeerRecordForIdentity:identity
                                                                 secret:entropy
                                                                  error:&localError];
    if (!bps || localError != nil) {
        secerror("octagon: failed to create bottled peer record: %@", localError);
        if (error) {
            *error = localError;
        }
        return nil;
    }
    secnotice("octagon", "created bottled peer:%@", bps);
    
    OTBottledPeerRecord *bprec = [bps asRecord:identity.spID];
    
    if (!identity.spID) {
        secerror("octagon: cannot enroll without a spID");
        if(error){
            *error = [NSError errorWithDomain:octagonErrorDomain code:OTErrorNoIdentity userInfo:@{NSLocalizedDescriptionKey: @"OTIdentity does not have an SOS peer id"}];
        }
        return nil;
    }

    OTPreflightInfo* info = [[OTPreflightInfo alloc]init];
    info.escrowedSigningSPKI = bprec.escrowedSigningSPKI;

    if(!info.escrowedSigningSPKI){
        if(error){
            *error = [NSError errorWithDomain:octagonErrorDomain code:OTErrorEscrowSigningSPKI userInfo:@{NSLocalizedDescriptionKey: @"Escrowed spinging SPKI is nil"}];
        }
        secerror("octagon: Escrowed spinging SPKI is nil");
        return nil;
    }

    info.bottleID = bprec.recordName;
    if(!info.bottleID){
        if(error){
            *error = [NSError errorWithDomain:octagonErrorDomain code:OTErrorBottleID userInfo:@{NSLocalizedDescriptionKey: @"BottleID is nil"}];
        }
        secerror("octagon: BottleID is nil");
        return nil;
    }

    //store record in localStore
    BOOL result = [self.localStore insertBottledPeerRecord:bprec escrowRecordID:identity.spID error:&localError];
    if(!result || localError){
        secerror("octagon: could not persist the bottle record: %@", localError);
        if (error) {
            *error = localError;
        }
        return nil;
    }

    return info;
}

- (BOOL)scrubBottledPeer:(NSString*)contextID
                bottleID:(NSString*)bottleID
                   error:(NSError**)error
{
    secnotice("octagon", "scrubBottledPeer");
    NSError* localError = nil;
    if(self.lockStateTracker.isLocked){
        secnotice("octagon", "device is locked");
        if(error){
            *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:nil];
        }
        return YES;
    }

    BOOL result = [self.localStore deleteBottledPeer:bottleID error:&localError];
    if(!result || localError != nil){
        secerror("octagon: could not remove record for bottleID %@, error:%@", bottleID, localError);
        if (error) {
            *error = localError;
        }
    }
    return result;
}

- (OTBottledPeerSigned *) restoreFromEscrowRecordID:(NSString*)escrowRecordID
                                             secret:(NSData*)secret
                                              error:(NSError**)error
{
    NSError *localError = nil;

    if(self.lockStateTracker.isLocked){
        if(error){
            *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:nil];
        }
        return nil;
    }

    OTEscrowKeys *escrowKeys = [[OTEscrowKeys alloc] initWithSecret:secret dsid:self.dsid error:&localError];
    if (!escrowKeys || localError != nil) {
        secerror("unable to derive escrow keys: %@", localError);
        if (error) {
            *error = localError;
        }
        return nil;
    }

    BOOL result = [self.cloudStore downloadBottledPeerRecord:&localError];
    if(!result || localError){
        secerror("octagon: could not download bottled peer record:%@", localError);
        if(error){
            *error = localError;
        }
    }
    NSString* recordName = [OTBottledPeerRecord constructRecordID:escrowRecordID
                                                escrowSigningSPKI:[escrowKeys.signingKey.publicKey asSPKI]];
    OTBottledPeerRecord* rec = [self.localStore readLocalBottledPeerRecordWithRecordID:recordName error:&localError];

    if (!rec) {
        secerror("octagon: could not read bottled peer record:%@", localError);
        if (error) {
            *error = localError;
        }
        return nil;
    }

    OTBottledPeerSigned *bps = [[OTBottledPeerSigned alloc] initWithBottledPeerRecord:rec
                                                                           escrowKeys:escrowKeys
                                                                                error:&localError];
    if (!bps) {
        secerror("octagon: could not unpack bottled peer:%@", localError);
        if (error) {
            *error = localError;
        }
        return nil;
    }

    return bps;
}

-(BOOL)bottleExistsLocallyForIdentity:(OTIdentity*)identity logger:(CKKSAnalytics*)logger error:(NSError**)error
{
    NSError* localError = nil;
    //read all the local bp records
    NSArray<OTBottledPeerRecord*>* bottles = [self.localStore readLocalBottledPeerRecordsWithMatchingPeerID:identity.spID error:&localError];
    if(!bottles || [bottles count] == 0 || localError != nil){
        secerror("octagon: there are no eligible bottle peer records: %@", localError);
        [logger logRecoverableError:localError
                           forEvent:OctagonEventBottleCheck
                           zoneName:kOTRampZoneName
                     withAttributes:NULL];
        if(error){
            *error = localError;
        }
        return NO;
    }

    BOOL hasBottle = NO;
    //if check all the records if the peer signing public key matches the bottled one!
    for(OTBottledPeerRecord* bottle in bottles){
        NSData* bottledSigningSPKIData = [[SFECPublicKey fromSPKI:bottle.peerSigningSPKI] keyData];
        NSData* currentIdentitySPKIData = [identity.peerSigningKey.publicKey keyData];

        //spIDs are the same AND check bottle signature
        if([currentIdentitySPKIData isEqualToData:bottledSigningSPKIData] &&
           [OTBottledPeerSigned verifyBottleSignature:bottle.bottle
                                            signature:bottle.signatureUsingPeerKey
                                                  key:identity.peerSigningKey.publicKey
                                                error:error]){
            hasBottle = YES;
        }
    }



    return hasBottle;
}

-(BOOL)queryCloudKitForBottle:(OTIdentity*)identity logger:(CKKSAnalytics*)logger error:(NSError**)error
{
    NSError* localError = nil;
    BOOL hasBottle = NO;
    //attempt to pull down all the records, but continue checking local store even if this fails.
    BOOL fetched = [self.cloudStore downloadBottledPeerRecord:&localError];
    if(fetched == NO || localError != nil){ //couldn't download bottles
        secerror("octagon: 0 bottled peers downloaded: %@", localError);
        [logger logRecoverableError:localError
                           forEvent:OctagonEventBottleCheck
                           zoneName:kOTRampZoneName
                     withAttributes:NULL];
        if(error){
            *error = localError;
        }
        return NO;
    }else{ //downloaded bottles, let's check local store
        hasBottle = [self bottleExistsLocallyForIdentity:identity logger:logger error:&localError];
    }

    if(error){
        *error = localError;
    }
    return hasBottle;
}

-(OctagonBottleCheckState) doesThisDeviceHaveABottle:(NSError**)error
{
    secnotice("octagon", "checking if device has enrolled a bottle");

    if(self.lockStateTracker.isLocked){
        secnotice("octagon", "device locked, not checking for bottle");
        if(error){
            *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:nil];
        }
        return UNCLEAR;
    }

    if(self.accountTracker.currentCKAccountInfo.accountStatus != CKAccountStatusAvailable){
        if(error){
            *error = [NSError errorWithDomain:octagonErrorDomain
                                         code:OTErrorNotSignedIn
                                     userInfo:@{NSLocalizedDescriptionKey: @"iCloud account is logged out"}];
        }
        secnotice("octagon", "not logged into an account");
        return UNCLEAR;
    }

    NSError* localError = nil;
    OctagonBottleCheckState bottleStatus = NOBOTTLE;
    CKKSAnalytics* logger = [CKKSAnalytics logger];
    SFAnalyticsActivityTracker *tracker = [logger logSystemMetricsForActivityNamed:CKKSActivityBottleCheck withAction:nil];
    [tracker start];

    //get our current identity
    OTIdentity* identity = [self.identityProvider currentIdentity:&localError];

    //if we get the locked error, return true so we don't prompt the user
    if(localError && [_lockStateTracker isLockedError:localError]){
        secnotice("octagon", "attempting to perform bottle check while locked: %@", localError);
        return UNCLEAR;
    }

    if(!identity && localError != nil){
        secerror("octagon: do not have an identity: %@", localError);
        [logger logRecoverableError:localError
                           forEvent:OctagonEventBottleCheck
                           zoneName:kOTRampZoneName
                     withAttributes:NULL];
        [tracker stop];
        if(error){
            *error = localError;
        }
        return NOBOTTLE;
    }

    //check locally first
    BOOL bottleExistsLocally = [self bottleExistsLocallyForIdentity:identity logger:logger error:&localError];

    //no bottle and we have no network
    if(!bottleExistsLocally && !self.reachabilityTracker.currentReachability){
        secnotice("octagon", "no network, can't query");
        localError = [NSError errorWithDomain:octagonErrorDomain
                                         code:OTErrorNoNetwork
                                     userInfo:@{NSLocalizedDescriptionKey: @"no network"}];
        [tracker stop];
        if(error){
            *error = localError;
        }
        return UNCLEAR;
    }
    else if(!bottleExistsLocally){
        if([self queryCloudKitForBottle:identity logger:logger error:&localError]){
            bottleStatus = BOTTLE;
        }
    }else if(bottleExistsLocally){
        bottleStatus = BOTTLE;
    }

    if(bottleStatus == NOBOTTLE){
        localError = [NSError errorWithDomain:octagonErrorDomain code:OTErrorNoBottlePeerRecords userInfo:@{NSLocalizedDescriptionKey: @"Peer %@ does not have any bottled records"}];
        secerror("octagon: this device does not have any bottled peers: %@", localError);
        [logger logRecoverableError:localError
                           forEvent:OctagonEventBottleCheck
                           zoneName:kOTRampZoneName
                     withAttributes:@{ OctagonEventAttributeFailureReason : @"does not have bottle"}];
        if(error){
            *error = localError;
        }
    }
    else{
        [logger logSuccessForEventNamed:OctagonEventBottleCheck];
    }

    [tracker stop];

    return bottleStatus;
}

@end
#endif