"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseExchangeKeysManager = void 0;
const utils_1 = require("../utils");
const utils_2 = require("./utils");
/**
 * @internal This class is meant only for internal use and may be changed without notice.
 * Functions to create and get exchange keys.
 * The methods of this api require to pass the appropriate keys for encryption/decryption manually.
 */
class BaseExchangeKeysManager {
    constructor(primitives, dataOwnerApi, hcpartyBaseApi, patientBaseApi, deviceBaseApi) {
        this.generateKeyConcurrencyMap = {};
        this.primitives = primitives;
        this.dataOwnerApi = dataOwnerApi;
        this.hcpartyBaseApi = hcpartyBaseApi;
        this.patientBaseApi = patientBaseApi;
        this.deviceBaseApi = deviceBaseApi;
    }
    /**
     * Updates the aes exchange keys between the current data owner and another data owner to allow the other data owner to access the exchange key
     * using his new public key. Note that this will make existing exchange keys from the other data owner to the current data owner invalid for
     * encryption.
     * @param otherDataOwner the other data owner.
     * @param newDataOwnerPublicKey a new public key of the other data owner.
     * @param keyPairsByFingerprint all available key pairs to use for the decryption of existing aes exchange keys.
     */
    giveAccessBackTo(otherDataOwner, newDataOwnerPublicKey, keyPairsByFingerprint) {
        return __awaiter(this, void 0, void 0, function* () {
            const selfId = yield this.dataOwnerApi.getCurrentDataOwnerId();
            const other = yield this.dataOwnerApi.getCryptoActorStub(otherDataOwner);
            const newKeyHashVersion = (0, utils_2.getShaVersionForKey)(other.stub, newDataOwnerPublicKey);
            if (!newKeyHashVersion)
                throw new Error(`Public key not found for data owner ${otherDataOwner}`);
            const newPublicKey = yield this.primitives.RSA.importKey('spki', (0, utils_1.ua2ab)((0, utils_1.hex2ua)(newDataOwnerPublicKey)), ['encrypt'], newKeyHashVersion);
            yield this.extendForGiveAccessBackTo(selfId, otherDataOwner, (0, utils_2.fingerprintV1)(newDataOwnerPublicKey), newPublicKey, keyPairsByFingerprint);
            yield this.extendForGiveAccessBackTo(otherDataOwner, selfId, (0, utils_2.fingerprintV1)(newDataOwnerPublicKey), newPublicKey, keyPairsByFingerprint);
        });
    }
    /**
     * Get the encrypted exchange keys for a delegator-delegate pair.
     * @param delegatorId id of the delegator data owner.
     * @param delegateId id of the delegate data owner.
     * @return an array of exchange keys from the delegator to delegate where each key is encrypted with one or more public keys.
     */
    getEncryptedExchangeKeysFor(delegatorId, delegateId) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a, _b, _c;
            const delegator = yield this.dataOwnerApi.getCryptoActorStub(delegatorId);
            const res = Object.values((_a = delegator.stub.aesExchangeKeys) !== null && _a !== void 0 ? _a : {}).flatMap((delegateToKey) => {
                const encryptedKeyForDelegate = delegateToKey[delegateId];
                if (encryptedKeyForDelegate) {
                    return [encryptedKeyForDelegate];
                }
                else {
                    return [];
                }
            });
            const legacyDelegation = (_c = (_b = delegator.stub.hcPartyKeys) === null || _b === void 0 ? void 0 : _b[delegateId]) === null || _c === void 0 ? void 0 : _c[1];
            if (legacyDelegation)
                res.push({ '': legacyDelegation });
            return res;
        });
    }
    /**
     * Get all exchange keys where the provided data owner is involved either as the delegator or as the delegate.
     * @param dataOwnerId id of a data owner.
     * @param otherOwnerTypes only exchange keys between the current data owner and data owners of this type will be included in the result. If null considers all delegates
     * @return all exchange keys involving the provided data owner. Note that there may be an overlap between some keys to and from the data owner.
     */
    getAllExchangeKeysWith(dataOwnerId, otherOwnerTypes) {
        return __awaiter(this, void 0, void 0, function* () {
            if (otherOwnerTypes != null && otherOwnerTypes.length === 0)
                throw new Error('otherOwnerTypes must not be empty!');
            const keysToOwner = yield Promise.all([
                otherOwnerTypes == null || otherOwnerTypes.find((x) => x === 'hcp')
                    ? this.hcpartyBaseApi.getAesExchangeKeysForDelegate(dataOwnerId).catch(() => { })
                    : Promise.resolve({}),
                otherOwnerTypes == null || otherOwnerTypes.find((x) => x === 'patient')
                    ? this.patientBaseApi.getPatientAesExchangeKeysForDelegate(dataOwnerId).catch(() => { })
                    : Promise.resolve({}),
                otherOwnerTypes == null || otherOwnerTypes.find((x) => x === 'device')
                    ? this.deviceBaseApi.getDeviceAesExchangeKeysForDelegate(dataOwnerId).catch(() => { })
                    : Promise.resolve({}),
            ]).then(([a, b, c]) => (Object.assign(Object.assign(Object.assign({}, a), b), c)));
            const dataOwner = yield this.dataOwnerApi.getCryptoActorStub(dataOwnerId);
            const allOwnerKeys = yield this.combineLegacyHcpKeysWithAesExchangeKeys(dataOwner.stub, undefined, false);
            const filteredDelegates = new Set(yield Array.from(new Set(Object.values(allOwnerKeys).flatMap((x) => Object.keys(x)))).reduce((acc, ownerId) => __awaiter(this, void 0, void 0, function* () {
                const awaitedAcc = yield acc;
                if (ownerId === dataOwnerId || otherOwnerTypes == null) {
                    return [...awaitedAcc, ownerId];
                }
                else {
                    try {
                        const dataOwnerType = (yield this.dataOwnerApi.getCryptoActorStub(ownerId)).type;
                        if (otherOwnerTypes.some((x) => x === dataOwnerType)) {
                            return [...awaitedAcc, ownerId];
                        }
                        else
                            return awaitedAcc;
                    }
                    catch (_) {
                        return awaitedAcc;
                    }
                }
            }), Promise.resolve([])));
            const keysFromOwner = Object.fromEntries(Object.entries(allOwnerKeys)
                .map(([delegatorPubKey, delegateToKeys]) => [
                delegatorPubKey,
                Object.fromEntries(Object.entries(delegateToKeys).filter(([delegateId]) => filteredDelegates.has(delegateId))),
            ])
                .filter(([, x]) => Object.keys(x).length > 0));
            return { keysToOwner, keysFromOwner };
        });
    }
    /**
     * Attempts to decrypt many exchange keys using any of the provided key pairs.
     * @param encryptedExchangeKeys an array of exchange keys where each key is encrypted with one or more public keys.
     * @param keyPairsByFingerprint rsa key pairs to use for decryption.
     * @return an array all successfully decrypted exchange keys and an array containing all exchange keys which could not be decrypted.
     */
    tryDecryptExchangeKeys(encryptedExchangeKeys, keyPairsByFingerprint) {
        return __awaiter(this, void 0, void 0, function* () {
            const raws = new Set();
            const res = { successfulDecryptions: [], failedDecryptions: [] };
            for (const encryptedExchangeKey of encryptedExchangeKeys) {
                const decrypted = yield this.tryDecryptExchangeKey(encryptedExchangeKey, keyPairsByFingerprint);
                if (decrypted !== undefined && !raws.has(decrypted.raw)) {
                    raws.add(decrypted.raw);
                    res.successfulDecryptions.push(decrypted.key);
                }
                else {
                    res.failedDecryptions.push(encryptedExchangeKey);
                }
            }
            return res;
        });
    }
    extendForGiveAccessBackTo(delegatorId, delegateId, newPublicKeyFp, newPublicKey, decryptionKeyPairsByFingerprint) {
        return __awaiter(this, void 0, void 0, function* () {
            yield (0, utils_1.notConcurrent)(this.generateKeyConcurrencyMap, delegatorId, () => __awaiter(this, void 0, void 0, function* () {
                const delegator = yield this.dataOwnerApi.getCryptoActorStub(delegatorId);
                const delegate = yield this.dataOwnerApi.getCryptoActorStub(delegateId);
                let didUpdateSomeKey = false;
                const combinedKeys = yield this.combineLegacyHcpKeysWithAesExchangeKeys(delegator.stub, delegate.stub, true);
                const updatedExchangeKeys = {};
                const newEncryptionKeys = { [newPublicKeyFp]: newPublicKey };
                for (const [currDelegatorKey, currDelegatesToKeys] of Object.entries(combinedKeys)) {
                    const updatedDelegatesToKeys = {};
                    for (const [currDelegateId, currEncryptedKey] of Object.entries(currDelegatesToKeys)) {
                        if (delegateId !== currDelegateId || newPublicKeyFp in currEncryptedKey) {
                            updatedDelegatesToKeys[currDelegateId] = currEncryptedKey;
                        }
                        else {
                            const decrypted = yield this.tryDecryptExchangeKey(currEncryptedKey, decryptionKeyPairsByFingerprint);
                            if (decrypted) {
                                didUpdateSomeKey = true;
                                updatedDelegatesToKeys[currDelegateId] = Object.assign(Object.assign({}, currEncryptedKey), (yield this.encryptExchangeKey(decrypted, newEncryptionKeys)).encryptedExchangeKey);
                            }
                            else {
                                updatedDelegatesToKeys[currDelegateId] = currEncryptedKey;
                            }
                        }
                    }
                    updatedExchangeKeys[currDelegatorKey] = updatedDelegatesToKeys;
                }
                if (didUpdateSomeKey) {
                    yield this.dataOwnerApi.modifyCryptoActorStub({
                        type: delegator.type,
                        stub: Object.assign(Object.assign({}, delegator.stub), { aesExchangeKeys: updatedExchangeKeys }),
                    });
                }
            }));
        });
    }
    /**
     * Attempts to decrypt an exchange key using any of the provided key pairs.
     * @param encryptedExchangeKey an encrypted exchange key, in the form publicKeyXyzFingerprint -> hex(exchangeKeyEncryptedByPrivateKeyXyz).
     * @param keyPairsByFingerprint rsa key pairs to use for decryption.
     * @return the decrypted exchange key, in raw and key format (to allow deduplication), or undefined if the key could not be decrypted using the
     * provided keys.
     */
    tryDecryptExchangeKey(encryptedExchangeKey, keyPairsByFingerprint) {
        return __awaiter(this, void 0, void 0, function* () {
            for (const [entryKey, encrypted] of Object.entries(encryptedExchangeKey)) {
                // Due to bugs in past version the entry may actually contain the full public key instead of just the fingerprint.
                const fp = (0, utils_2.fingerprintV1)(entryKey);
                const keyPair = keyPairsByFingerprint[fp];
                if (keyPair !== undefined) {
                    const res = yield this.tryDecryptExchangeKeyWith(encrypted, keyPair, fp);
                    if (res !== undefined)
                        return res;
                }
            }
            const defaultEncryptedKey = encryptedExchangeKey[''];
            if (defaultEncryptedKey !== undefined) {
                for (const keyPair of Object.values(keyPairsByFingerprint)) {
                    // disable error logging, we are not sure keyPair is the correct key
                    const res = yield this.tryDecryptExchangeKeyWith(defaultEncryptedKey, keyPair, undefined);
                    if (res !== undefined)
                        return res;
                }
            }
            return undefined;
        });
    }
    /**
     * Creates an encrypted exchange key for the provided public keys.
     * @param exchangeKey the exchange key in raw and imported format. If undefined a new key will be automatically created.
     * @param publicKeys additional public keys that will have access to the exchange key in spki format, hex-encoded.
     * @return the exchangeKey and an object with the exchange key encrypted with each of the provided public keys, hex-encoded, by fingerprint.
     */
    encryptExchangeKey(exchangeKey, publicKeys) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            const exchangeKeyCrypto = (_a = exchangeKey === null || exchangeKey === void 0 ? void 0 : exchangeKey.key) !== null && _a !== void 0 ? _a : (yield this.primitives.AES.generateCryptoKey(false));
            const exchangeKeyBytes = exchangeKey !== undefined ? (0, utils_1.hex2ua)(exchangeKey.raw) : yield this.primitives.AES.exportKey(exchangeKeyCrypto, 'raw');
            const encryptedExchangeKey = yield Object.entries(publicKeys).reduce((acc_1, _a) => __awaiter(this, [acc_1, _a], void 0, function* (acc, [currKeyFp, currKey]) {
                return (Object.assign(Object.assign({}, (yield acc)), { [currKeyFp]: (0, utils_1.ua2hex)(yield this.primitives.RSA.encrypt(currKey, new Uint8Array(exchangeKeyBytes))) }));
            }), Promise.resolve({}));
            return { exchangeKey: exchangeKeyCrypto, encryptedExchangeKey };
        });
    }
    /**
     * Attempts to decrypt an exchange key using the provided key pairs.
     * If you are confident that the provided {@link keyPair} was used to encrypt {@link encryptedByOneKey} you should also provide {@link keyPairFp},
     * so that if the decryption fail an error will be logged to console before returning undefined.
     * @param encryptedByOneKey an exchange key which may be encrypted with {@link keyPair}.
     * @param keyPair an rsa key pair.
     * @param keyPairFp the fingerprint of the provided key pair or undefined to disable logging of error if the decryption failed.
     * @return the decrypted exchange key, in raw and key format (to allow deduplication), or undefined if the key could not be decrypted using the
     * provided key.
     */
    tryDecryptExchangeKeyWith(encryptedByOneKey, keyPair, keyPairFp // if undefined will not log errors, when we are not sure the key to be used is the provided key.
    ) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                const decrypted = yield this.primitives.RSA.decrypt(keyPair.privateKey, (0, utils_1.hex2ua)(encryptedByOneKey));
                return { raw: (0, utils_1.ua2hex)(decrypted), key: yield this.primitives.AES.importKey('raw', decrypted) };
            }
            catch (e) {
                if (keyPairFp) {
                    console.error(`Failed to decrypt or import exchange key ${encryptedByOneKey} using key with fingerprint ${keyPairFp}.`, e);
                }
            }
        });
    }
    // Copy all legacy hcp exchange keys into the new aes exchange keys
    combineLegacyHcpKeysWithAesExchangeKeys(owner, delegate, fillDelegateLegacyKey) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a, _b, _c, _d, _e;
            const ownerLegacyPublicKey = owner.publicKey;
            if (ownerLegacyPublicKey != undefined && !((_a = owner.aesExchangeKeys) !== null && _a !== void 0 ? _a : {})[ownerLegacyPublicKey]) {
                /*
                 * This condition would technically prevent new updates to the hcPartyKeys to be migrated to the aes exchange keys, but since I can only update
                 * data for self data owner and parent entities this is not an issue, because I will always be using the new api from now on and I won't have
                 * a situation where the legacy keys are updated but the aes exchange keys are not.
                 */
                const counterPartsById = {};
                if (fillDelegateLegacyKey) {
                    counterPartsById[owner.id] = owner;
                    if (delegate) {
                        counterPartsById[delegate.id] = delegate;
                    }
                    for (const other of Object.keys((_b = owner.hcPartyKeys) !== null && _b !== void 0 ? _b : {}).filter((x) => x !== owner.id && x !== (delegate === null || delegate === void 0 ? void 0 : delegate.id))) {
                        try {
                            counterPartsById[other] = (yield this.dataOwnerApi.getCryptoActorStub(other)).stub;
                        }
                        catch (_f) {
                            // ignore
                        }
                    }
                }
                return Object.assign({ [ownerLegacyPublicKey]: Object.entries((_c = owner.hcPartyKeys) !== null && _c !== void 0 ? _c : {}).reduce((acc, [hcpId, keys]) => {
                        var _a, _b;
                        const counterpartKey = (_a = counterPartsById[hcpId]) === null || _a === void 0 ? void 0 : _a.publicKey;
                        acc[hcpId] = {
                            [(0, utils_2.fingerprintV1)(ownerLegacyPublicKey)]: keys[0],
                            [(_b = (counterpartKey && (0, utils_2.fingerprintV1)(counterpartKey))) !== null && _b !== void 0 ? _b : '']: keys[1],
                        };
                        return acc;
                    }, {}) }, ((_d = owner.aesExchangeKeys) !== null && _d !== void 0 ? _d : {}));
            }
            else
                return (_e = owner.aesExchangeKeys) !== null && _e !== void 0 ? _e : {};
        });
    }
}
exports.BaseExchangeKeysManager = BaseExchangeKeysManager;
//# sourceMappingURL=BaseExchangeKeysManager.js.map