diff --git a/src/controller.cpp b/src/controller.cpp index 66d73ad6e..e54f13e9b 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -409,7 +409,7 @@ void Controller::removeConnection(const QString &userId) bool Controller::csSupported() const { -#if Quotient_VERSION_MINOR > 9 +#if Quotient_VERSION_MINOR > 8 return true; #else return false; diff --git a/src/foreigntypes.h b/src/foreigntypes.h index 02f6e71ea..c86c3d3c9 100644 --- a/src/foreigntypes.h +++ b/src/foreigntypes.h @@ -10,6 +10,10 @@ #include #include +#if Quotient_VERSION_MINOR > 8 +#include +#endif + #include "controller.h" #include "neochatconfig.h" @@ -38,3 +42,12 @@ struct ForeignSSSSHandler { QML_FOREIGN(Quotient::SSSSHandler) QML_NAMED_ELEMENT(SSSSHandler) }; + +#if Quotient_VERSION_MINOR > 8 +struct ForeignKeyImport { + Q_GADGET + QML_SINGLETON + QML_FOREIGN(Quotient::KeyImport) + QML_NAMED_ELEMENT(KeyImport) +}; +#endif diff --git a/src/neochatconnection.cpp b/src/neochatconnection.cpp index 2b2f7ebcb..67c77b745 100644 --- a/src/neochatconnection.cpp +++ b/src/neochatconnection.cpp @@ -574,4 +574,21 @@ LinkPreviewer *NeoChatConnection::previewerForLink(const QUrl &link) return previewer; } +#if Quotient_VERSION_MINOR > 8 +KeyImport::Error NeoChatConnection::exportMegolmSessions(const QString &passphrase, const QString &path) +{ + KeyImport keyImport; + auto result = keyImport.exportKeys(passphrase, this); + if (!result.has_value()) { + return result.error(); + } + QUrl url(path); + QFile file(url.toLocalFile()); + file.open(QFile::WriteOnly); + file.write(result.value()); + file.close(); + return KeyImport::Success; +} +#endif + #include "moc_neochatconnection.cpp" diff --git a/src/neochatconnection.h b/src/neochatconnection.h index 81c4a2f31..49fd19a50 100644 --- a/src/neochatconnection.h +++ b/src/neochatconnection.h @@ -9,6 +9,10 @@ #include #include +#if Quotient_VERSION_MINOR > 8 +#include +#endif + #include "models/threepidmodel.h" class LinkPreviewer; @@ -169,6 +173,10 @@ public: */ Q_INVOKABLE QString accountDataJsonString(const QString &type) const; +#if Quotient_VERSION_MINOR > 8 + Q_INVOKABLE Quotient::KeyImport::Error exportMegolmSessions(const QString &passphrase, const QString &path); +#endif + qsizetype directChatNotifications() const; bool directChatsHaveHighlightNotifications() const; qsizetype homeNotifications() const; diff --git a/src/settings/CMakeLists.txt b/src/settings/CMakeLists.txt index 60f73cf79..a7e408d7d 100644 --- a/src/settings/CMakeLists.txt +++ b/src/settings/CMakeLists.txt @@ -40,4 +40,6 @@ ecm_add_qml_module(settings GENERATE_PLUGIN_SOURCE PasswordSheet.qml ThemeRadioButton.qml ThreePIdCard.qml + ImportKeysDialog.qml + ExportKeysDialog.qml ) diff --git a/src/settings/ExportKeysDialog.qml b/src/settings/ExportKeysDialog.qml new file mode 100644 index 000000000..4c9f442e5 --- /dev/null +++ b/src/settings/ExportKeysDialog.qml @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2024 Tobias Fella +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import QtQuick.Dialogs + +import org.kde.kirigami as Kirigami +import org.kde.kirigamiaddons.formcard as FormCard +import org.kde.kirigamiaddons.labs.components as KirigamiComponents + +import org.kde.neochat + +FormCard.FormCardPage { + id: root + + title: i18nc("@title", "Export Keys") + + required property NeoChatConnection connection + + header: KirigamiComponents.Banner { + id: banner + showCloseButton: true + visible: false + type: Kirigami.MessageType.Error + } + + FormCard.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + FormCard.FormTextFieldDelegate { + id: passphraseField + label: i18nc("@label:textbox", "Passphrase:") + echoMode: TextInput.Password + } + FormCard.FormTextDelegate { + text: i18n("A passphrase to secure your key backup. It should not be your account password.") + } + FormCard.FormDelegateSeparator {} + FormCard.FormButtonDelegate { + enabled: passphraseField.text.length > 0 + text: i18nc("@action:button", "Export keys") + onClicked: { + let dialog = saveDialog.createObject(root); + dialog.accepted.connect(() => { + banner.visible = false; + let error = root.connection.exportMegolmSessions(passphraseField.text, dialog.selectedFile); + passphraseField.text = ""; + if (error === KeyImport.Success) { + banner.text = i18nc("@info", "Keys exported successfully"); + banner.type = Kirigami.MessageType.Positive; + banner.visible = true; + } else { + banner.text = i18nc("@info", "Unknown error"); + banner.type = Kirigami.MessageType.Error; + banner.visible = true; + } + }); + dialog.open(); + } + } + } + + Component { + id: saveDialog + FileDialog { + fileMode: FileDialog.SaveFile + currentFolder: Config.lastSaveDirectory.length > 0 ? Config.lastSaveDirectory : StandardPaths.writableLocation(StandardPaths.DocumentsLocation) + } + } +} diff --git a/src/settings/ImportKeysDialog.qml b/src/settings/ImportKeysDialog.qml new file mode 100644 index 000000000..9d08fdf94 --- /dev/null +++ b/src/settings/ImportKeysDialog.qml @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2024 Tobias Fella +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Dialogs +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami +import org.kde.kirigamiaddons.formcard as FormCard +import org.kde.kirigamiaddons.labs.components as KirigamiComponents + +import org.kde.neochat + +FormCard.FormCardPage { + id: root + + required property NeoChatConnection connection + + signal success + + title: i18nc("@title", "Import Keys") + + header: KirigamiComponents.Banner { + id: banner + showCloseButton: true + visible: false + type: Kirigami.MessageType.Error + } + + FormCard.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + FormCard.FormButtonDelegate { + id: fileButton + text: i18nc("@action:button", "Choose backup file") + description: "" + icon.name: "cloud-upload" + onClicked: { + let dialog = Qt.createComponent("QtQuick.Dialogs", "FileDialog").createObject(root, { + fileMode: FileDialog.OpenFile + }); + dialog.accepted.connect(() => { + fileButton.description = dialog.selectedFile.toString().substring(7); + }); + dialog.open(); + } + } + FormCard.FormDelegateSeparator {} + FormCard.FormTextFieldDelegate { + id: passphraseField + label: i18nc("@label:textbox", "Passphrase:") + echoMode: TextInput.Password + } + FormCard.FormDelegateSeparator {} + FormCard.FormButtonDelegate { + text: i18nc("@action:button", "Import keys") + enabled: fileButton.description.length > 0 && passphraseField.text.length > 0 + onClicked: { + banner.visible = false; + let error = KeyImport.importKeys(Controller.loadFileContent(fileButton.description), passphraseField.text, root.connection); + passphraseField.text = ""; + if (error === KeyImport.Success) { + root.success(); + root.closeDialog(); + } else if (error === KeyImport.InvalidPassphrase) { + banner.text = i18nc("@info", "Invalid passphrase"); + banner.type = Kirigami.MessageType.Error; + banner.visible = true; + } else if (error === KeyImport.InvalidData) { + banner.text = i18nc("@info", "Invalid key backup data"); + banner.type = Kirigami.MessageType.Error; + banner.visible = true; + } else { + banner.text = i18nc("@info", "Unknown error"); + banner.type = Kirigami.MessageType.Error; + banner.visible = true; + } + } + } + } + + +} diff --git a/src/settings/NeoChatSecurityPage.qml b/src/settings/NeoChatSecurityPage.qml index ca5ed65be..395196694 100644 --- a/src/settings/NeoChatSecurityPage.qml +++ b/src/settings/NeoChatSecurityPage.qml @@ -2,10 +2,11 @@ // SPDX-License-Identifier: GPL-2.0-or-later import QtQuick -import QtQuick.Controls +import QtQuick.Controls as QQC2 import org.kde.kirigami as Kirigami import org.kde.kirigamiaddons.formcard as FormCard +import org.kde.kirigamiaddons.labs.components as KirigamiComponents import org.kde.neochat @@ -16,6 +17,13 @@ FormCard.FormCardPage { title: i18nc("@title", "Security") + header: KirigamiComponents.Banner { + id: banner + showCloseButton: true + visible: false + type: Kirigami.MessageType.Error + } + FormCard.FormHeader { title: i18nc("@title:group", "Invitations") } @@ -60,6 +68,43 @@ FormCard.FormCardPage { } } + FormCard.FormHeader { + visible: Controller.csSupported + title: i18nc("@title", "Encryption Keys") + } + FormCard.FormCard { + visible: Controller.csSupported + FormCard.FormButtonDelegate { + text: i18nc("@action:button", "Import Encryption Keys") + icon.name: "document-import" + onClicked: { + let dialog = root.QQC2.ApplicationWindow.window.pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat.settings", "ImportKeysDialog"), { + connection: root.connection + }, { + title: i18nc("@title", "Import Keys"), + }); + dialog.success.connect(() => { + banner.text = i18nc("@info", "Keys imported successfully"); + banner.type = Kirigami.MessageType.Positive; + banner.visible = true; + }); + banner.visible = false; + } + } + FormCard.FormButtonDelegate { + text: i18nc("@action:button", "Export Encryption Keys") + icon.name: "document-export" + onClicked: { + root.QQC2.ApplicationWindow.window.pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat.settings", "ExportKeysDialog"), { + connection: root.connection + }, { + title: i18nc("@title", "Export Keys") + }); + banner.visible = false; + } + } + } + Component { id: ignoredUsersDialogComponent IgnoredUsersDialog {