/*
 * 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@
 */

//
//  SOSAccountRecovery.c
//  Security
//

#include <AssertMacros.h>
#include "SOSAccountPriv.h"
#include "SOSCloudKeychainClient.h"

#include <Security/SecureObjectSync/SOSPeerInfoCollections.h>
#include <Security/SecureObjectSync/SOSViews.h>
#include <Security/SecureObjectSync/SOSAccountTrustClassic+Circle.h>
#include <Security/SecureObjectSync/SOSAccountTrustClassic+Expansion.h>

#include "SOSInternal.h"
#include "SecADWrapper.h"

#include <Security/SecureObjectSync/SOSRecoveryKeyBag.h>
#include <Security/SecureObjectSync/SOSRingRecovery.h>

CFStringRef kRecoveryRingKey = CFSTR("recoveryKeyBag");

bool SOSAccountSetRecoveryKeyBagEntry(CFAllocatorRef allocator, SOSAccount* account, SOSRecoveryKeyBagRef rkbg, CFErrorRef *error) {
    CFDataRef rkbg_as_data = NULL;
    bool result = false;
    rkbg_as_data = SOSRecoveryKeyBagCopyEncoded(rkbg, error);
    result = rkbg_as_data && SOSAccountSetValue(account, kRecoveryRingKey, rkbg_as_data, error);
    CFReleaseNull(rkbg_as_data);
    return result;
}

SOSRecoveryKeyBagRef SOSAccountCopyRecoveryKeyBagEntry(CFAllocatorRef allocator, SOSAccount* account, CFErrorRef *error) {
    SOSRecoveryKeyBagRef retval = NULL;
    CFDataRef rkbg_as_data = asData(SOSAccountGetValue(account, kRecoveryRingKey, error), error);
    require_quiet(rkbg_as_data, errOut);
    retval = SOSRecoveryKeyBagCreateFromData(allocator, rkbg_as_data, error);
errOut:
    return retval;
}

SOSRecoveryKeyBagRef SOSAccountCopyRecoveryKeyBag(CFAllocatorRef allocator, SOSAccount* account, CFErrorRef *error) {
    SOSRingRef recRing = NULL;
    SOSRecoveryKeyBagRef rkbg = NULL;
    require_action_quiet(account, errOut, SOSCreateError(kSOSErrorParam, CFSTR("No Account Object"), NULL, error));
    recRing = SOSAccountCopyRingNamed(account, kSOSRecoveryRing, error);
    require_quiet(recRing, errOut);
    rkbg = SOSRingCopyRecoveryKeyBag(recRing, error);
errOut:
    CFReleaseNull(recRing);
    return rkbg;
}


static CFDataRef SOSRKNullKey(void) {
    static CFDataRef localNullKey = NULL;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        localNullKey = CFDataCreate(kCFAllocatorDefault, (const UInt8*) "nullkey", 8);
    });
    return localNullKey;
}

CFDataRef SOSAccountCopyRecoveryPublic(CFAllocatorRef allocator, SOSAccount* account, CFErrorRef *error) {
    SOSRecoveryKeyBagRef rkbg = SOSAccountCopyRecoveryKeyBag(allocator, account, error);
    CFDataRef recKey = NULL;
    require_quiet(rkbg, errOut);
    CFDataRef tmpKey = SOSRecoveryKeyBagGetKeyData(rkbg, error);
    require_quiet(tmpKey, errOut);
    require_quiet(!CFEqualSafe(tmpKey, SOSRKNullKey()), errOut);
    recKey = CFDataCreateCopy(kCFAllocatorDefault, tmpKey);
errOut:
    CFReleaseNull(rkbg);
    if(!recKey) {
        if(error && !(*error)) SOSErrorCreate(kSOSErrorNoKey, error, NULL, CFSTR("No recovery key available"));
    }
    return recKey;
}

