diff --git a/TODO.md b/TODO.md index a730c68b..1648b826 100644 --- a/TODO.md +++ b/TODO.md @@ -121,7 +121,6 @@ - Edit/delete own devices - Request room keys from own other devices - Auto-trust accounts within the same client - - Export keys - Uploads & proper http thumbnails - Reduce messages ListView cacheBuffer height once http thumbnails downloading is implemented diff --git a/src/python/matrix_client.py b/src/python/matrix_client.py index e9a9d18b..b9207fff 100644 --- a/src/python/matrix_client.py +++ b/src/python/matrix_client.py @@ -338,6 +338,17 @@ class MatrixClient(nio.AsyncClient): await self.retry_decrypting_events() + async def export_keys(self, outfile: str, passphrase: str) -> None: + path = Path(outfile) + path.parent.mkdir(parents=True, exist_ok=True) + + # Remove any existing file + # (the QML dialog asks the user if he wants to overwrite before this) + path.unlink() + + await super().export_keys(outfile, passphrase) + + async def clear_import_error(self) -> None: self.models[Account][self.user_id].import_error = ("", "", "") diff --git a/src/qml/Base/HPasswordPopup.qml b/src/qml/Base/HPasswordPopup.qml index df3126c3..eba42ebb 100644 --- a/src/qml/Base/HPasswordPopup.qml +++ b/src/qml/Base/HPasswordPopup.qml @@ -28,10 +28,10 @@ Popup { function verifyPassword(pass, callback) { - // Implement this function when using this component. - // Return true on success, false on invalid password, or + // Can be reimplemented when using this component. + // Pass to the callback true on success, false on invalid password, or // a [error message, translated] array for any other error. - return ["Verification not implemented", false] + callback(true) } diff --git a/src/qml/Pages/EditAccount/Encryption.qml b/src/qml/Pages/EditAccount/Encryption.qml index 573e3309..47450ebf 100644 --- a/src/qml/Pages/EditAccount/Encryption.qml +++ b/src/qml/Pages/EditAccount/Encryption.qml @@ -14,6 +14,19 @@ HLoader { property bool importing: false + function exportKeys(file, passphrase, button=null) { + if (button) button.loading = true + + let path = file.toString().replace(/^file:\/\//, "") + + py.callClientCoro( + editAccount.userId, "export_keys", [path, passphrase], () => { + // null: user is on another page + if (encryptionUI !== null && button) button.loading = false + } + ) + } + function importKeys(file, passphrase, button=null) { if (button) button.loading = true encryptionUI.importing = true @@ -22,7 +35,7 @@ HLoader { py.callClientCoro( editAccount.userId, "import_keys", [path, passphrase], () => { - if (encryptionUI !== null) { // null: user is on another page + if (encryptionUI !== null) { encryptionUI.importing = false if (button) button.loading = false } diff --git a/src/qml/Pages/EditAccount/ImportExportKeys.qml b/src/qml/Pages/EditAccount/ImportExportKeys.qml index 93bd2bcf..3cecb534 100644 --- a/src/qml/Pages/EditAccount/ImportExportKeys.qml +++ b/src/qml/Pages/EditAccount/ImportExportKeys.qml @@ -1,22 +1,27 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 +import Qt.labs.platform 1.1 import "../../Base" import "../../utils.js" as Utils HBox { + property var exportButton: null + horizontalSpacing: currentSpacing verticalSpacing: currentSpacing buttonModel: [ - { name: "export", text: qsTr("Export"), iconName: "export-keys", - enabled: false }, + { name: "export", text: qsTr("Export"), iconName: "export-keys"}, { name: "import", text: qsTr("Import"), iconName: "import-keys"}, ] buttonCallbacks: ({ - export: button => {}, - import: button => { fileDialog.dialog.open() }, + export: button => { + exportButton = button + exportFileDialog.dialog.open() + }, + import: button => { importFileDialog.dialog.open() }, }) @@ -24,29 +29,49 @@ HBox { wrapMode: Text.Wrap text: qsTr( "The decryption keys for messages you received in encrypted " + - "rooms can be exported to a passphrase-protected file.%1" + - "You will then be able to import this file in another " + - "Matrix client." - ).arg(pageLoader.isWide ? "\n" :"\n\n") + "rooms can be exported to a passphrase-protected file.\n" + + "You can then import this file on another Matrix account or " + + "client, to be able to decrypt these messages again." + ) Layout.fillWidth: true } HFileDialogOpener { - id: fileDialog + id: exportFileDialog fill: false - dialog.title: qsTr("Select a decryption key file to import") + dialog.title: qsTr("Save decryption keys file as...") + dialog.fileMode: FileDialog.SaveFile + onFileChanged: { + exportPasswordPopup.file = file + exportPasswordPopup.open() + } + } + + HFileDialogOpener { + id: importFileDialog + fill: false + dialog.title: qsTr("Select a decryption keys file to import") onFileChanged: { importPasswordPopup.file = file importPasswordPopup.open() } } + HPasswordPopup { + property url file: "" + + id: exportPasswordPopup + label.text: qsTr("Please enter a passphrase to protect this file:") + onAcceptedPasswordChanged: + encryptionUI.exportKeys(file, acceptedPassword, exportButton) + } + HPasswordPopup { property url file: "" function verifyPassword(pass, callback) { - return py.callCoro( + py.callCoro( "check_exported_keys_passphrase", [file.toString().replace(/^file:\/\//, ""), pass], callback