"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.KeyRecovery = void 0;
const RSA_1 = require("./RSA");
const utils_1 = require("../utils");
const utils_2 = require("./utils");
/**
 * @internal this class is intended only for internal use and may be changed without notice.
 *
 */
class KeyRecovery {
    constructor(primitives, dataOwnerApi, baseExchangeKeysManager, baseExchangeDataManager) {
        this.primitives = primitives;
        this.dataOwnerApi = dataOwnerApi;
        this.baseExchangeKeysManager = baseExchangeKeysManager;
        this.baseExchangeDataManager = baseExchangeDataManager;
    }
    /*TODO
     * Currently there is no support for the recovery of signature keys. When implementing a recovery solution we should consider:
     * - unlike decryption keys signature keys are completely useless if not verified.
     * - we are fine losing the signature private key, but if we could recover and verify the public key it would reduce the inconveniences on user and
     * limit the creation of new exchange data
     * - We can't guarantee that encrypted data in iCure was put there by us... unless we put a piece of the private key as well: we can save an
     * hash of (any verification public key + any encryption private key) in the data owner: only the owner of the private key can create this hash,
     * so if the data owner can recreate the hash with one of his recovered and verified encryption private keys then the signature public key will
     * for sure be safe as well.
     */
    /**
     * Attempt to recover private keys of a data owner using all available key recovery methods and all available private keys. The method will
     * automatically try to use newly recovered key pairs to recover additional key pairs.
     * @param dataOwner a data owner.
     * @param knownKeys keys available for the data owner.
     * @return new key pairs (exclude already known pairs) which could be recovered using the known keys.
     */
    recoverKeys(dataOwner, knownKeys) {
        return __awaiter(this, void 0, void 0, function* () {
            const selfPublicKeys = Array.from([
                ...this.dataOwnerApi.getHexPublicKeysWithSha1Of(dataOwner.dataOwner),
                ...this.dataOwnerApi.getHexPublicKeysWithSha256Of(dataOwner.dataOwner),
            ]);
            const knownKeysFpSet = new Set(Object.keys(knownKeys));
            const missingKeysFpSet = new Set(selfPublicKeys.map((x) => (0, utils_2.fingerprintV1)(x)).filter((x) => !knownKeysFpSet.has(x)));
            const recoveryFunctions = [this.recoverFromTransferKeys.bind(this), this.recoverFromShamirSplitKeys.bind(this)];
            let allPrivateKeys = Object.assign({}, knownKeys);
            let foundNewPrivateKeys = true;
            while (missingKeysFpSet.size > 0 && foundNewPrivateKeys) {
                // TODO for each recovered verify correct association with public key
                foundNewPrivateKeys = false;
                for (const recover of recoveryFunctions) {
                    const recovered = yield recover(dataOwner, allPrivateKeys, missingKeysFpSet);
                    const validatedRecovered = {};
                    for (const [fp, keyPair] of Object.entries(recovered)) {
                        const valid = yield this.primitives.RSA.checkKeyPairValidity(keyPair);
                        if (valid) {
                            validatedRecovered[fp] = keyPair;
                        }
                    }
                    if (Object.keys(validatedRecovered).length > 0) {
                        foundNewPrivateKeys = true;
                        Object.keys(validatedRecovered).forEach((fp) => missingKeysFpSet.delete(fp));
                        allPrivateKeys = Object.assign(Object.assign({}, allPrivateKeys), validatedRecovered);
                    }
                }
            }
            return Object.fromEntries(Object.entries(allPrivateKeys).filter(([keyFp]) => !knownKeysFpSet.has(keyFp)));
        });
    }
    /*TODO?
     * Ask access back suggestion: if by getting access back to an exchange key with another data owner I may recover additional keys we could suggest
     * to ask for access back to that data owner.
     */
    recoverFromShamirSplitKeys(dataOwner, allKeys, missingKeysFp) {
        return __awaiter(this, void 0, void 0, function* () {
            if (dataOwner.dataOwner.publicKey &&
                dataOwner.dataOwner.privateKeyShamirPartitions &&
                Object.keys(dataOwner.dataOwner.privateKeyShamirPartitions).length > 0) {
                const selfId = dataOwner.dataOwner.id;
                const shamirSplits = {
                    [(0, utils_2.fingerprintV1)(dataOwner.dataOwner.publicKey)]: dataOwner.dataOwner.privateKeyShamirPartitions,
                };
                const delegatesOfSplits = Array.from(new Set(Object.entries(shamirSplits).flatMap(([splitKeyFp, splitKeyData]) => (missingKeysFp.has(splitKeyFp) ? Object.keys(splitKeyData) : []))));
                const exchangeKeys = {};
                for (const delegate of delegatesOfSplits) {
                    exchangeKeys[delegate] = yield this.getExchangeKeys(selfId, delegate, allKeys);
                }
                return yield this.recoverWithSplits(dataOwner, shamirSplits, exchangeKeys);
            }
            else
                return {};
        });
    }
    recoverWithSplits(dataOwner, splits, exchangeKeys) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            const res = {};
            const keysByFp = (0, utils_2.fingerprintToPublicKeysMapOf)(dataOwner.dataOwner, RSA_1.ShaVersion.Sha1);
            const keysByFpWithSha256 = (0, utils_2.fingerprintToPublicKeysMapOf)(dataOwner.dataOwner, RSA_1.ShaVersion.Sha256);
            for (const [fp, split] of Object.entries(splits)) {
                const pub = (_a = keysByFp[fp]) !== null && _a !== void 0 ? _a : keysByFpWithSha256[fp];
                const shaVersion = !!pub && !!keysByFp[fp] ? RSA_1.ShaVersion.Sha1 : !!pub && !!keysByFpWithSha256[fp] ? RSA_1.ShaVersion.Sha256 : undefined;
                if (!!pub && !!shaVersion) {
                    const recovered = yield this.tryRecoverSplitPrivate(split, exchangeKeys, shaVersion);
                    if (recovered) {
                        const pub = keysByFp[fp];
                        try {
                            res[fp] = {
                                privateKey: recovered,
                                publicKey: yield this.primitives.RSA.importKey('spki', (0, utils_1.ua2ab)((0, utils_1.hex2ua)(pub)), ['encrypt'], shaVersion),
                            };
                        }
                        catch (e) {
                            console.warn(`Failed to import public key ${pub}`, e);
                        }
                    }
                }
                else {
                    console.warn(`Missing public key for fingerprint ${fp} of recovered shamir key.`);
                }
            }
            return res;
        });
    }
    tryRecoverSplitPrivate(split, exchangeKeys, shaVersion) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            const splitsCount = Object.keys(split).length;
            if (splitsCount === 1) {
                // Not sure about the key nor if the key is accessible
                const [delegate, encryptedKey] = Object.entries(split)[0];
                for (const exchangeKey of (_a = exchangeKeys[delegate]) !== null && _a !== void 0 ? _a : []) {
                    try {
                        const decrypted = yield this.primitives.AES.decrypt(exchangeKey, (0, utils_1.hex2ua)(encryptedKey));
                        const importedKey = yield this.primitives.RSA.importKey('pkcs8', decrypted, ['decrypt'], shaVersion);
                        if (importedKey)
                            return importedKey;
                    }
                    catch (e) { }
                }
                return undefined;
            }
            else {
                const decryptedSplits = [];
                for (const delegateAndEncryptedSplit of Object.entries(split)) {
                    const decrypted = yield this.tryDecryptSplitPiece(delegateAndEncryptedSplit, exchangeKeys, splitsCount);
                    if (decrypted)
                        decryptedSplits.push((0, utils_1.ua2hex)(decrypted).slice(1)); // Drop padding
                }
                try {
                    const combinedKey = (0, utils_1.hex2ua)(this.primitives.shamir.combine(decryptedSplits));
                    return yield this.primitives.RSA.importKey('pkcs8', (0, utils_1.ua2ab)(combinedKey), ['decrypt'], shaVersion);
                }
                catch (e) {
                    // Could be not enough splits decrypted
                    return undefined;
                }
            }
        });
    }
    tryDecryptSplitPiece(delegateAndEncryptedSplit, exchangeKeys, splitsCount) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            const [delegate, encryptedPiece] = delegateAndEncryptedSplit;
            for (const exchangeKey of (_a = exchangeKeys[delegate]) !== null && _a !== void 0 ? _a : []) {
                try {
                    const decrypted = yield this.primitives.AES.decrypt(exchangeKey, (0, utils_1.hex2ua)(encryptedPiece));
                    if (this.validateDecryptedSplit(decrypted, splitsCount))
                        return decrypted;
                }
                catch (e) { }
            }
            return undefined;
        });
    }
    validateDecryptedSplit(decryptedSplit, splitsCount) {
        // Normally shamir split starts with 8 followed by the index of the split in hexadecimal.
        // However, we pad with an extra 'f' at the beginning
        const decryptedHex = (0, utils_1.ua2hex)(decryptedSplit);
        if (decryptedHex[0] !== 'f' || decryptedHex[1] !== '8')
            return false;
        try {
            const splitIndex = parseInt(decryptedHex.slice(2, 4), 16);
            return splitIndex > 0 && splitIndex <= splitsCount;
        }
        catch (e) {
            return false;
        }
    }
    recoverFromTransferKeys(dataOwner, allKeys, missingKeysFp) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            const publicKeyFingerprintToPublicKey = Object.assign(Object.assign({}, (0, utils_2.fingerprintToPublicKeysMapOf)(dataOwner.dataOwner, RSA_1.ShaVersion.Sha1)), (0, utils_2.fingerprintToPublicKeysMapOf)(dataOwner.dataOwner, RSA_1.ShaVersion.Sha256));
            const missingKeysTransferData = {};
            Object.values((_a = dataOwner.dataOwner.transferKeys) !== null && _a !== void 0 ? _a : {}).forEach((transferKeysByEncryptor) => {
                Object.entries(transferKeysByEncryptor).forEach(([transferToPublicKey, transferPrivateKeyEncrypted]) => {
                    const transferToPublicKeyFp = (0, utils_2.fingerprintV1)(transferToPublicKey); // We are not sure if transfer public key will be a fp or not
                    if (missingKeysFp.has(transferToPublicKeyFp)) {
                        const existingEntryValue = missingKeysTransferData[transferToPublicKeyFp];
                        if (existingEntryValue) {
                            existingEntryValue.encryptedPrivateKey.add(transferPrivateKeyEncrypted);
                        }
                        else {
                            const fullPublicKey = publicKeyFingerprintToPublicKey[transferToPublicKeyFp];
                            if (fullPublicKey) {
                                missingKeysTransferData[transferToPublicKeyFp] = {
                                    publicKey: fullPublicKey,
                                    encryptedPrivateKey: new Set([transferPrivateKeyEncrypted]),
                                };
                            }
                            else {
                                console.warn('Invalid data owner: there is a transfer key for an unknown public key');
                            }
                        }
                    }
                });
            });
            const availableExchangeKeys = yield this.getExchangeKeys(dataOwner.dataOwner.id, dataOwner.dataOwner.id, allKeys);
            return Object.entries(missingKeysTransferData).reduce((acc_1, _a) => __awaiter(this, [acc_1, _a], void 0, function* (acc, [recoverableKeyPubFp, transferData]) {
                const awaitedAcc = yield acc;
                const shaVersion = (0, utils_2.getShaVersionForKey)(dataOwner.dataOwner, transferData.publicKey);
                if (!!shaVersion) {
                    const decryptedTransferData = yield this.tryDecryptTransferData(transferData, availableExchangeKeys, shaVersion);
                    if (decryptedTransferData != undefined) {
                        return Object.assign(Object.assign({}, awaitedAcc), { [recoverableKeyPubFp]: decryptedTransferData });
                    }
                    else
                        return awaitedAcc;
                }
                else
                    return awaitedAcc;
            }), Promise.resolve({}));
        });
    }
    tryDecryptTransferData(transferData, availableExchangeKeys, shaVersion) {
        return __awaiter(this, void 0, void 0, function* () {
            for (const encryptedTransferKey of transferData.encryptedPrivateKey) {
                const decryptedTransferKey = yield this.tryDecryptTransferKey(encryptedTransferKey, availableExchangeKeys, shaVersion);
                if (decryptedTransferKey != undefined)
                    return {
                        privateKey: decryptedTransferKey,
                        publicKey: yield this.primitives.RSA.importKey('spki', (0, utils_1.ua2ab)((0, utils_1.hex2ua)(transferData.publicKey)), ['encrypt'], shaVersion),
                    };
            }
            return undefined;
        });
    }
    // attempt to decrypt a transfer key in pkcs8 using any of the provided exchange keys
    tryDecryptTransferKey(encryptedTransferKey, // in hex format
    exchangeKeys, shaVersion) {
        return __awaiter(this, void 0, void 0, function* () {
            const encryptedKeyBytes = (0, utils_1.hex2ua)(encryptedTransferKey);
            for (const exchangeKey of exchangeKeys) {
                try {
                    const decryptedKeyData = yield this.primitives.AES.decrypt(exchangeKey, encryptedKeyBytes);
                    const importedPrivateKey = yield this.primitives.RSA.importPrivateKey('pkcs8', decryptedKeyData, shaVersion);
                    if (importedPrivateKey != undefined)
                        return importedPrivateKey;
                }
                catch (e) {
                    /* failure is a valid possibility: we don't know the correct key to use */
                }
            }
            return undefined;
        });
    }
    // Get exchange keys from aes exchange keys / hc party keys and exchange data
    getExchangeKeys(from, to, availableDecryptionKeys) {
        return __awaiter(this, void 0, void 0, function* () {
            const aesExchangeKeys = yield this.baseExchangeKeysManager.tryDecryptExchangeKeys(yield this.baseExchangeKeysManager.getEncryptedExchangeKeysFor(from, to), availableDecryptionKeys);
            const decryptedExchangeDataKeys = yield this.baseExchangeDataManager.tryDecryptExchangeKeys(yield this.baseExchangeDataManager.getExchangeDataByDelegatorDelegatePair(from, to), availableDecryptionKeys);
            return [...aesExchangeKeys.successfulDecryptions, ...decryptedExchangeDataKeys.successfulDecryptions];
        });
    }
}
exports.KeyRecovery = KeyRecovery;
//# sourceMappingURL=KeyRecovery.js.map