static bool SOSAccountUpdateRecoveryRing(SOSAccount* account, CFErrorRef *error,
                                         SOSRingRef (^modify)(SOSRingRef existing, CFErrorRef *error)) {
    bool result = SOSAccountUpdateNamedRing(account, kSOSRecoveryRing, error, ^SOSRingRef(CFStringRef ringName, CFErrorRef *error) {
        return SOSRingCreate(ringName, (__bridge CFStringRef)(account.peerID), kSOSRingRecovery, error);
    }, modify);
    
    return result;
}

static bool SOSAccountSetKeybagForRecoveryRing(SOSAccount* account, SOSRecoveryKeyBagRef keyBag, CFErrorRef *error) {
    bool result = SOSAccountUpdateRecoveryRing(account, error, ^SOSRingRef(SOSRingRef existing, CFErrorRef *error) {
        SOSRingRef newRing = NULL;
        CFSetRef peerSet = [account.trust copyPeerSetMatching:^bool(SOSPeerInfoRef peer) {
            return true;
        }];
        CFMutableSetRef cleared = CFSetCreateMutableForCFTypes(NULL);
        
        SOSRingSetPeerIDs(existing, cleared);
        SOSRingAddAll(existing, peerSet);
        
        require_quiet(SOSRingSetRecoveryKeyBag(existing, account.fullPeerInfo, keyBag, error), exit);
        
        newRing = CFRetainSafe(existing);
    exit:
        CFReleaseNull(cleared);
        return newRing;
    });
    
    SOSClearErrorIfTrue(result, error);
    
    if (!result) {
        secnotice("recovery", "Got error setting keybag for recovery : %@", error ? (CFTypeRef) *error : (CFTypeRef) CFSTR("No error space."));
    }
    return result;
}

bool SOSAccountRemoveRecoveryKey(SOSAccount* account, CFErrorRef *error) {
    return SOSAccountSetRecoveryKey(account, SOSRKNullKey(), error);
}

bool SOSAccountSetRecoveryKey(SOSAccount* account, CFDataRef pubData, CFErrorRef *error) {
    __block bool result = false;
    CFDataRef oldRecoveryKey = NULL;
    SOSRecoveryKeyBagRef rkbg = NULL;

    require_quiet([account.trust isInCircle:error], exit);
    oldRecoveryKey = SOSAccountCopyRecoveryPublic(kCFAllocatorDefault, account, NULL); // ok to fail here. don't collect error
    require_action_quiet(!CFEqualSafe(pubData, oldRecoveryKey), exit, result = true);

    CFDataPerformWithHexString(pubData, ^(CFStringRef recoveryKeyString) {
        CFDataPerformWithHexString(oldRecoveryKey, ^(CFStringRef oldRecoveryKeyString) {
            secnotice("recovery", "SetRecoveryPublic: %@ from %@", recoveryKeyString, oldRecoveryKeyString);
        });
    });
    
    rkbg = SOSRecoveryKeyBagCreateForAccount(kCFAllocatorDefault, (__bridge CFTypeRef)account, pubData, error);
    require_quiet(rkbg, exit);

    result = SOSAccountSetKeybagForRecoveryRing(account, rkbg, error);
    SOSAccountSetRecoveryKeyBagEntry(kCFAllocatorDefault, account, rkbg, NULL);
    {
        SOSAccountForEachBackupView(account, ^(const void *value) {
            CFStringRef viewName = (CFStringRef)value;
            SOSAccountNewBKSBForView(account, viewName, error);
        });
    }

    account.circle_rings_retirements_need_attention = true;

exit:
    CFReleaseNull(oldRecoveryKey);
    CFReleaseNull(rkbg);
    SOSClearErrorIfTrue(result, error);
    if (!result) {
        // if we're failing and something above failed to give an error - make a generic one.
        if(error && !(*error)) SOSErrorCreate(kSOSErrorProcessingFailure, error, NULL, CFSTR("Failed to set Recovery Key"));
        secnotice("recovery", "SetRecoveryPublic Failed: %@", error ? (CFTypeRef) *error : (CFTypeRef) CFSTR("No error space"));
    }
    return result;
}

