Source: keychain/keychain.ts

/**
 * @author Erik Haßlmeyer <erik.hasslmeyer@refinio.net>
 * @copyright REFINIO GmbH 2022
 * @license CC-BY-NC-SA-2.5; portions MIT License
 * @version 0.0.1
 */

import type {CryptoApi} from '../crypto/CryptoApi';
import type {KeyPair} from '../crypto/encryption';
import {createKeyPair} from '../crypto/encryption';
import type {SignKeyPair} from '../crypto/sign';
import {createSignKeyPair} from '../crypto/sign';
import {createError} from '../errors';
import {createMessageBus} from '../message-bus';
import type {Instance, Keys, Person} from '../recipes';
import {getAllEntries} from '../reverse-map-query';
import type {SHA256Hash, SHA256IdHash} from '../util/type-checks';
import {createCryptoApi, storeKeys} from './key-storage';
import {hasSecretKeys} from './key-storage-secret';
import {MasterKeyManager} from './master-key-manager';

const MessageBus = createMessageBus('keychain-person');
const masterKeyManager = new MasterKeyManager('keychain_masterkey', 'keychain_salt');

// ######## Keychain lock / unlock ########

/**
 * Unlock the keychain.
 *
 * This will derive a key from the secret and store the master key in memory.
 *
 * @param {string} secret
 * @returns {Promise<void>}
 */
export async function unlockOrCreateKeyChain(secret: string): Promise<void> {
    await masterKeyManager.loadOrCreateMasterKey(secret);
}

/**
 * Lock the keychain.
 *
 * The master key is purged from memory and all other keychain functions won't work anymore.
 */
export function lockKeyChain(): void {
    masterKeyManager.unloadMasterKey();
}

/**
 * Changes the secret used to unlock the keychain.
 *
 * @param {string} oldSecret
 * @param {string} newSecret
 */
export async function changeKeyChainSecret(oldSecret: string, newSecret: string): Promise<void> {
    await masterKeyManager.changeSecret(oldSecret, newSecret);
}

// ######## get crypto apis ########

/**
 * Create a crypto functions for a person or instance.
 *
 * @param {SHA256IdHash<Person | Instance>} owner
 * @returns {Promise<CryptoApi>}
 */
export async function createCryptoApiFromDefaultKeys(
    owner: SHA256IdHash<Person | Instance>
): Promise<CryptoApi> {
    const defaultKeys = await getDefaultKeys(owner);
    return createCryptoApi(defaultKeys, masterKeyManager);
}

// ######## Get keys associated with a specific owner ########

/**
 * Get a list of all keys associated with this owner.
 *
 * Note: Incomplete keys are not trustworthy. The Keys object could have been sent from anyone.
 *
 * @param {SHA256IdHash<Person | Instance>} owner
 * @returns {Promise<Array<{keys: SHA256Hash<Keys>, complete: boolean, default: boolean}>>}
 */
export async function getListOfKeys(owner: SHA256IdHash<Person | Instance>): Promise<
    Array<{
        keys: SHA256Hash<Keys>;
        complete: boolean;
        default: boolean;
    }>
> {
    const keysObjs = await getAllEntries(owner, 'Keys');
    return Promise.all(
        keysObjs.map(async keysObj => {
            const secretKeysExist = await hasSecretKeys(keysObj);
            return {keys: keysObj, complete: secretKeysExist, default: secretKeysExist};
        })
    );
}

/**
 * Get incomplete keys associated with a person.
 *
 * Incomplete means, that you do not possess secret keys for this keys object.
 *
 * Note: The keys are not trustworthy. The Keys object could have been sent from anyone.
 *
 * @param {SHA256IdHash<Person | Instance>} owner
 * @returns {Promise<Array<SHA256Hash<Keys>>>}
 */
export async function getListOfIncompleteKeys(
    owner: SHA256IdHash<Person | Instance>
): Promise<Array<SHA256Hash<Keys>>> {
    const listOfKeys = await getListOfKeys(owner);
    return listOfKeys.filter(keys => !keys.complete).map(keys => keys.keys);
}

