"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.UserEncryptionKeysManager = void 0;
const RSA_1 = require("./RSA");
const utils_1 = require("../utils");
const utils_2 = require("./utils");
const CryptoActorStub_1 = require("../../icc-api/model/CryptoActorStub");
const nothingKeyRecovererAndVerifier = (x) => Promise.resolve(x.reduce((acc, { dataOwner }) => (Object.assign(Object.assign({}, acc), { [dataOwner.dataOwner.id]: { recoveredKeys: {}, keyAuthenticity: {} } })), {}));
/**
 * Allows to manage public and private keys for the current user and his parent hierarchy.
 */
class UserEncryptionKeysManager {
    constructor(primitives, dataOwnerApi, icureStorage, keyRecovery, strategies, initialiseParentKeys, keypairRecoverer, selfIsAnonymous) {
        this.primitives = primitives;
        this.dataOwnerApi = dataOwnerApi;
        this.icureStorage = icureStorage;
        this.keyRecovery = keyRecovery;
        this.strategies = strategies;
        this.initialiseParentKeys = initialiseParentKeys;
        this.keypairRecoverer = keypairRecoverer;
        this.selfIsAnonymous = selfIsAnonymous;
        this.keysCache = undefined;
    }
    /**
     * @internal
     * Get all key pairs available for the current data owner and his parents.
     * @return an object with:
     * - `self` an object containing the current data owner id and the list of key pairs available for the current data owner with verification details.
     * - `parents` the list of parents to the current data owner with the list of key pairs available for each parent. The list is ordered from the
     *   topmost ancestor (at index 0) to the direct parent of the current data owner (at the last index, may be 0).
     */
    getCurrentUserHierarchyAvailableKeypairs() {
        return __awaiter(this, void 0, void 0, function* () {
            this.ensureInitialised();
            const hierarchy = yield this.dataOwnerApi.getCurrentDataOwnerHierarchyIds();
            const selfId = hierarchy[hierarchy.length - 1];
            const selfKeys = Object.values(this.keysCache[selfId]).map(({ pair, isVerified, isDevice }) => ({ pair, verified: isVerified || isDevice }));
            const remainingHierarchy = hierarchy.slice(0, hierarchy.length - 1);
            return {
                self: {
                    dataOwnerId: selfId,
                    keys: selfKeys,
                },
                parents: remainingHierarchy.map((x) => ({
                    dataOwnerId: x,
                    keys: Object.values(this.keysCache[x]).map(({ pair, isVerified, isDevice }) => ({ pair, verified: isVerified || isDevice })),
                })),
            };
        });
    }
    /**
     * Get the public keys of available key pairs for the current user in hex-encoded spki representation (uses cached keys: no request is done to the
     * server).
     * By setting {@link verifiedOnly} to true only the public keys for verified key pairs will be returned: these will include only key pairs created
     * on this device or which have been verified using {@link CryptoStrategies} on this device.
     * @param verifiedOnly if true only the verified public keys will be returned.
     * @return the spki representation of public keys of available keypairs for the current user.
     */
    getCurrentUserAvailablePublicKeysHex(verifiedOnly) {
        return __awaiter(this, void 0, void 0, function* () {
            this.ensureInitialised();
            let selectedData = Object.values(this.keysCache[this.selfId]);
            if (verifiedOnly) {
                selectedData = selectedData.filter((data) => data.isVerified || data.isDevice);
            }
            return yield Promise.all(selectedData.map((keyData) => __awaiter(this, void 0, void 0, function* () { return (0, utils_1.ua2hex)(yield this.primitives.RSA.exportKey(keyData.pair.publicKey, 'spki')); })));
        });
    }
    /**
     * Get the public keys of available key pairs for the current user and his parents in hex-encoded spki representation (uses cached keys: no request
     * is done to the server).
     * Note that this will also include unverified keys.
     * @return the spki representation of public keys of available keypairs for the current user.
     */
    getCurrentUserHierarchyAvailablePublicKeysHex() {
        return __awaiter(this, void 0, void 0, function* () {
            return yield Promise.all(Object.values(this.keysCache)
                .flatMap((pairsForParent) => Object.values(pairsForParent))
                .map((keyPairData) => __awaiter(this, void 0, void 0, function* () { return (0, utils_1.ua2hex)(yield this.primitives.RSA.exportKey(keyPairData.pair.publicKey, 'spki')); })));
        });
    }
    /**
     * Get a key pair with the provided fingerprint if present.
     * @param fingerprint a key-pair/public-key fingerprint
     * @return the pair associated to the fingerprint and a boolean indicating if the pair is verified, if present, else undefined
     */
    getKeyPairForFingerprint(fingerprint) {
        const foundVerified = this.getSelfVerifiedKeys().find((x) => x.fingerprint === fingerprint);
        if (foundVerified)
            return { pair: foundVerified.pair, verified: true };
        const foundOther = this.getDecryptionKeys()[fingerprint];
        if (foundOther)
            return { pair: foundOther, verified: false };
        return undefined;
    }
    /**
     * @internal This method is intended for internal use only and may be changed without notice.
     * Initializes all keys for the current data owner. This method needs to be called before any other method of this class can be used.
     * @throws if the current user is not a data owner, or if there is no key and no new key could be created according to this manager crypt
     * strategies.
     * @return if a new key was created during initialisation the newly created key, else undefined.
     */
    initialiseKeys() {
        return __awaiter(this, void 0, void 0, function* () {
            const newKey = yield this.doLoadKeys((x) => this.strategies.recoverAndVerifySelfHierarchyKeys(x, this.primitives, this.keypairRecoverer), (x) => this.strategies.generateNewKeyForDataOwner(x, this.primitives));
            return newKey ? { newKeyPair: newKey.pair, newKeyFingerprint: newKey.fingerprint } : undefined;
        });
    }
    /**
     * @internal This method is intended for internal use only and may be changed without notice.
     * Forces to reload keys for the current data owner. This could be useful if the data owner has logged in from another device in order to update the
     * transfer keys.
     * This method will complete only after keys have been reloaded successfully.
     */
    reloadKeys() {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.doLoadKeys(nothingKeyRecovererAndVerifier, (x) => {
                throw new Error("Can't create new keys at reload time: it should have already been created on initialisation");
            });
        });
    }
    /**
     * @internal This method is intended for internal use only and may be changed without notice.
     * Get all verified key pairs for the current data owner which can safely be used for encryption. This includes all key pairs created on the current
     * device and all recovered key pairs which have been verified.
     * The keys returned by this method will be in the following order:
     * 1. Legacy key pair if it is verified
     * 2. All device key pais, in alphabetical order according to the fingerprint
     * 3. Other verified key pairs, in alphabetical order according to the fingerprint
     * @return all verified keys, in order.
     */
    getSelfVerifiedKeys() {
        this.ensureInitialised();
        const selfKeys = this.keysCache[this.selfId];
        const allKeysEntries = Object.entries(selfKeys);
        const legacyKeyFp = this.selfLegacyPublicKey && (0, utils_2.fingerprintV1)(this.selfLegacyPublicKey);
        const legacyKeyData = legacyKeyFp ? selfKeys[legacyKeyFp] : undefined;
        const legacyEntry = (legacyKeyData === null || legacyKeyData === void 0 ? void 0 : legacyKeyData.isVerified) && legacyKeyFp ? [{ fingerprint: legacyKeyFp, pair: legacyKeyData.pair }] : [];
        function filteredEntries(filterFunction) {
            return allKeysEntries
                .filter(([fp, data]) => filterFunction(fp, data) && fp !== legacyKeyFp)
                .sort(([a], [b]) => {
                // need to make sure that the comparison is independent of the locale, but the actual ordering is not that important
                return a == b ? 0 : a > b ? 1 : -1;
            })
                .map(([fingerprint, { pair }]) => ({ fingerprint, pair }));
        }
        return [...legacyEntry, ...filteredEntries((_, data) => data.isDevice), ...filteredEntries((_, data) => !data.isDevice && data.isVerified)];
    }
    /**
     * Get all verified keys for a member of the current data owner hierarchy in no particular order.
     * @param dataOwner the current data owner or a member of his hierarchy.
     * @throws if the provided data owner is not part of the current data owner hierarchy
     */
    getVerifiedPublicKeysFor(dataOwner) {
        return __awaiter(this, void 0, void 0, function* () {
            this.ensureInitialised();
            const availableKeys = this.keysCache[dataOwner.id];
            if (!availableKeys)
                throw new Error(`Data owner ${dataOwner.id} is not part of the hierarchy of the current data owner ${this.selfId}`);
            const availableVerifiedKeysFp = new Set(Object.entries(availableKeys).flatMap(([fp, info]) => (info.isVerified || info.isDevice ? [fp] : [])));
            const otherVerifiedFp = new Set(Object.entries(yield this.icureStorage.loadSelfVerifiedKeys(dataOwner.id)).flatMap(([fp, verified]) => (verified ? [fp] : [])));
            return Array.from([
                ...this.dataOwnerApi.getHexPublicKeysWithSha1Of(dataOwner),
                ...this.dataOwnerApi.getHexPublicKeysWithSha256Of(dataOwner),
            ]).filter((key) => {
                const fp = (0, utils_2.fingerprintV1)(key);
                return availableVerifiedKeysFp.has(fp) || otherVerifiedFp.has(fp);
            });
        });
    }
    /**
     * @internal This method is intended for internal use only and may be changed without notice.
     * Get all key pairs for the current data owner and his parents. These keys should be used only for decryption as they may have not been verified.
     * @return all key pairs available for decryption.
     */
    getDecryptionKeys() {
        this.ensureInitialised();
        return Object.values(this.keysCache).reduce((acc, curr) => {
            return Object.assign(Object.assign({}, acc), this.plainKeysByFingerprint(curr));
        }, {});
    }
    doLoadKeys(keyRecovererAndVerifier, currentOwnerKeyGenerator) {
        return __awaiter(this, void 0, void 0, function* () {
            // Load all keys for self from key store
            const hierarchy = (yield this.dataOwnerApi.getCurrentDataOwnerHierarchy()).map(({ dataOwner, type }) => {
                var _a;
                if (dataOwner.publicKey == '') {
                    if (Object.entries((_a = dataOwner.hcPartyKeys) !== null && _a !== void 0 ? _a : {}).length > 0)
                        throw new Error(`Data owner ${dataOwner.id} has "" as public key but has non-empty hcPartyKeys`);
                    delete dataOwner.publicKey;
                }
                return {
                    dataOwner,
                    type,
                };
            });
            const self = hierarchy[hierarchy.length - 1];
            this.selfId = self.dataOwner.id;
            const keysData = [];
            const toInit = this.initialiseParentKeys ? hierarchy : [self];
            for (let i = 0; i < toInit.length; i++) {
                const dowt = toInit[i];
                let parentKeysForRecovery = {};
                for (let j = 0; j < i; j++) {
                    parentKeysForRecovery = Object.assign(Object.assign({}, keysData[j].availableKeys), parentKeysForRecovery);
                }
                const availableKeys = yield this.loadAndRecoverKeysFor(dowt, parentKeysForRecovery);
                const availableKeysFpSet = new Set(Object.keys(availableKeys));
                const verifiedKeysMap = yield this.icureStorage.loadSelfVerifiedKeys(dowt.dataOwner.id);
                const allPublicKeys = new Set([
                    ...this.dataOwnerApi.getHexPublicKeysWithSha1Of(dowt.dataOwner),
                    ...this.dataOwnerApi.getHexPublicKeysWithSha256Of(dowt.dataOwner),
                ]);
                const unavailableKeys = Array.from(allPublicKeys).flatMap((key) => {
                    return availableKeysFpSet.has((0, utils_2.fingerprintV1)(key)) ? [] : [key];
                });
                const unknownKeys = Array.from(allPublicKeys).filter((x) => { var _a; return !((0, utils_2.fingerprintV1)(x) in verifiedKeysMap) && !(((_a = availableKeys === null || availableKeys === void 0 ? void 0 : availableKeys[(0, utils_2.fingerprintV1)(x)]) === null || _a === void 0 ? void 0 : _a.isDevice) === true); });
                keysData.push({ dowt, availableKeys, unavailableKeys, unknownKeys });
            }
            const recoveryInfo = keysData.map(({ dowt, unavailableKeys, unknownKeys }) => ({ dataOwner: dowt, unavailableKeys, unknownKeys }));
            const recoveryAndVerificationResult = recoveryInfo.some((x) => x.unavailableKeys.length > 0 || x.unknownKeys.length > 0)
                ? yield keyRecovererAndVerifier(recoveryInfo)
                : yield nothingKeyRecovererAndVerifier(recoveryInfo);
            const keysCache = {};
            for (let i = 0; i < keysData.length; i++) {
                const keyData = keysData[i];
                const currAuthenticity = this.ensureFingerprintKeys(recoveryAndVerificationResult[keyData.dowt.dataOwner.id].keyAuthenticity);
                const currExternallyRecovered = this.ensureFingerprintKeys(recoveryAndVerificationResult[keyData.dowt.dataOwner.id].recoveredKeys);
                for (const [fp, keyPair] of Object.entries(currExternallyRecovered)) {
                    const jwkPair = yield this.primitives.RSA.exportKeys(keyPair, 'jwk', 'jwk');
                    yield this.icureStorage.saveKey(keyData.dowt.dataOwner.id, fp, jwkPair, currAuthenticity[fp] != undefined ? currAuthenticity[fp] : true);
                }
                const updatedVerifiedMap = yield this.icureStorage.saveSelfVerifiedKeys(keyData.dowt.dataOwner.id, [...Object.keys(currAuthenticity), ...Object.keys(currExternallyRecovered)].reduce((acc, currFp) => (Object.assign(Object.assign({}, acc), { [currFp]: currAuthenticity[currFp] != undefined ? currAuthenticity[currFp] : currFp in currExternallyRecovered ? true : undefined })), {}));
                const keysWithExternallyRecovered = Object.assign(Object.assign({}, keyData.availableKeys), Object.fromEntries(Object.entries(currExternallyRecovered).map(([k, v]) => [k, { pair: v, isDevice: false }])));
                let parentKeysForRecovery = {};
                for (let j = 0; j < i; j++) {
                    parentKeysForRecovery = Object.assign(Object.assign({}, keysCache[keysData[j].dowt.dataOwner.id]), parentKeysForRecovery);
                }
                const additionallyRecovered = yield this.recoverAndCacheKeys(keyData.dowt, this.plainKeysByFingerprint(Object.assign(Object.assign({}, keysWithExternallyRecovered), parentKeysForRecovery)));
                const keys = Object.assign(Object.assign({}, keysWithExternallyRecovered), Object.fromEntries(Object.entries(additionallyRecovered).map(([k, v]) => [k, { pair: v, isDevice: false }])));
                keysCache[keyData.dowt.dataOwner.id] = this.verifyKeys(keys, updatedVerifiedMap);
            }
            if (Object.entries(keysCache).some(([ownerId, data]) => ownerId !== self.dataOwner.id && !this.hasVerifiedKey(data))) {
                throw new Error('Some parent hcps have no verified keys: impossible to generate locally a new key for a parent.');
            }
            else if (this.hasVerifiedKey(keysCache[self.dataOwner.id])) {
                this.keysCache = keysCache;
                this.selfLegacyPublicKey = self.dataOwner.publicKey;
                return undefined;
            }
            else {
                const whatToDo = yield currentOwnerKeyGenerator(self);
                if (whatToDo === false) {
                    throw new Error(`No verified key found for ${self.dataOwner.id} and settings do not allow creation of a new key.`);
                }
                else if (typeof whatToDo == 'string' && whatToDo == 'keyless') {
                    /*
                     * Currently non-anonymous data owners aren't allowed to use keyless mode.
                     * Today the delegator is not emitted in views and is not used for access control: an entity without delegation
                     * self->self but only a delegation self->other would not be searchable by nor accessible to an explicit data
                     * owner.
                     * This is not a problem for anonymous data owner, since search and access control use the secure delegation
                     * key.
                     * Since keyless api was designed for use by patients this is not an issue at the time, but if in future we need
                     * to support keyless api for explicit data owner before updating views we can add stubs to legacy delegations.
                     */
                    if (!this.selfIsAnonymous)
                        throw new Error('Keyless api mode is available only for anonymous data owners.');
                    this.keysCache = keysCache;
                    return undefined;
                }
                else {
                    const updateInfo = yield this.createAndSaveNewKeyPair(whatToDo === true ? undefined : whatToDo, self);
                    // self may be outdated now
                    this.selfLegacyPublicKey = updateInfo.updatedSelf.stub.publicKey;
                    this.keysCache = Object.assign(Object.assign({}, keysCache), { [self.dataOwner.id]: Object.assign(Object.assign({}, keysCache[self.dataOwner.id]), { [updateInfo.publicKeyFingerprint]: { pair: updateInfo.keyPair, isDevice: true, isVerified: true } }) });
                    return { pair: updateInfo.keyPair, fingerprint: updateInfo.publicKeyFingerprint };
                }
            }
        });
    }
    createAndSaveNewKeyPair(importedKeyPair, selfDataOwner) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            const keyPair = importedKeyPair !== null && importedKeyPair !== void 0 ? importedKeyPair : (yield this.primitives.RSA.generateKeyPair(RSA_1.ShaVersion.Sha256));
            const publicKeyHex = (0, utils_1.ua2hex)(yield this.primitives.RSA.exportKey(keyPair.publicKey, 'spki'));
            const jwKey = yield this.primitives.RSA.exportKey(keyPair.publicKey, 'jwk');
            if (jwKey.alg !== 'RSA-OAEP-256') {
                throw new Error('Only keys generated with SHA-256 are currently supported');
            }
            const publicKeyFingerprint = (0, utils_2.fingerprintV1)(publicKeyHex);
            yield this.icureStorage.saveKey(selfDataOwner.dataOwner.id, publicKeyFingerprint, yield this.primitives.RSA.exportKeys(keyPair, 'jwk', 'jwk'), true);
            yield this.icureStorage.saveSelfVerifiedKeys(selfDataOwner.dataOwner.id, { [publicKeyFingerprint]: true });
            const updatedSelf = yield this.dataOwnerApi.modifyCryptoActorStub({
                stub: Object.assign(Object.assign({}, CryptoActorStub_1.CryptoActorStub.fromDataOwner(selfDataOwner.dataOwner)), { publicKeysForOaepWithSha256: [...((_a = selfDataOwner.dataOwner.publicKeysForOaepWithSha256) !== null && _a !== void 0 ? _a : []), publicKeyHex] }),
                type: selfDataOwner.type,
            });
            return { publicKeyFingerprint, keyPair: keyPair, updatedSelf };
        });
    }
    loadStoredKeys(dataOwner, pubKeysFingerprints) {
        return __awaiter(this, void 0, void 0, function* () {
            const pubKeysFingerprintsWithVersion = Object.entries(pubKeysFingerprints).flatMap(([shaVersion, fps]) => {
                return fps.map((fp) => [shaVersion, fp]);
            });
            return yield pubKeysFingerprintsWithVersion.reduce((acc_1, _a) => __awaiter(this, [acc_1, _a], void 0, function* (acc, [shaVersion, currentFingerprint]) {
                const awaitedAcc = yield acc;
                let loadedPair = undefined;
                try {
                    const storedKeypair = yield this.icureStorage.loadKey(dataOwner.dataOwner.id, currentFingerprint, dataOwner.dataOwner.publicKey);
                    if (!!storedKeypair) {
                        const importedKey = yield this.primitives.RSA.importKeyPair('jwk', storedKeypair.pair.privateKey, 'jwk', storedKeypair.pair.publicKey, shaVersion);
                        loadedPair = { pair: importedKey, isDevice: storedKeypair.isDevice };
                    }
                }
                catch (e) {
                    console.warn('Error while loading keypair', currentFingerprint, e);
                }
                return loadedPair
                    ? Object.assign(Object.assign({}, awaitedAcc), { [currentFingerprint]: loadedPair }) : awaitedAcc;
            }), Promise.resolve({}));
        });
    }
    verifyKeys(keys, verifiedKeysMap) {
        return Object.fromEntries(Object.entries(keys).map(([fp, keyData]) => [fp, Object.assign(Object.assign({}, keyData), { isVerified: keyData.isDevice || (verifiedKeysMap === null || verifiedKeysMap === void 0 ? void 0 : verifiedKeysMap[fp]) === true })]));
    }
    plainKeysByFingerprint(richKeys) {
        return Object.fromEntries(Object.entries(richKeys).map(([fp, keyData]) => [fp, keyData.pair]));
    }
    ensureInitialised() {
        if (!this.keysCache)
            throw new Error('Key manager was not properly initialised');
    }
    recoverAndCacheKeys(dataOwner, availableKeys) {
        return __awaiter(this, void 0, void 0, function* () {
            const recoveredKeys = yield this.keyRecovery.recoverKeys(dataOwner, availableKeys);
            for (const [fp, pair] of Object.entries(recoveredKeys)) {
                yield this.icureStorage.saveKey(dataOwner.dataOwner.id, fp, yield this.primitives.RSA.exportKeys(pair, 'jwk', 'jwk'), false);
            }
            return recoveredKeys;
        });
    }
    loadAndRecoverKeysFor(dataOwner, ancestorsKeys) {
        return __awaiter(this, void 0, void 0, function* () {
            const pubKeysFingerprints = {
                'sha-1': [...new Set(this.dataOwnerApi.getHexPublicKeysWithSha1Of(dataOwner.dataOwner))].map((x) => (0, utils_2.fingerprintV1)(x)),
                'sha-256': [...new Set(this.dataOwnerApi.getHexPublicKeysWithSha256Of(dataOwner.dataOwner))].map((x) => (0, utils_2.fingerprintV1)(x)),
            };
            const loadedKeys = pubKeysFingerprints['sha-1'].length + pubKeysFingerprints['sha-256'].length > 0 ? yield this.loadStoredKeys(dataOwner, pubKeysFingerprints) : {};
            const loadedKeysFingerprints = Object.keys(loadedKeys);
            if (loadedKeysFingerprints.length !== pubKeysFingerprints['sha-1'].length + pubKeysFingerprints['sha-256'].length &&
                (loadedKeysFingerprints.length > 0 || Object.keys(ancestorsKeys).length > 0)) {
                const recoveredKeys = yield this.recoverAndCacheKeys(dataOwner, this.plainKeysByFingerprint(Object.assign(Object.assign({}, loadedKeys), ancestorsKeys)));
                for (const [fp, pair] of Object.entries(recoveredKeys)) {
                    loadedKeys[fp] = { pair, isDevice: false };
                }
            }
            return loadedKeys;
        });
    }
    ensureFingerprintKeys(obj) {
        return Object.fromEntries(Object.entries(obj).map(([k, v]) => [(0, utils_2.fingerprintV1)(k), v]));
    }
    hasVerifiedKey(keysData) {
        return Object.values(keysData).some((x) => x.isVerified || x.isDevice);
    }
}
exports.UserEncryptionKeysManager = UserEncryptionKeysManager;
//# sourceMappingURL=UserEncryptionKeysManager.js.map