bool SOSAccountRecoveryKeyIsInBackupAndCurrentInView(SOSAccount* account, CFStringRef viewname) {
    bool result = false;
    CFErrorRef bsError = NULL;
    CFDataRef backupSliceData = NULL;
    SOSRingRef ring = NULL;
    SOSBackupSliceKeyBagRef backupSlice = NULL;

    CFDataRef rkbg = SOSAccountCopyRecoveryPublic(kCFAllocatorDefault, account, NULL);
    require_quiet(rkbg, errOut);
    
    CFStringRef ringName = SOSBackupCopyRingNameForView(viewname);
    ring = [account.trust copyRing:ringName err:&bsError];
    CFReleaseNull(ringName);
    
    require_quiet(ring, errOut);
    
    //grab the backup slice from the ring
    backupSliceData = SOSRingGetPayload(ring, &bsError);
    require_quiet(backupSliceData, errOut);
    
    backupSlice = SOSBackupSliceKeyBagCreateFromData(kCFAllocatorDefault, backupSliceData, &bsError);
    require_quiet(backupSlice, errOut);

    result = SOSBKSBPrefixedKeyIsInKeyBag(backupSlice, bskbRkbgPrefix, rkbg);
    CFReleaseNull(backupSlice);
errOut:
    CFReleaseNull(ring);
    CFReleaseNull(rkbg);
    
    if (bsError) {
        secnotice("backup", "Failed to find BKSB: %@, %@ (%@)", backupSliceData, backupSlice, bsError);
    }
    CFReleaseNull(bsError);
    return result;

}

static void sosRecoveryAlertAndNotify(SOSAccount* account, SOSRecoveryKeyBagRef oldRingRKBG, SOSRecoveryKeyBagRef ringRKBG) {
    secnotice("recovery", "Recovery Key changed: old %@ new %@", oldRingRKBG, ringRKBG);
    notify_post(kSOSCCRecoveryKeyChanged);
}

void SOSAccountEnsureRecoveryRing(SOSAccount* account) {
    static SOSRecoveryKeyBagRef oldRingRKBG = NULL;
    bool inCircle = [account.trust isInCircle:NULL];
    CFStringRef accountDSID = SOSAccountGetValue(account, kSOSDSIDKey, NULL); // murfxxx this needs to be consulted still
    SOSRecoveryKeyBagRef acctRKBG = SOSAccountCopyRecoveryKeyBagEntry(kCFAllocatorDefault, account, NULL);
    SOSRecoveryKeyBagRef ringRKBG = SOSAccountCopyRecoveryKeyBag(kCFAllocatorDefault, account, NULL);
    if(!SOSRecoveryKeyBagDSIDIs(ringRKBG, accountDSID)) CFReleaseNull(ringRKBG);
    if(!SOSRecoveryKeyBagDSIDIs(acctRKBG, accountDSID)) CFReleaseNull(acctRKBG);
    
    if(inCircle && acctRKBG == NULL && ringRKBG == NULL) {
        // Nothing to do at this time - notify if this is a change down below.
    } else if(inCircle && acctRKBG == NULL) { // then we have a ringRKBG
        secnotice("recovery", "Harvesting account recovery key from ring");
        SOSAccountSetRecoveryKeyBagEntry(kCFAllocatorDefault, account, ringRKBG, NULL);
    } else if(ringRKBG == NULL) {
        // Nothing to do at this time - notify if this is a change down below.
        secnotice("recovery", "Account has a recovery key, but none found in recovery ring");
    } else if(!CFEqual(acctRKBG, ringRKBG)) {
        secnotice("recovery", "Harvesting account recovery key from ring");
        SOSAccountSetRecoveryKeyBagEntry(kCFAllocatorDefault, account, ringRKBG, NULL);
    }
    
    if(!CFEqualSafe(oldRingRKBG, ringRKBG)) {
        sosRecoveryAlertAndNotify(account, oldRingRKBG, ringRKBG);
        CFTransferRetained(oldRingRKBG, ringRKBG);
    }
    
    CFReleaseNull(ringRKBG);
    CFReleaseNull(acctRKBG);
}
