"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.TransferKeysManager = void 0;
const RSA_1 = require("./RSA");
const utils_1 = require("../utils");
const graph_utils_1 = require("../utils/graph-utils");
const utils_2 = require("./utils");
/**
 * @internal this class is intended only for internal use and may be changed without notice.
 * Allows to create new transfer keys.
 */
class TransferKeysManager {
    constructor(primitives, baseExchangeDataManager, dataOwnerApi, encryptionKeysManager, icureStorage) {
        this.primitives = primitives;
        this.baseExchangeDataManager = baseExchangeDataManager;
        this.dataOwnerApi = dataOwnerApi;
        this.encryptionKeysManager = encryptionKeysManager;
        this.icureStorage = icureStorage;
    }
    /**
     * Analyses the transfer keys graph and creates new transfer keys which allow to improve data accessibility from other devices.
     * For security reasons transfer keys will only be created between keys verified by the user, but this will be done ignoring any existing edges from
     * unverified keys to verified keys.
     * @param self the current data owner.
     * @return the updated data owner.
     */
    updateTransferKeys(self) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            const newEdgesByTarget = yield this.getNewVerifiedTransferKeysEdges(self);
            if (!newEdgesByTarget.length)
                return;
            const selfId = self.stub.id;
            const fpToPublicKey = (0, utils_2.fingerprintToPublicKeysMapOf)(self.stub, RSA_1.ShaVersion.Sha1);
            const fpToPublicKeyWithSha256 = (0, utils_2.fingerprintToPublicKeysMapOf)(self.stub, RSA_1.ShaVersion.Sha256);
            const verifiedFps = new Set(this.encryptionKeysManager.getSelfVerifiedKeys().map((x) => x.fingerprint));
            const allVerifiedSourcesAndTarget = Array.from(new Set(newEdgesByTarget.flatMap((x) => 
            // Sources are guaranteed to be verified, but target may not be
            verifiedFps.has(x.targetFp) ? [x.targetFp, ...x.sources] : x.sources)));
            const newExchangeKeyPublicKeys = allVerifiedSourcesAndTarget.map((fp) => fpToPublicKey[fp]).filter((key) => !!key);
            const newExchangeKeyPublicKeysWithSha256 = allVerifiedSourcesAndTarget.map((fp) => fpToPublicKeyWithSha256[fp]).filter((key) => !!key);
            const createdExchangeData = yield this.baseExchangeDataManager.createExchangeData(selfId, Object.fromEntries(this.encryptionKeysManager.getSelfVerifiedKeys().map((x) => [x.fingerprint, x.pair.privateKey])), Object.assign(Object.assign({}, (yield (0, utils_2.loadPublicKeys)(this.primitives.RSA, newExchangeKeyPublicKeys, RSA_1.ShaVersion.Sha1))), (yield (0, utils_2.loadPublicKeys)(this.primitives.RSA, newExchangeKeyPublicKeysWithSha256, RSA_1.ShaVersion.Sha256))));
            let updatedTransferKeys = (_a = self.stub.transferKeys) !== null && _a !== void 0 ? _a : {};
            for (const newEdges of newEdgesByTarget) {
                const encryptedTransferKey = yield this.encryptTransferKey(newEdges.target, createdExchangeData.exchangeKey);
                updatedTransferKeys = newEdges.sources.reduce((acc, candidateFp) => {
                    var _a;
                    const existingKeys = Object.assign({}, ((_a = acc[candidateFp]) !== null && _a !== void 0 ? _a : {}));
                    existingKeys[newEdges.targetFp] = encryptedTransferKey;
                    acc[candidateFp] = existingKeys;
                    return acc;
                }, updatedTransferKeys);
            }
            yield this.dataOwnerApi.modifyCryptoActorStub({
                stub: Object.assign(Object.assign({}, self.stub), { transferKeys: updatedTransferKeys }),
                type: self.type,
            });
        });
    }
    // encrypts a transfer key in pkcs8 format using an exchange key, returns the hex representation
    encryptTransferKey(transferKey, exchangeKey) {
        return __awaiter(this, void 0, void 0, function* () {
            const exportedKey = yield this.primitives.RSA.exportKey(transferKey.privateKey, 'pkcs8');
            return (0, utils_1.ua2hex)(yield this.primitives.AES.encrypt(exchangeKey, exportedKey));
        });
    }
    // all public keys would go to the stored keys -> no need for specifying the key.
    // Result only includes the acyclic label, not the full group
    transferKeysCandidatesFp(keyToFp, graph) {
        return Object.entries((0, graph_utils_1.reachSetsAcyclic)(graph.acyclicGraph))
            .filter(([from, reachable]) => {
            var _a;
            const currGroup = (_a = graph.acyclicLabelToGroup[from]) !== null && _a !== void 0 ? _a : [from];
            const reachableOriginal = new Set(Array.from(reachable).flatMap((x) => { var _a; return (_a = graph.acyclicLabelToGroup[x]) !== null && _a !== void 0 ? _a : [x]; }));
            return !currGroup.includes(keyToFp) && !reachableOriginal.has(keyToFp);
        })
            .map((x) => x[0]);
    }
    transferTargetKeys(keyManager, graph) {
        return __awaiter(this, void 0, void 0, function* () {
            const availableGroups = new Set(Object.keys(keyManager.getDecryptionKeys()).map((x) => { var _a; return (_a = graph.originalLabelToAcyclicLabel[x]) !== null && _a !== void 0 ? _a : x; }));
            const groupsReachableFromAvailable = new Set(Object.entries(graph.acyclicGraph).flatMap(([source, reachables]) => (availableGroups.has(source) ? Array.from(reachables) : [])));
            const targetCandidates = Array.from(availableGroups).filter((x) => !groupsReachableFromAvailable.has(x));
            const verifiedFps = new Set(keyManager.getSelfVerifiedKeys().map((x) => x.fingerprint));
            return targetCandidates.map((candidateFp) => {
                var _a, _b;
                const candidateGroup = (_a = graph.acyclicLabelToGroup[candidateFp]) !== null && _a !== void 0 ? _a : [candidateFp]; // May not be part of transfer keys graph yet
                const bestCandidateOfGroup = (_b = candidateGroup.find((x) => verifiedFps.has(x))) !== null && _b !== void 0 ? _b : candidateFp;
                return { fingerprint: bestCandidateOfGroup, pair: keyManager.getKeyPairForFingerprint(bestCandidateOfGroup).pair };
            });
        });
    }
    // Decides the best edges considering the verified public keys.
    getNewVerifiedTransferKeysEdges(self) {
        return __awaiter(this, void 0, void 0, function* () {
            const verifiedKeysFpSet = new Set(this.encryptionKeysManager.getSelfVerifiedKeys().map((x) => x.fingerprint));
            Object.entries(yield this.icureStorage.loadSelfVerifiedKeys(self.stub.id)).forEach(([key, verified]) => {
                if (verified)
                    verifiedKeysFpSet.add((0, utils_2.fingerprintV1)(key));
            });
            if (verifiedKeysFpSet.size == 0)
                return [];
            const graph = (0, utils_2.transferKeysFpGraphOf)(self.stub);
            // 1. Choose keys available in this device which should be reachable from all other verified keys
            const targetKeys = yield this.transferTargetKeys(this.encryptionKeysManager, graph);
            const res = [];
            for (const { fingerprint: targetKeyFp, pair: targetKey } of targetKeys) {
                // 2. Find groups which can't reach the existing target keys
                const candidatesFp = this.transferKeysCandidatesFp(targetKeyFp, graph);
                // 3. Keep only groups which contain at least a verified candidate
                const verifiedGroupCandidates = candidatesFp.filter((candidate) => graph.acyclicLabelToGroup[candidate].some((candidateGroupMember) => verifiedKeysFpSet.has(candidateGroupMember)));
                const verifiedCandidatesSet = new Set(verifiedGroupCandidates);
                if (verifiedCandidatesSet.size == 0)
                    continue;
                // 4. Drop all candidates which can already reach another candidate group: it is sufficient to create a transfer key from that group.
                const reachSets = (0, graph_utils_1.reachSetsAcyclic)(graph.acyclicGraph);
                const optimizedCandidates = verifiedGroupCandidates.filter((candidate) => Array.from(reachSets[candidate]).every((reachableNode) => !verifiedCandidatesSet.has(reachableNode)));
                if (optimizedCandidates.length == 0)
                    throw new Error('Check failed: at least one candidate should survive optimization');
                // 5. Transfer keys could also be faked: to make sure we are not giving access to unauthorised people we remap the candidates to a verified public
                // keys
                const verifiedOptimizedCandidates = optimizedCandidates.map((candidate) => {
                    const res = graph.acyclicLabelToGroup[candidate].find((groupMemberFp) => verifiedKeysFpSet.has(groupMemberFp));
                    if (!res)
                        throw new Error('Check failed: optimized candidates groups should have at least a verified member');
                    return res;
                });
                res.push({
                    sources: verifiedOptimizedCandidates,
                    target: targetKey,
                    targetFp: targetKeyFp,
                });
            }
            return res;
        });
    }
}
exports.TransferKeysManager = TransferKeysManager;
//# sourceMappingURL=TransferKeysManager.js.map