/* Copyright 2024 New Vector Ltd. Copyright 2022 The Matrix.org Foundation C.I.C. Copyright 2017 Vector Creations Ltd SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ import FileSaver from "file-saver"; import React, { ChangeEvent } from "react"; import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { _t, _td } from "../../../../languageHandler"; import * as MegolmExportEncryption from "../../../../utils/MegolmExportEncryption"; import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; import { KeysStartingWith } from "../../../../@types/common"; import PassphraseField from "../../../../components/views/auth/PassphraseField"; import PassphraseConfirmField from "../../../../components/views/auth/PassphraseConfirmField"; import Field from "../../../../components/views/elements/Field"; enum Phase { Edit = "edit", Exporting = "exporting", } interface IProps { matrixClient: MatrixClient; onFinished(doExport?: boolean): void; } interface IState { phase: Phase; errStr: string | null; passphrase1: string; passphrase2: string; } type AnyPassphrase = KeysStartingWith; export default class ExportE2eKeysDialog extends React.Component { private fieldPassword: Field | null = null; private fieldPasswordConfirm: Field | null = null; private unmounted = false; public constructor(props: IProps) { super(props); this.state = { phase: Phase.Edit, errStr: null, passphrase1: "", passphrase2: "", }; } public componentWillUnmount(): void { this.unmounted = true; } private async verifyFieldsBeforeSubmit(): Promise { const fieldsInDisplayOrder = [this.fieldPassword, this.fieldPasswordConfirm]; const invalidFields: Field[] = []; for (const field of fieldsInDisplayOrder) { if (!field) continue; const valid = await field.validate({ allowEmpty: false }); if (!valid) { invalidFields.push(field); } } if (invalidFields.length === 0) { return true; } // Focus on the first invalid field, then re-validate, // which will result in the error tooltip being displayed for that field. invalidFields[0].focus(); invalidFields[0].validate({ allowEmpty: false, focused: true }); return false; } private onPassphraseFormSubmit = async (ev: React.FormEvent): Promise => { ev.preventDefault(); if (!(await this.verifyFieldsBeforeSubmit())) return; if (this.unmounted) return; const passphrase = this.state.passphrase1; this.startExport(passphrase); }; private startExport(passphrase: string): void { // extra Promise.resolve() to turn synchronous exceptions into // asynchronous ones. Promise.resolve() .then(() => { return this.props.matrixClient.getCrypto()!.exportRoomKeysAsJson(); }) .then((k) => { return MegolmExportEncryption.encryptMegolmKeyFile(k, passphrase); }) .then((f) => { const blob = new Blob([f], { type: "text/plain;charset=us-ascii", }); FileSaver.saveAs(blob, "element-keys.txt"); this.props.onFinished(true); }) .catch((e) => { logger.error("Error exporting e2e keys:", e); if (this.unmounted) { return; } const msg = e.friendlyText || _t("error|unknown"); this.setState({ errStr: msg, phase: Phase.Edit, }); }); this.setState({ errStr: null, phase: Phase.Exporting, }); } private onCancelClick = (ev: React.MouseEvent): boolean => { ev.preventDefault(); this.props.onFinished(false); return false; }; private onPassphraseChange = (ev: React.ChangeEvent, phrase: AnyPassphrase): void => { this.setState({ [phrase]: ev.target.value, } as Pick); }; public render(): React.ReactNode { const disableForm = this.state.phase === Phase.Exporting; return (

{_t("settings|key_export_import|export_description_1")}

{_t("settings|key_export_import|export_description_2")}

{this.state.errStr}
) => this.onPassphraseChange(e, "passphrase1") } autoFocus={true} size={64} type="password" disabled={disableForm} autoComplete="new-password" fieldRef={(field) => (this.fieldPassword = field)} />
) => this.onPassphraseChange(e, "passphrase2") } size={64} type="password" disabled={disableForm} autoComplete="new-password" fieldRef={(field) => (this.fieldPasswordConfirm = field)} />
); } }