/**
 * Get a list of complete keys associated with a person.
 *
 * Note: You can trust complete keys, because the secret part can only be written by yourself.
 *
 * @param {SHA256IdHash<Person | Instance>} owner
 * @returns {Promise<Array<{keys: SHA256Hash<Keys>, default: boolean}>>}
 */
export async function getListOfCompleteKeys(owner: SHA256IdHash<Person | Instance>): Promise<
    Array<{
        keys: SHA256Hash<Keys>;
        default: boolean;
    }>
> {
    const listOfKeys = await getListOfKeys(owner);
    return listOfKeys
        .filter(keys => keys.complete)
        .map(keys => ({keys: keys.keys, default: keys.default}));
}

/**
 * Get the person keys for which we have the secret part (hence complete)
 *
 * @param {SHA256IdHash<Person | Instance>} owner
 * @returns {Promise<SHA256Hash<Keys>>}
 */
export async function getDefaultKeys(
    owner: SHA256IdHash<Person | Instance>
): Promise<SHA256Hash<Keys>> {
    const listOfKeys = await getListOfKeys(owner);
    const defaultKeys = listOfKeys.filter(keys => keys.default);

    if (defaultKeys.length === 0) {
        throw createError('KEYCH-NODEFKEYS', {owner});
    }

    if (defaultKeys.length > 1) {
        MessageBus.send(
            'Error',
            'We have more than one complete set keys. That is currently not expected.'
        );
    }

    return defaultKeys[0].keys;
}

/**
 * Returns whether we have a complete keypair for a person.
 *
 * 'Complete' means that we have public and secret keys.
 *
 * @param {SHA256IdHash<Person | Instance>} owner
 * @returns {Promise<boolean>}
 */
export async function hasDefaultKeys(owner: SHA256IdHash<Person | Instance>): Promise<boolean> {
    const listOfKeys = await getListOfKeys(owner);
    return listOfKeys.some(keys => keys.default);
}

// ######## Keys creation ########

/**
 * Create new default encryption and sign key pairs for a person.
 *
 * If a default keypair already exists, this function will fail. At the moment this is to ensure,
 * that we only have a single private key for a person (will change in the future - but makes things
 * easier right now).
 *
 * @param {SHA256IdHash<Person | Instance>} owner
 * @param {KeyPair} encryptionKeyPair - If keypair is omitted, create a random keypair
 * @param {SignKeyPair} signKeyPair - If keypair is omitted, create a random keypair
 * @returns {Promise<SHA256Hash<Keys>>}
 */
export async function createDefaultKeys(
    owner: SHA256IdHash<Person | Instance>,
    encryptionKeyPair: KeyPair = createKeyPair(),
    signKeyPair: SignKeyPair = createSignKeyPair()
): Promise<SHA256Hash<Keys>> {
    masterKeyManager.ensureMasterKeyLoaded();

    if (await hasDefaultKeys(owner)) {
        throw createError('KEYCH-HASDEFKEYS');
    }

    return (await storeKeys(owner, encryptionKeyPair, signKeyPair, masterKeyManager)).hash;
}

/**
 * Same as createDefaultKeys() but skip if default keys already exist.
 *
 * @param {SHA256IdHash<Person | Instance>} owner
 * @param {KeyPair} encryptionKeyPair
 * @param {SignKeyPair} signKeyPair
 * @returns {Promise<SHA256Hash<Keys>>}
 */
export async function createDefaultKeysIfNotExist(
    owner: SHA256IdHash<Person | Instance>,
    encryptionKeyPair: KeyPair = createKeyPair(),
    signKeyPair: SignKeyPair = createSignKeyPair()
): Promise<{keys: SHA256Hash<Keys>; exists: boolean}> {
    if (await hasDefaultKeys(owner)) {
        const keys = await getDefaultKeys(owner);

        return {
            keys,
            exists: true
        };
    } else {
        const keysResult = await storeKeys(owner, encryptionKeyPair, signKeyPair, masterKeyManager);

        return {
            keys: keysResult.hash,
            exists: false
        };
    }
}