"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
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.CryptoClient = void 0;
const LogService_1 = require("../logging/LogService");
const Olm = require("@matrix-org/olm");
const crypto = require("crypto");
const anotherJson = require("another-json");
const Crypto_1 = require("../models/Crypto");
const decorators_1 = require("./decorators");
const RoomTracker_1 = require("./RoomTracker");
const DeviceTracker_1 = require("./DeviceTracker");
const EncryptionEvent_1 = require("../models/events/EncryptionEvent");
const EncryptedRoomEvent_1 = require("../models/events/EncryptedRoomEvent");
const RoomEvent_1 = require("../models/events/RoomEvent");
const b64_1 = require("../b64");
const stream_1 = require("stream");
/**
 * Manages encryption for a MatrixClient. Get an instance from a MatrixClient directly
 * rather than creating one manually.
 * @category Encryption
 */
class CryptoClient {
    constructor(client) {
        this.client = client;
        this.ready = false;
        this.roomTracker = new RoomTracker_1.RoomTracker(this.client);
        this.deviceTracker = new DeviceTracker_1.DeviceTracker(this.client);
    }
    /**
     * The device ID for the MatrixClient.
     */
    get clientDeviceId() {
        return this.deviceId;
    }
    /**
     * Whether or not the crypto client is ready to be used. If not ready, prepare() should be called.
     * @see prepare
     */
    get isReady() {
        return this.ready;
    }
    getOlmAccount() {
        return __awaiter(this, void 0, void 0, function* () {
            const account = new Olm.Account();
            account.unpickle(this.pickleKey, yield this.client.cryptoStore.getPickledAccount());
            return account;
        });
    }
    storeAndFreeOlmAccount(account) {
        return __awaiter(this, void 0, void 0, function* () {
            const pickled = account.pickle(this.pickleKey);
            yield this.client.cryptoStore.setPickledAccount(pickled);
            account.free();
        });
    }
    /**
     * Prepares the crypto client for usage.
     * @param {string[]} roomIds The room IDs the MatrixClient is joined to.
     */
    prepare(roomIds) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.roomTracker.prepare(roomIds);
            const storedDeviceId = yield this.client.cryptoStore.getDeviceId();
            if (storedDeviceId) {
                this.deviceId = storedDeviceId;
            }
            else {
                const deviceId = (yield this.client.getWhoAmI())['device_id'];
                if (!deviceId) {
                    throw new Error("Encryption not possible: server not revealing device ID");
                }
                this.deviceId = deviceId;
                yield this.client.cryptoStore.setDeviceId(this.deviceId);
            }
            LogService_1.LogService.debug("CryptoClient", "Starting with device ID:", this.deviceId);
            // We should be in a ready enough shape to kick off Olm
            yield Olm.init();
            let pickled = yield this.client.cryptoStore.getPickledAccount();
            let pickleKey = yield this.client.cryptoStore.getPickleKey();
            const account = new Olm.Account();
            const makeReady = () => {
                const keys = JSON.parse(account.identity_keys());
                this.deviceCurve25519 = keys['curve25519'];
                this.deviceEd25519 = keys['ed25519'];
                this.pickleKey = pickleKey;
                this.maxOTKs = account.max_number_of_one_time_keys();
                this.ready = true;
            };
            try {
                if (!pickled || !pickleKey) {
                    LogService_1.LogService.debug("CryptoClient", "Creating new Olm account: previous session lost or not set up");
                    account.create();
                    pickleKey = crypto.randomBytes(64).toString('hex');
                    pickled = account.pickle(pickleKey);
                    yield this.client.cryptoStore.setPickleKey(pickleKey);
                    yield this.client.cryptoStore.setPickledAccount(pickled);
                    makeReady();
                    const counts = yield this.client.uploadDeviceKeys([
                        Crypto_1.EncryptionAlgorithm.MegolmV1AesSha2,
                        Crypto_1.EncryptionAlgorithm.OlmV1Curve25519AesSha2,
                    ], {
                        [`${Crypto_1.DeviceKeyAlgorithm.Ed25519}:${this.deviceId}`]: this.deviceEd25519,
                        [`${Crypto_1.DeviceKeyAlgorithm.Curve25519}:${this.deviceId}`]: this.deviceCurve25519,
                    });
                    yield this.updateCounts(counts);
                }
                else {
                    account.unpickle(pickleKey, pickled);
                    makeReady();
                    yield this.updateCounts(yield this.client.checkOneTimeKeyCounts());
                }
            }
            finally {
                account.free();
            }
        });
    }
    /**
     * Checks if a room is encrypted.
     * @param {string} roomId The room ID to check.
     * @returns {Promise<boolean>} Resolves to true if encrypted, false otherwise.
     */
    isRoomEncrypted(roomId) {
        return __awaiter(this, void 0, void 0, function* () {
            const config = yield this.roomTracker.getRoomCryptoConfig(roomId);
            return !!(config === null || config === void 0 ? void 0 : config.algorithm);
        });
    }
    /**
     * Updates the One Time Key counts, potentially triggering an async upload of more
     * one time keys.
     * @param {OTKCounts} counts The current counts to work within.
     * @returns {Promise<void>} Resolves when complete.
     */
    updateCounts(counts) {
        return __awaiter(this, void 0, void 0, function* () {
            const have = counts[Crypto_1.OTKAlgorithm.Signed] || 0;
            const need = Math.floor(this.maxOTKs / 2) - have;
            if (need <= 0)
                return;
            LogService_1.LogService.debug("CryptoClient", `Creating ${need} more OTKs`);
            const account = yield this.getOlmAccount();
            try {
                account.generate_one_time_keys(need);
                const { curve25519: keys } = JSON.parse(account.one_time_keys());
                const signed = {};
                for (const keyId in keys) {
                    if (!keys.hasOwnProperty(keyId))
                        continue;
                    const obj = { key: keys[keyId] };
                    obj['signatures'] = yield this.sign(obj);
                    signed[`${Crypto_1.OTKAlgorithm.Signed}:${keyId}`] = obj;
                }
                yield this.client.uploadDeviceOneTimeKeys(signed);
                account.mark_keys_as_published();
            }
            finally {
                yield this.storeAndFreeOlmAccount(account);
            }
        });
    }
    /**
     * Updates the client's fallback key.
     * @returns {Promise<void>} Resolves when complete.
     */
    updateFallbackKey() {
        return __awaiter(this, void 0, void 0, function* () {
            const account = yield this.getOlmAccount();
            try {
                account.generate_fallback_key();
                const key = JSON.parse(account.fallback_key());
                const keyId = Object.keys(key[Crypto_1.OTKAlgorithm.Unsigned])[0];
                const obj = {
                    key: key[Crypto_1.OTKAlgorithm.Unsigned][keyId],
                    fallback: true,
                };
                const signatures = yield this.sign(obj);
                const fallback = {
                    keyId: keyId,
                    key: Object.assign(Object.assign({}, obj), { signatures: signatures }),
                };
                yield this.client.uploadFallbackKey(fallback);
            }
            finally {
                yield this.storeAndFreeOlmAccount(account);
            }
        });
    }
    /**
     * Signs an object using the device keys.
     * @param {object} obj The object to sign.
     * @returns {Promise<Signatures>} The signatures for the object.
     */
    sign(obj) {
        return __awaiter(this, void 0, void 0, function* () {
            obj = JSON.parse(JSON.stringify(obj));
            const existingSignatures = obj['signatures'] || {};
            delete obj['signatures'];
            delete obj['unsigned'];
            const account = yield this.getOlmAccount();
            try {
                const sig = account.sign(anotherJson.stringify(obj));
                return Object.assign({ [yield this.client.getUserId()]: {
                        [`${Crypto_1.DeviceKeyAlgorithm.Ed25519}:${this.deviceId}`]: sig,
                    } }, existingSignatures);
            }
            finally {
                account.free();
            }
        });
    }
    /**
     * Verifies a signature on an object.
     * @param {object} obj The signed object.
     * @param {string} key The key which has supposedly signed the object.
     * @param {string} signature The advertised signature.
     * @returns {Promise<boolean>} Resolves to true if a valid signature, false otherwise.
     */
    verifySignature(obj, key, signature) {
        return __awaiter(this, void 0, void 0, function* () {
            obj = JSON.parse(JSON.stringify(obj));
            delete obj['signatures'];
            delete obj['unsigned'];
            const util = new Olm.Utility();
            try {
                const message = anotherJson.stringify(obj);
                util.ed25519_verify(key, message, signature);
            }
            catch (e) {
                // Assume it's a verification failure
                return false;
            }
            finally {
                util.free();
            }
            return true;
        });
    }
    /**
     * Flags multiple user's device lists as outdated, optionally queuing an immediate update.
     * @param {string} userIds The user IDs to flag the device lists of.
     * @param {boolean} resync True (default) to queue an immediate update, false otherwise.
     * @returns {Promise<void>} Resolves when the device lists have been flagged. Will also wait
     * for the resync if one was requested.
     */
    flagUsersDeviceListsOutdated(userIds, resync = true) {
        return this.deviceTracker.flagUsersOutdated(userIds, resync);
    }
    /**
     * Gets or creates Olm sessions for the given users and devices. Where sessions cannot be created,
     * the user/device will be excluded from the returned map.
     * @param {Record<string, string[]>} userDeviceMap Map of user IDs to device IDs
     * @param {boolean} force If true, force creation of a session for the referenced users.
     * @returns {Promise<Record<string, Record<string, IOlmSession>>>} Resolves to a map of user ID to device
     * ID to session. Users/devices which cannot have sessions made will not be included, thus the object
     * may be empty.
     */
    getOrCreateOlmSessions(userDeviceMap, force = false) {
        var _a, _b;
        return __awaiter(this, void 0, void 0, function* () {
            const otkClaimRequest = {};
            const userDeviceSessionIds = {};
            const myUserId = yield this.client.getUserId();
            const myDeviceId = this.clientDeviceId;
            for (const userId of Object.keys(userDeviceMap)) {
                for (const deviceId of userDeviceMap[userId]) {
                    if (userId === myUserId && deviceId === myDeviceId) {
                        // Skip creating a session for our own device
                        continue;
                    }
                    const existingSession = force ? null : (yield this.client.cryptoStore.getCurrentOlmSession(userId, deviceId));
                    if (existingSession) {
                        if (!userDeviceSessionIds[userId])
                            userDeviceSessionIds[userId] = {};
                        userDeviceSessionIds[userId][deviceId] = existingSession;
                    }
                    else {
                        if (!otkClaimRequest[userId])
                            otkClaimRequest[userId] = {};
                        otkClaimRequest[userId][deviceId] = Crypto_1.OTKAlgorithm.Signed;
                    }
                }
            }
            if (Object.keys(otkClaimRequest).length > 0) {
                const claimed = yield this.client.claimOneTimeKeys(otkClaimRequest);
                for (const userId of Object.keys(claimed.one_time_keys)) {
                    if (!otkClaimRequest[userId]) {
                        LogService_1.LogService.warn("CryptoClient", `Server injected unexpected user: ${userId} - not claiming keys`);
                        continue;
                    }
                    const storedDevices = yield this.client.cryptoStore.getActiveUserDevices(userId);
                    for (const deviceId of Object.keys(claimed.one_time_keys[userId])) {
                        try {
                            if (!otkClaimRequest[userId][deviceId]) {
                                LogService_1.LogService.warn("CryptoClient", `Server provided an unexpected device in claim response (skipping): ${userId} ${deviceId}`);
                                continue;
                            }
                            const device = storedDevices.find(d => d.user_id === userId && d.device_id === deviceId);
                            if (!device) {
                                LogService_1.LogService.warn("CryptoClient", `Failed to handle claimed OTK: unable to locate stored device for user: ${userId} ${deviceId}`);
                                continue;
                            }
                            const deviceKeyLabel = `${Crypto_1.DeviceKeyAlgorithm.Ed25519}:${deviceId}`;
                            const keyId = Object.keys(claimed.one_time_keys[userId][deviceId])[0];
                            const signedKey = claimed.one_time_keys[userId][deviceId][keyId];
                            const signature = (_b = (_a = signedKey === null || signedKey === void 0 ? void 0 : signedKey.signatures) === null || _a === void 0 ? void 0 : _a[userId]) === null || _b === void 0 ? void 0 : _b[deviceKeyLabel];
                            if (!signature) {
                                LogService_1.LogService.warn("CryptoClient", `Failed to find appropriate signature for claimed OTK ${userId} ${deviceId}`);
                                continue;
                            }
                            const verified = yield this.verifySignature(signedKey, device.keys[deviceKeyLabel], signature);
                            if (!verified) {
                                LogService_1.LogService.warn("CryptoClient", `Invalid signature for claimed OTK ${userId} ${deviceId}`);
                                continue;
                            }
                            // TODO: Handle spec rate limiting
                            // Clients should rate-limit the number of sessions it creates per device that it receives a message
                            // from. Clients should not create a new session with another device if it has already created one
                            // for that given device in the past 1 hour.
                            // Finally, we can create a session. We do this on each loop just in case something goes wrong given
                            // we don't have app-level transaction support here. We want to persist as many outbound sessions as
                            // we can before exploding.
                            const account = yield this.getOlmAccount();
                            const session = new Olm.Session();
                            try {
                                const curveDeviceKey = device.keys[`${Crypto_1.DeviceKeyAlgorithm.Curve25519}:${deviceId}`];
                                session.create_outbound(account, curveDeviceKey, signedKey.key);
                                const storedSession = {
                                    sessionId: session.session_id(),
                                    lastDecryptionTs: Date.now(),
                                    pickled: session.pickle(this.pickleKey),
                                };
                                yield this.client.cryptoStore.storeOlmSession(userId, deviceId, storedSession);
                                if (!userDeviceSessionIds[userId])
                                    userDeviceSessionIds[userId] = {};
                                userDeviceSessionIds[userId][deviceId] = storedSession;
                            }
                            finally {
                                session.free();
                                yield this.storeAndFreeOlmAccount(account);
                            }
                        }
                        catch (e) {
                            LogService_1.LogService.warn("CryptoClient", `Unable to verify signature of claimed OTK ${userId} ${deviceId}:`, e);
                        }
                    }
                }
            }
            return userDeviceSessionIds;
        });
    }
    encryptAndSendOlmMessage(device, session, type, content) {
        return __awaiter(this, void 0, void 0, function* () {
            const olmSession = new Olm.Session();
            try {
                olmSession.unpickle(this.pickleKey, session.pickled);
                const payload = {
                    keys: {
                        ed25519: this.deviceEd25519,
                    },
                    recipient_keys: {
                        ed25519: device.keys[`${Crypto_1.DeviceKeyAlgorithm.Ed25519}:${device.device_id}`],
                    },
                    recipient: device.user_id,
                    sender: yield this.client.getUserId(),
                    content: content,
                    type: type,
                };
                const encrypted = olmSession.encrypt(JSON.stringify(payload));
                yield this.client.cryptoStore.storeOlmSession(device.user_id, device.device_id, {
                    pickled: olmSession.pickle(this.pickleKey),
                    lastDecryptionTs: session.lastDecryptionTs,
                    sessionId: olmSession.session_id(),
                });
                const message = {
                    algorithm: Crypto_1.EncryptionAlgorithm.OlmV1Curve25519AesSha2,
                    ciphertext: {
                        [device.keys[`${Crypto_1.DeviceKeyAlgorithm.Curve25519}:${device.device_id}`]]: encrypted,
                    },
                    sender_key: this.deviceCurve25519,
                };
                yield this.client.sendToDevices("m.room.encrypted", {
                    [device.user_id]: {
                        [device.device_id]: message,
                    },
                });
            }
            finally {
                olmSession.free();
            }
        });
    }
    /**
     * Encrypts the details of a room event, returning an encrypted payload to be sent in an
     * `m.room.encrypted` event to the room. If needed, this function will send decryption keys
     * to the appropriate devices in the room (this happens when the Megolm session rotates or
     * gets created).
     * @param {string} roomId The room ID to encrypt within. If the room is not encrypted, an
     * error is thrown.
     * @param {string} eventType The event type being encrypted.
     * @param {any} content The event content being encrypted.
     * @returns {Promise<IMegolmEncrypted>} Resolves to the encrypted content for an `m.room.encrypted` event.
     */
    encryptRoomEvent(roomId, eventType, content) {
        var _a, _b;
        return __awaiter(this, void 0, void 0, function* () {
            if (!(yield this.isRoomEncrypted(roomId))) {
                throw new Error("Room is not encrypted");
            }
            let relatesTo;
            if (content['m.relates_to']) {
                relatesTo = JSON.parse(JSON.stringify(content['m.relates_to']));
                delete content['m.relates_to'];
            }
            const now = (new Date()).getTime();
            let currentSession = yield this.client.cryptoStore.getCurrentOutboundGroupSession(roomId);
            if (currentSession && (currentSession.expiresTs <= now || currentSession.usesLeft <= 0)) {
                currentSession = null; // force rotation
            }
            if (!currentSession) {
                // Make a new session, either because we don't have one or it rotated.
                const roomConfig = new EncryptionEvent_1.EncryptionEvent({
                    type: "m.room.encryption",
                    state_key: "",
                    content: yield this.roomTracker.getRoomCryptoConfig(roomId),
                });
                const newSession = new Olm.OutboundGroupSession();
                try {
                    newSession.create();
                    const pickled = newSession.pickle(this.pickleKey);
                    currentSession = {
                        sessionId: newSession.session_id(),
                        roomId: roomId,
                        pickled: pickled,
                        isCurrent: true,
                        usesLeft: roomConfig.rotationPeriodMessages,
                        expiresTs: now + roomConfig.rotationPeriodMs,
                    };
                    // Store the session as an inbound session up front. This is to ensure that we have the
                    // earliest possible ratchet available to our own decryption functions. We don't store
                    // the outbound session here as it is stored earlier on.
                    yield this.storeInboundGroupSession({
                        room_id: roomId,
                        session_id: newSession.session_id(),
                        session_key: newSession.session_key(),
                        algorithm: Crypto_1.EncryptionAlgorithm.MegolmV1AesSha2,
                    }, yield this.client.getUserId(), this.clientDeviceId);
                }
                finally {
                    newSession.free();
                }
            }
            // TODO: Include invited members?
            const memberUserIds = yield this.client.getJoinedRoomMembers(roomId);
            const devices = yield this.deviceTracker.getDevicesFor(memberUserIds);
            const session = new Olm.OutboundGroupSession();
            try {
                session.unpickle(this.pickleKey, currentSession.pickled);
                const neededSessions = {};
                for (const userId of Object.keys(devices)) {
                    neededSessions[userId] = devices[userId].map(d => d.device_id);
                }
                const olmSessions = yield this.getOrCreateOlmSessions(neededSessions);
                for (const userId of Object.keys(devices)) {
                    for (const device of devices[userId]) {
                        const olmSession = (_a = olmSessions[userId]) === null || _a === void 0 ? void 0 : _a[device.device_id];
                        if (!olmSession) {
                            LogService_1.LogService.warn("CryptoClient", `Unable to send Megolm session to ${userId} ${device.device_id}: No Olm session`);
                            continue;
                        }
                        const lastSession = yield this.client.cryptoStore.getLastSentOutboundGroupSession(userId, device.device_id, roomId);
                        if ((lastSession === null || lastSession === void 0 ? void 0 : lastSession.sessionId) !== session.session_id() || session.message_index() < ((_b = lastSession === null || lastSession === void 0 ? void 0 : lastSession.index) !== null && _b !== void 0 ? _b : Number.MAX_SAFE_INTEGER)) {
                            yield this.encryptAndSendOlmMessage(device, olmSession, "m.room_key", {
                                algorithm: Crypto_1.EncryptionAlgorithm.MegolmV1AesSha2,
                                room_id: roomId,
                                session_id: session.session_id(),
                                session_key: session.session_key(),
                            });
                            yield this.client.cryptoStore.storeSentOutboundGroupSession(currentSession, session.message_index(), device);
                        }
                    }
                }
                // Encrypt after to avoid UNKNOWN_MESSAGE_INDEX errors on remote end
                const encrypted = session.encrypt(JSON.stringify({
                    type: eventType,
                    content: content,
                    room_id: roomId,
                }));
                currentSession.pickled = session.pickle(this.pickleKey);
                currentSession.usesLeft--;
                yield this.client.cryptoStore.storeOutboundGroupSession(currentSession);
                const body = {
                    sender_key: this.deviceCurve25519,
                    ciphertext: encrypted,
                    session_id: session.session_id(),
                    device_id: this.clientDeviceId,
                };
                if (relatesTo) {
                    body['m.relates_to'] = relatesTo;
                }
                return Object.assign(Object.assign({}, body), { algorithm: Crypto_1.EncryptionAlgorithm.MegolmV1AesSha2 });
            }
            finally {
                session.free();
            }
        });
    }
    /**
     * Decrypts a room event. Currently only supports Megolm-encrypted events (default for this SDK).
     * @param {EncryptedRoomEvent} event The encrypted event.
     * @param {string} roomId The room ID where the event was sent.
     * @returns {Promise<RoomEvent<unknown>>} Resolves to a decrypted room event, or rejects/throws with
     * an error if the event is undecryptable.
     */
    decryptRoomEvent(event, roomId) {
        return __awaiter(this, void 0, void 0, function* () {
            if (event.algorithm !== Crypto_1.EncryptionAlgorithm.MegolmV1AesSha2) {
                throw new Error("Unable to decrypt: Unknown algorithm");
            }
            const encrypted = event.megolmProperties;
            const senderDevice = yield this.client.cryptoStore.getActiveUserDevice(event.sender, encrypted.device_id);
            if (!senderDevice) {
                throw new Error("Unable to decrypt: Unknown device for sender");
            }
            if (senderDevice.keys[`${Crypto_1.DeviceKeyAlgorithm.Curve25519}:${senderDevice.device_id}`] !== encrypted.sender_key) {
                throw new Error("Unable to decrypt: Device key mismatch");
            }
            const storedSession = yield this.client.cryptoStore.getInboundGroupSession(event.sender, encrypted.device_id, roomId, encrypted.session_id);
            if (!storedSession) {
                throw new Error("Unable to decrypt: Unknown inbound session ID");
            }
            const session = new Olm.InboundGroupSession();
            try {
                session.unpickle(this.pickleKey, storedSession.pickled);
                const cleartext = session.decrypt(encrypted.ciphertext);
                const eventBody = JSON.parse(cleartext.plaintext);
                const messageIndex = cleartext.message_index;
                const existingEventId = yield this.client.cryptoStore.getEventForMessageIndex(roomId, storedSession.sessionId, messageIndex);
                if (existingEventId && existingEventId !== event.eventId) {
                    throw new Error("Unable to decrypt: Message replay attack");
                }
                yield this.client.cryptoStore.setMessageIndexForEvent(roomId, event.eventId, storedSession.sessionId, messageIndex);
                storedSession.pickled = session.pickle(this.pickleKey);
                yield this.client.cryptoStore.storeInboundGroupSession(storedSession);
                return new RoomEvent_1.RoomEvent(Object.assign(Object.assign({}, event.raw), { type: eventBody.type || "io.t2bot.unknown", content: (typeof (eventBody.content) === 'object') ? eventBody.content : {} }));
            }
            finally {
                session.free();
            }
        });
    }
    /**
     * Handles an inbound to-device message, decrypting it if needed. This will not throw
     * under normal circumstances and should always resolve successfully.
     * @param {IToDeviceMessage<IOlmEncrypted>} message The message to process.
     * @returns {Promise<void>} Resolves when complete. Should never fail.
     */
    processInboundDeviceMessage(message) {
        var _a, _b, _c, _d;
        return __awaiter(this, void 0, void 0, function* () {
            if (!(message === null || message === void 0 ? void 0 : message.content) || !(message === null || message === void 0 ? void 0 : message.sender) || !(message === null || message === void 0 ? void 0 : message.type)) {
                LogService_1.LogService.warn("CryptoClient", "Received invalid encrypted message");
                return;
            }
            try {
                if (message.type === "m.room.encrypted") {
                    if (((_a = message.content) === null || _a === void 0 ? void 0 : _a['algorithm']) !== Crypto_1.EncryptionAlgorithm.OlmV1Curve25519AesSha2) {
                        LogService_1.LogService.warn("CryptoClient", "Received encrypted message with unknown encryption algorithm");
                        return;
                    }
                    const myMessage = (_b = message.content.ciphertext) === null || _b === void 0 ? void 0 : _b[this.deviceCurve25519];
                    if (!myMessage) {
                        LogService_1.LogService.warn("CryptoClient", "Received encrypted message not intended for us (ignoring message)");
                        return;
                    }
                    if (!Number.isFinite(myMessage.type) || !myMessage.body) {
                        LogService_1.LogService.warn("CryptoClient", "Received invalid encrypted message (ignoring message)");
                        return;
                    }
                    const userDevices = yield this.client.cryptoStore.getActiveUserDevices(message.sender);
                    const senderDevice = userDevices.find(d => d.keys[`${Crypto_1.DeviceKeyAlgorithm.Curve25519}:${d.device_id}`] === message.content.sender_key);
                    if (!senderDevice) {
                        LogService_1.LogService.warn("CryptoClient", "Received encrypted message from unknown identity key (ignoring message):", message.content.sender_key);
                        return;
                    }
                    const sessions = yield this.client.cryptoStore.getOlmSessions(senderDevice.user_id, senderDevice.device_id);
                    let trySession;
                    for (const storedSession of sessions) {
                        const checkSession = new Olm.Session();
                        try {
                            checkSession.unpickle(this.pickleKey, storedSession.pickled);
                            if (checkSession.matches_inbound(myMessage.body)) {
                                trySession = storedSession;
                                break;
                            }
                        }
                        finally {
                            checkSession.free();
                        }
                    }
                    if (myMessage.type === 0 && !trySession) {
                        // Store the session because we can
                        const inboundSession = new Olm.Session();
                        const account = yield this.getOlmAccount();
                        try {
                            inboundSession.create_inbound_from(account, message.content.sender_key, myMessage.body);
                            account.remove_one_time_keys(inboundSession);
                            trySession = {
                                pickled: inboundSession.pickle(this.pickleKey),
                                sessionId: inboundSession.session_id(),
                                lastDecryptionTs: Date.now(),
                            };
                            yield this.client.cryptoStore.storeOlmSession(senderDevice.user_id, senderDevice.device_id, trySession);
                        }
                        finally {
                            inboundSession.free();
                            yield this.storeAndFreeOlmAccount(account);
                        }
                    }
                    if (myMessage.type !== 0 && !trySession) {
                        LogService_1.LogService.warn("CryptoClient", "Unable to find suitable session for encrypted to-device message; Establishing new session");
                        yield this.establishNewOlmSession(senderDevice);
                        return;
                    }
                    // Try decryption (finally)
                    const session = new Olm.Session();
                    let decrypted;
                    try {
                        session.unpickle(this.pickleKey, trySession.pickled);
                        decrypted = JSON.parse(session.decrypt(myMessage.type, myMessage.body));
                    }
                    catch (e) {
                        LogService_1.LogService.warn("CryptoClient", "Decryption error with to-device message, assuming corrupted session and re-establishing.", e);
                        yield this.establishNewOlmSession(senderDevice);
                        return;
                    }
                    finally {
                        session.free();
                    }
                    const wasForUs = decrypted.recipient === (yield this.client.getUserId());
                    const wasFromThem = decrypted.sender === message.sender;
                    const hasType = typeof (decrypted.type) === 'string';
                    const hasContent = !!decrypted.content && typeof (decrypted.content) === 'object';
                    const ourKeyMatches = ((_c = decrypted.recipient_keys) === null || _c === void 0 ? void 0 : _c.ed25519) === this.deviceEd25519;
                    const theirKeyMatches = ((_d = decrypted.keys) === null || _d === void 0 ? void 0 : _d.ed25519) === senderDevice.keys[`${Crypto_1.DeviceKeyAlgorithm.Ed25519}:${senderDevice.device_id}`];
                    if (!wasForUs || !wasFromThem || !hasType || !hasContent || !ourKeyMatches || !theirKeyMatches) {
                        LogService_1.LogService.warn("CryptoClient", "Successfully decrypted to-device message, but it failed validation. Ignoring message.", {
                            wasForUs,
                            wasFromThem,
                            hasType,
                            hasContent,
                            ourKeyMatches,
                            theirKeyMatches,
                        });
                        return;
                    }
                    trySession.lastDecryptionTs = Date.now();
                    yield this.client.cryptoStore.storeOlmSession(senderDevice.user_id, senderDevice.device_id, trySession);
                    if (decrypted.type === "m.room_key") {
                        yield this.handleInboundRoomKey(decrypted, senderDevice, message);
                    }
                    else if (decrypted.type === "m.dummy") {
                        // success! Nothing to do.
                    }
                    else {
                        LogService_1.LogService.warn("CryptoClient", `Unknown decrypted to-device message type: ${decrypted.type}`);
                    }
                }
                else {
                    LogService_1.LogService.warn("CryptoClient", `Unknown to-device message type: ${message.type}`);
                }
            }
            catch (e) {
                LogService_1.LogService.error("CryptoClient", "Non-fatal error while processing to-device message:", e);
            }
        });
    }
    handleInboundRoomKey(message, device, original) {
        var _a, _b, _c, _d, _e;
        return __awaiter(this, void 0, void 0, function* () {
            if (((_a = message.content) === null || _a === void 0 ? void 0 : _a.algorithm) !== Crypto_1.EncryptionAlgorithm.MegolmV1AesSha2) {
                LogService_1.LogService.warn("CryptoClient", "Ignoring m.room_key for unknown encryption algorithm");
                return;
            }
            if (!((_b = message.content) === null || _b === void 0 ? void 0 : _b.room_id) || !((_c = message.content) === null || _c === void 0 ? void 0 : _c.session_id) || !((_d = message.content) === null || _d === void 0 ? void 0 : _d.session_key)) {
                LogService_1.LogService.warn("CryptoClient", "Ignoring invalid m.room_key");
                return;
            }
            const deviceKey = device.keys[`${Crypto_1.DeviceKeyAlgorithm.Curve25519}:${device.device_id}`];
            if (deviceKey !== ((_e = original.content) === null || _e === void 0 ? void 0 : _e.sender_key)) {
                LogService_1.LogService.warn("CryptoClient", "Ignoring m.room_key message from unexpected sender");
                return;
            }
            // See if we already know about this session (if we do: ignore the message)
            const knownSession = yield this.client.cryptoStore.getInboundGroupSession(device.user_id, device.device_id, message.content.room_id, message.content.session_id);
            if (knownSession) {
                return; // ignore
            }
            yield this.storeInboundGroupSession(message.content, device.user_id, device.device_id);
        });
    }
    storeInboundGroupSession(key, senderUserId, senderDeviceId) {
        return __awaiter(this, void 0, void 0, function* () {
            const inboundSession = new Olm.InboundGroupSession();
            try {
                inboundSession.create(key.session_key);
                if (inboundSession.session_id() !== key.session_id) {
                    LogService_1.LogService.warn("CryptoClient", "Ignoring m.room_key with mismatched session_id");
                    return;
                }
                yield this.client.cryptoStore.storeInboundGroupSession({
                    roomId: key.room_id,
                    sessionId: key.session_id,
                    senderDeviceId: senderDeviceId,
                    senderUserId: senderUserId,
                    pickled: inboundSession.pickle(this.pickleKey),
                });
            }
            finally {
                inboundSession.free();
            }
        });
    }
    establishNewOlmSession(device) {
        return __awaiter(this, void 0, void 0, function* () {
            const olmSessions = yield this.getOrCreateOlmSessions({
                [device.user_id]: [device.device_id],
            }, true);
            // Share the session immediately
            yield this.encryptAndSendOlmMessage(device, olmSessions[device.user_id][device.device_id], "m.dummy", {});
        });
    }
    /**
     * Encrypts a file for uploading in a room, returning the encrypted data and information
     * to include in a message event (except media URL) for sending.
     * @param {Buffer} file The file to encrypt.
     * @returns {{buffer: Buffer, file: Omit<EncryptedFile, "url">}} Resolves to the encrypted
     * contents and file information.
     */
    encryptMedia(file) {
        return __awaiter(this, void 0, void 0, function* () {
            const key = crypto.randomBytes(32);
            const iv = new Uint8Array(16);
            crypto.randomBytes(8).forEach((v, i) => iv[i] = v); // only fill high side to avoid 64bit overflow
            const cipher = crypto.createCipheriv("aes-256-ctr", key, iv);
            const buffers = [];
            cipher.on('data', b => {
                buffers.push(b);
            });
            const stream = new stream_1.PassThrough();
            stream.pipe(cipher);
            stream.end(file);
            const finishPromise = new Promise(resolve => {
                cipher.end(() => {
                    resolve(Buffer.concat(buffers));
                });
            });
            const cipheredContent = yield finishPromise;
            let sha256;
            const util = new Olm.Utility();
            try {
                const arr = new Uint8Array(cipheredContent);
                sha256 = util.sha256(arr);
            }
            finally {
                util.free();
            }
            return {
                buffer: Buffer.from(cipheredContent),
                file: {
                    hashes: {
                        sha256: sha256,
                    },
                    key: {
                        alg: "A256CTR",
                        ext: true,
                        key_ops: ['encrypt', 'decrypt'],
                        kty: "oct",
                        k: b64_1.encodeUnpaddedUrlSafeBase64(key),
                    },
                    iv: b64_1.encodeUnpaddedBase64(iv),
                    v: 'v2',
                },
            };
        });
    }
    /**
     * Decrypts a previously-uploaded encrypted file, validating the fields along the way.
     * @param {EncryptedFile} file The file to decrypt.
     * @returns {Promise<Buffer>} Resolves to the decrypted file contents.
     */
    decryptMedia(file) {
        var _a, _b, _c, _d;
        return __awaiter(this, void 0, void 0, function* () {
            if (file.v !== "v2") {
                throw new Error("Unknown encrypted file version");
            }
            if (((_a = file.key) === null || _a === void 0 ? void 0 : _a.kty) !== "oct" || ((_b = file.key) === null || _b === void 0 ? void 0 : _b.alg) !== "A256CTR" || ((_c = file.key) === null || _c === void 0 ? void 0 : _c.ext) !== true) {
                throw new Error("Improper JWT: Missing or invalid fields");
            }
            if (!file.key.key_ops.includes("encrypt") || !file.key.key_ops.includes("decrypt")) {
                throw new Error("Missing required key_ops");
            }
            if (!((_d = file.hashes) === null || _d === void 0 ? void 0 : _d.sha256)) {
                throw new Error("Missing SHA256 hash");
            }
            const key = b64_1.decodeUnpaddedUrlSafeBase64(file.key.k);
            const iv = b64_1.decodeUnpaddedBase64(file.iv);
            const ciphered = (yield this.client.downloadContent(file.url)).data;
            let sha256;
            const util = new Olm.Utility();
            try {
                const arr = new Uint8Array(ciphered);
                sha256 = util.sha256(arr);
            }
            finally {
                util.free();
            }
            if (sha256 !== file.hashes.sha256) {
                throw new Error("SHA256 mismatch");
            }
            const decipher = crypto.createDecipheriv("aes-256-ctr", key, iv);
            const buffers = [];
            decipher.on('data', b => {
                buffers.push(b);
            });
            const stream = new stream_1.PassThrough();
            stream.pipe(decipher);
            stream.end(ciphered);
            return new Promise(resolve => {
                decipher.end(() => {
                    resolve(Buffer.concat(buffers));
                });
            });
        });
    }
}
__decorate([
    decorators_1.requiresReady(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [String]),
    __metadata("design:returntype", Promise)
], CryptoClient.prototype, "isRoomEncrypted", null);
__decorate([
    decorators_1.requiresReady(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], CryptoClient.prototype, "updateCounts", null);
__decorate([
    decorators_1.requiresReady(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", Promise)
], CryptoClient.prototype, "updateFallbackKey", null);
__decorate([
    decorators_1.requiresReady(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], CryptoClient.prototype, "sign", null);
__decorate([
    decorators_1.requiresReady(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object, String, String]),
    __metadata("design:returntype", Promise)
], CryptoClient.prototype, "verifySignature", null);
__decorate([
    decorators_1.requiresReady(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Array, Object]),
    __metadata("design:returntype", Promise)
], CryptoClient.prototype, "flagUsersDeviceListsOutdated", null);
__decorate([
    decorators_1.requiresReady(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object, Object]),
    __metadata("design:returntype", Promise)
], CryptoClient.prototype, "getOrCreateOlmSessions", null);
__decorate([
    decorators_1.requiresReady(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object, Object, String, Object]),
    __metadata("design:returntype", Promise)
], CryptoClient.prototype, "encryptAndSendOlmMessage", null);
__decorate([
    decorators_1.requiresReady(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [String, String, Object]),
    __metadata("design:returntype", Promise)
], CryptoClient.prototype, "encryptRoomEvent", null);
__decorate([
    decorators_1.requiresReady(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [EncryptedRoomEvent_1.EncryptedRoomEvent, String]),
    __metadata("design:returntype", Promise)
], CryptoClient.prototype, "decryptRoomEvent", null);
__decorate([
    decorators_1.requiresReady(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], CryptoClient.prototype, "processInboundDeviceMessage", null);
__decorate([
    decorators_1.requiresReady(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Buffer]),
    __metadata("design:returntype", Promise)
], CryptoClient.prototype, "encryptMedia", null);
exports.CryptoClient = CryptoClient;
