From e07b87667745b9456189b92d048b0ea6414ac204 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 29 Jul 2023 23:18:49 +0200 Subject: [PATCH] Add UI for entering key backup passphrase --- src/CMakeLists.txt | 1 + src/controller.cpp | 17 +++++ src/controller.h | 6 ++ src/main.cpp | 8 ++ src/neochatconfig.kcfg | 4 + src/qml/AccountMenu.qml | 9 +++ src/qml/FeatureFlagPage.qml | 6 ++ src/qml/UnlockSSSSDialog.qml | 137 +++++++++++++++++++++++++++++++++++ 8 files changed, 188 insertions(+) create mode 100644 src/qml/UnlockSSSSDialog.qml diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c2ef77323..068b04057 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -309,6 +309,7 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN qml/IgnoredUsersDialog.qml qml/AccountData.qml qml/StateKeys.qml + qml/UnlockSSSSDialog.qml RESOURCES qml/confetti.png qml/glowdot.png diff --git a/src/controller.cpp b/src/controller.cpp index 09057312e..739b7af38 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -377,6 +377,14 @@ AccountRegistry &Controller::accounts() return m_accountRegistry; } +QString Controller::loadFileContent(const QString &path) const +{ + QUrl url(path); + QFile file(url.isLocalFile() ? url.toLocalFile() : url.toString()); + file.open(QFile::ReadOnly); + return QString::fromLatin1(file.readAll()); +} + #include "moc_controller.cpp" void Controller::setTestMode(bool test) @@ -393,3 +401,12 @@ void Controller::removeConnection(const QString &userId) SettingsGroup("Accounts"_ls).remove(userId); } } + +bool Controller::ssssSupported() const +{ +#if __has_include("Quotient/e2ee/sssshandler.h") + return true; +#else + return false; +#endif +} diff --git a/src/controller.h b/src/controller.h index ba90d0dff..2c82fde31 100644 --- a/src/controller.h +++ b/src/controller.h @@ -56,6 +56,8 @@ class Controller : public QObject Q_PROPERTY(QStringList accountsLoading MEMBER m_accountsLoading NOTIFY accountsLoadingChanged) + Q_PROPERTY(bool ssssSupported READ ssssSupported CONSTANT) + public: static Controller &instance(); static Controller *create(QQmlEngine *engine, QJSEngine *) @@ -92,12 +94,16 @@ public: */ static void listenForNotifications(); + Q_INVOKABLE QString loadFileContent(const QString &path) const; + Quotient::AccountRegistry &accounts(); static void setTestMode(bool testMode); Q_INVOKABLE void removeConnection(const QString &userId); + bool ssssSupported() const; + private: explicit Controller(QObject *parent = nullptr); diff --git a/src/main.cpp b/src/main.cpp index 77c4c0efa..8b13cfd1e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -35,6 +35,11 @@ #include "neochat-version.h" +#include +#if __has_include("Quotient/e2ee/sssshandler.h") +#include +#endif +#include #include #include "blurhashimageprovider.h" @@ -230,6 +235,9 @@ int main(int argc, char *argv[]) qmlRegisterSingletonInstance("org.kde.neochat.accounts", 1, 0, "AccountRegistry", &Controller::instance().accounts()); qmlRegisterUncreatableType("com.github.quotient_im.libquotient", 1, 0, "KeyVerificationSession", {}); +#if __has_include("Quotient/e2ee/sssshandler.h") + qmlRegisterType("com.github.quotient_im.libquotient", 1, 0, "SSSSHandler"); +#endif QQmlApplicationEngine engine; diff --git a/src/neochatconfig.kcfg b/src/neochatconfig.kcfg index 974502bb1..080e0ca16 100644 --- a/src/neochatconfig.kcfg +++ b/src/neochatconfig.kcfg @@ -161,6 +161,10 @@ false + + + false + diff --git a/src/qml/AccountMenu.qml b/src/qml/AccountMenu.qml index 2d70bea78..f672d10e1 100644 --- a/src/qml/AccountMenu.qml +++ b/src/qml/AccountMenu.qml @@ -62,6 +62,15 @@ QQC2.Menu { height: Kirigami.Units.gridUnit * 42 }) } + QQC2.MenuItem { + text: i18nc("@action:inmenu", "Open Secret Backup") + icon.name: "unlock" + visible: Config.secretBackup + onTriggered: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UnlockSSSSDialog.qml'), {}, { + title: i18nc("@title:window", "Open Key Backup") + }) + enabled: Controller.ssssSupported + } QQC2.MenuItem { text: i18n("Logout") icon.name: "list-remove-user" diff --git a/src/qml/FeatureFlagPage.qml b/src/qml/FeatureFlagPage.qml index 4e1b53414..308260d53 100644 --- a/src/qml/FeatureFlagPage.qml +++ b/src/qml/FeatureFlagPage.qml @@ -23,5 +23,11 @@ FormCard.FormCardPage { onToggled: Config.threads = checked } + FormCard.FormCheckDelegate { + text: i18nc("@option:check Enable the matrix 'secret backup' feature", "Secret Backup") + checked: Config.secretBackup + + onToggled: Config.secretBackup = checked + } } } diff --git a/src/qml/UnlockSSSSDialog.qml b/src/qml/UnlockSSSSDialog.qml new file mode 100644 index 000000000..e5299c876 --- /dev/null +++ b/src/qml/UnlockSSSSDialog.qml @@ -0,0 +1,137 @@ +// SPDX-FileCopyrightText: 2023 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick +import QtQuick.Controls as QQC2 +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 com.github.quotient_im.libquotient + +import org.kde.neochat + +FormCard.FormCardPage { + id: root + + title: i18nc("@title:window", "Load your encrypted messages") + + topPadding: Kirigami.Units.gridUnit + leftPadding: 0 + rightPadding: 0 + + header: KirigamiComponents.Banner { + id: banner + showCloseButton: true + visible: false + type: Kirigami.MessageType.Error + } + + property SSSSHandler ssssHandler: SSSSHandler { + id: ssssHandler + + property bool processing: false + + connection: Controller.activeConnection + onKeyBackupUnlocked: { + ssssHandler.processing = false + root.closeDialog() + } + onError: error => { + if (error !== SSSSHandler.WrongKeyError) { + banner.text = error + banner.visible = true + return; + } + passwordField.clear() + ssssHandler.processing = false + banner.text = i18nc("@info:status", "The security phrase was not correct.") + banner.visible = true + } + } + + FormCard.FormHeader { + title: i18nc("@title", "Unlock using Passphrase") + } + FormCard.FormCard { + FormCard.FormTextDelegate { + description: i18nc("@info", "If you have a backup passphrase for this account, enter it below.") + } + FormCard.FormTextFieldDelegate { + id: passwordField + label: i18nc("@label:textbox", "Backup Password:") + echoMode: TextInput.Password + } + FormCard.FormButtonDelegate { + id: unlockButton + text: i18nc("@action:button", "Unlock") + icon.name: "unlock" + enabled: passwordField.text.length > 0 && !ssssHandler.processing + onClicked: { + ssssHandler.processing = true + banner.visible = false + ssssHandler.unlockSSSSWithPassphrase(passwordField.text) + } + } + } + + FormCard.FormHeader { + title: i18nc("@title", "Unlock using Security Key") + } + FormCard.FormCard { + FormCard.FormTextDelegate { + description: i18nc("@info", "If you have a security key for this account, enter it below or upload it as a file.") + } + FormCard.FormTextFieldDelegate { + id: securityKeyField + label: i18nc("@label:textbox", "Security Key:") + echoMode: TextInput.Password + } + FormCard.FormButtonDelegate { + id: uploadSecurityKeyButton + text: i18nc("@action:button", "Upload from File") + icon.name: "cloud-upload" + enabled: !ssssHandler.processing + onClicked: { + ssssHandler.processing = true + openFileDialog.open() + } + } + FormCard.FormButtonDelegate { + id: unlockSecurityKeyButton + text: i18nc("@action:button", "Unlock") + icon.name: "unlock" + enabled: securityKeyField.text.length > 0 && !ssssHandler.processing + onClicked: { + ssssHandler.processing = true + ssssHandler.unlockSSSSFromSecurityKey(securityKeyField.text) + } + } + } + + FormCard.FormHeader { + title: i18nc("@title", "Unlock from Cross-Signing") + } + FormCard.FormCard { + FormCard.FormTextDelegate { + description: i18nc("@info", "If you have previously verified this device, you can try loading the backup key from other devices by clicking the button below.") + } + FormCard.FormButtonDelegate { + id: unlockCrossSigningButton + icon.name: "emblem-shared-symbolic" + text: i18nc("@action:button", "Request from other Devices") + enabled: !ssssHandler.processing + onClicked: { + ssssHandler.processing = true + ssssHandler.unlockSSSSFromCrossSigning() + } + } + } + + property OpenFileDialog openFileDialog: OpenFileDialog { + id: openFileDialog + onChosen: securityKeyField.text = Controller.loadFileContent(path) + } +}