From ad1e6c82986fd92d34fdc3d7f27a152a80d05e81 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 18 Sep 2021 00:21:14 +0200 Subject: [PATCH] Support bootstrapping crosssigning Showing the bootstrap state and showing there are unverified devices is still missing. --- CMakeLists.txt | 6 +- io.github.NhekoReborn.Nheko.yaml | 2 +- resources/qml/Avatar.qml | 1 + resources/qml/ChatPage.qml | 9 +- resources/qml/MessageView.qml | 9 +- resources/qml/Root.qml | 23 +++ resources/qml/SelfVerificationCheck.qml | 261 ++++++++++++++++++++++++ resources/qml/delegates/Reply.qml | 14 +- resources/qml/delegates/TextMessage.qml | 1 + resources/qml/dialogs/InputDialog.qml | 1 + resources/qml/dialogs/RoomDirectory.qml | 2 +- resources/qml/dialogs/UserProfile.qml | 47 +++-- resources/res.qrc | 1 + src/Cache.cpp | 12 ++ src/Cache_p.h | 1 + src/SelfVerificationStatus.cpp | 249 ++++++++++++++++++++++ src/SelfVerificationStatus.h | 43 ++++ src/timeline/TimelineViewManager.cpp | 44 ++-- src/ui/UIA.cpp | 136 ++++++++++++ src/ui/UIA.h | 40 ++++ 20 files changed, 834 insertions(+), 68 deletions(-) create mode 100644 resources/qml/SelfVerificationCheck.qml create mode 100644 src/SelfVerificationStatus.cpp create mode 100644 src/SelfVerificationStatus.h create mode 100644 src/ui/UIA.cpp create mode 100644 src/ui/UIA.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a3b344b8..3e5c2f09 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -328,6 +328,7 @@ set(SRC_FILES src/ui/Theme.cpp src/ui/ThemeManager.cpp src/ui/ToggleButton.cpp + src/ui/UIA.cpp src/ui/UserProfile.cpp # Generic notification stuff @@ -365,6 +366,7 @@ set(SRC_FILES src/RoomDirectoryModel.cpp src/RoomsModel.cpp src/Utils.cpp + src/SelfVerificationStatus.cpp src/WebRTCSession.cpp src/WelcomePage.cpp src/main.cpp @@ -385,7 +387,7 @@ if(USE_BUNDLED_MTXCLIENT) FetchContent_Declare( MatrixClient GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git - GIT_TAG 4a598632f432953f4dbfacf6cfed4f85a1c59c5a + GIT_TAG 8b56b466dbacde501ed9087d53bb4f51b297eca8 ) set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "") set(BUILD_LIB_TESTS OFF CACHE INTERNAL "") @@ -541,6 +543,7 @@ qt5_wrap_cpp(MOC_HEADERS src/ui/Theme.h src/ui/ThemeManager.h src/ui/ToggleButton.h + src/ui/UIA.h src/ui/UserProfile.h src/notifications/Manager.h @@ -573,6 +576,7 @@ qt5_wrap_cpp(MOC_HEADERS src/UsersModel.h src/RoomDirectoryModel.h src/RoomsModel.h + src/SelfVerificationStatus.h src/WebRTCSession.h src/WelcomePage.h src/ReadReceiptsModel.h diff --git a/io.github.NhekoReborn.Nheko.yaml b/io.github.NhekoReborn.Nheko.yaml index e4b6ed8f..ce272ea7 100644 --- a/io.github.NhekoReborn.Nheko.yaml +++ b/io.github.NhekoReborn.Nheko.yaml @@ -163,7 +163,7 @@ modules: buildsystem: cmake-ninja name: mtxclient sources: - - commit: 4a598632f432953f4dbfacf6cfed4f85a1c59c5a + - commit: 8b56b466dbacde501ed9087d53bb4f51b297eca8 type: git url: https://github.com/Nheko-Reborn/mtxclient.git - config-opts: diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml index 5d2b583a..58b22863 100644 --- a/resources/qml/Avatar.qml +++ b/resources/qml/Avatar.qml @@ -42,6 +42,7 @@ Rectangle { Image { id: identicon + anchors.fill: parent visible: Settings.useIdenticon && img.status != Image.Ready source: Settings.useIdenticon ? ("image://jdenticon/" + (userid !== "" ? userid : roomid) + "?radius=" + (Settings.avatarCircles ? 100 : 25)) : "" diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml index e56d7d46..082fa8d6 100644 --- a/resources/qml/ChatPage.qml +++ b/resources/qml/ChatPage.qml @@ -2,12 +2,15 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -import QtQuick 2.9 -import QtQuick.Controls 2.5 +import QtQuick 2.15 +import QtQuick.Controls 2.15 import QtQuick.Layouts 1.3 import "components" import im.nheko 1.0 +// this needs to be last +import QtQml 2.15 + Rectangle { id: chatPage @@ -41,6 +44,7 @@ Rectangle { value: communityListC.preferredWidth when: !adaptiveView.singlePageMode delayed: true + restoreMode: Binding.RestoreBindingOrValue } } @@ -66,6 +70,7 @@ Rectangle { value: roomListC.preferredWidth when: !adaptiveView.singlePageMode delayed: true + restoreMode: Binding.RestoreBindingOrValue } } diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 60c04098..7ed30112 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -20,12 +20,11 @@ ScrollView { ListView { id: chat - - displayMarginBeginning: height/2 - displayMarginEnd: height/2 property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < parent.availableWidth) ? Settings.timelineMaxWidth : parent.availableWidth) - parent.padding * 2 + displayMarginBeginning: height / 2 + displayMarginEnd: height / 2 model: room // reuseItems still has a few bugs, see https://bugreports.qt.io/browse/QTBUG-95105 https://bugreports.qt.io/browse/QTBUG-95107 //onModelChanged: if (room) room.sendReset() @@ -415,8 +414,6 @@ ScrollView { Loader { id: section - z: 4 - property int parentWidth: parent.width property string userId: wrapper.userId property string previousMessageUserId: wrapper.previousMessageUserId @@ -425,6 +422,7 @@ ScrollView { property string userName: wrapper.userName property var timestamp: wrapper.timestamp + z: 4 active: previousMessageUserId !== undefined && previousMessageUserId !== userId || previousMessageDay !== day //asynchronous: true sourceComponent: sectionHeader @@ -685,6 +683,7 @@ ScrollView { text: qsTr("&Go to quoted message") onTriggered: chat.model.showEvent(eventId) } + } } diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml index 4207f35b..45825f80 100644 --- a/resources/qml/Root.qml +++ b/resources/qml/Root.qml @@ -195,6 +195,29 @@ Page { target: CallManager } + SelfVerificationCheck { + } + + InputDialog { + id: uiaPassPrompt + + echoMode: TextInput.Password + title: UIA.title + prompt: qsTr("Please enter your login password to continue:") + onAccepted: (t) => { + return UIA.continuePassword(t); + } + } + + Connections { + function onPassword() { + console.log("UIA: password needed"); + uiaPassPrompt.show(); + } + + target: UIA + } + ChatPage { anchors.fill: parent } diff --git a/resources/qml/SelfVerificationCheck.qml b/resources/qml/SelfVerificationCheck.qml new file mode 100644 index 00000000..a12bfc61 --- /dev/null +++ b/resources/qml/SelfVerificationCheck.qml @@ -0,0 +1,261 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import Qt.labs.platform 1.1 as P +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.3 +import im.nheko 1.0 + +Item { + visible: false + enabled: false + + Dialog { + id: showRecoverKeyDialog + + property string recoveryKey: "" + + parent: Overlay.overlay + anchors.centerIn: parent + height: content.height + implicitFooterHeight + implicitHeaderHeight + width: content.width + padding: 0 + modal: true + standardButtons: Dialog.Ok + closePolicy: Popup.NoAutoClose + + ColumnLayout { + id: content + + spacing: 0 + + Label { + Layout.margins: Nheko.paddingMedium + Layout.maximumWidth: (Overlay.overlay ? Overlay.overlay.width : 400) - Nheko.paddingMedium * 4 + Layout.fillWidth: true + text: qsTr("This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don't share it with anyone and don't lose it! Don't go to start! Don't draw $200 from the bank!") + color: Nheko.colors.text + wrapMode: Text.Wrap + } + + TextEdit { + Layout.maximumWidth: (Overlay.overlay ? Overlay.overlay.width : 400) - Nheko.paddingMedium * 4 + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: TextEdit.AlignHCenter + verticalAlignment: TextEdit.AlignVCenter + readOnly: true + selectByMouse: true + text: showRecoverKeyDialog.recoveryKey + color: Nheko.colors.text + font.bold: true + wrapMode: TextEdit.Wrap + } + + } + + background: Rectangle { + color: Nheko.colors.window + border.color: Nheko.theme.separator + border.width: 1 + radius: Nheko.paddingSmall + } + + } + + P.MessageDialog { + id: successDialog + + buttons: P.MessageDialog.Ok + text: qsTr("Encryption setup successfully") + } + + P.MessageDialog { + id: failureDialog + + property string errorMessage + + buttons: P.MessageDialog.Ok + text: qsTr("Failed to setup encryption: %1").arg(errorMessage) + } + + Dialog { + id: bootstrapCrosssigning + + parent: Overlay.overlay + anchors.centerIn: parent + height: (Math.floor(parent.height / 2) - Nheko.paddingLarge) * 2 + width: (Math.floor(parent.width / 2) - Nheko.paddingLarge) * 2 + padding: 0 + modal: true + standardButtons: Dialog.Ok | Dialog.Cancel + closePolicy: Popup.NoAutoClose + onAccepted: SelfVerificationStatus.setupCrosssigning(storeSecretsOnline.checked, usePassword.checked ? passwordField.text : "", useOnlineKeyBackup.checked) + + ScrollView { + id: scroll + + clip: true + anchors.fill: parent + ScrollBar.horizontal.visible: false + ScrollBar.vertical.visible: true + + GridLayout { + id: grid + + width: scroll.width - scroll.ScrollBar.vertical.width + columns: 2 + rowSpacing: 0 + columnSpacing: 0 + + Label { + Layout.margins: Nheko.paddingMedium + Layout.alignment: Qt.AlignHCenter + Layout.columnSpan: 2 + font.pointSize: fontMetrics.font.pointSize * 2 + text: qsTr("Setup Encryption") + color: Nheko.colors.text + wrapMode: Text.Wrap + } + + Label { + Layout.margins: Nheko.paddingMedium + Layout.alignment: Qt.AlignLeft + Layout.columnSpan: 2 + Layout.maximumWidth: grid.width - Nheko.paddingMedium * 2 + text: qsTr("Hello and welcome to Matrix!\nIt seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!") + color: Nheko.colors.text + wrapMode: Text.Wrap + } + + Label { + Layout.margins: Nheko.paddingMedium + Layout.alignment: Qt.AlignLeft + Layout.columnSpan: 1 + Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2 + text: "Store secrets online.\nYou have a few secrets to make all the encryption magic work. While you can keep them stored only locally, we recommend storing them encrypted on the server. Otherwise it will be painful to recover them. Only disable this if you are paranoid and like losing your data!" + color: Nheko.colors.text + wrapMode: Text.Wrap + } + + Item { + Layout.margins: Nheko.paddingMedium + Layout.preferredHeight: storeSecretsOnline.height + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + Layout.fillWidth: true + + ToggleButton { + id: storeSecretsOnline + + checked: true + onClicked: console.log("Store secrets toggled: " + checked) + } + + } + + Label { + Layout.margins: Nheko.paddingMedium + Layout.alignment: Qt.AlignLeft + Layout.columnSpan: 1 + Layout.rowSpan: 2 + Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2 + visible: storeSecretsOnline.checked + text: "Set an online backup password.\nWe recommend you DON'T set a password and instead only rely on the recovery key. You will get a recovery key in any case when storing the cross-signing secrets online, but passwords are usually not very random, so they are easier to attack than a completely random recovery key. If you choose to use a password, DON'T make it the same as your login password, otherwise your server can read all your encrypted messages. (You don't want that.)" + color: Nheko.colors.text + wrapMode: Text.Wrap + } + + Item { + Layout.margins: Nheko.paddingMedium + Layout.topMargin: Nheko.paddingLarge + Layout.preferredHeight: storeSecretsOnline.height + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.rowSpan: usePassword.checked ? 1 : 2 + Layout.fillWidth: true + visible: storeSecretsOnline.checked + + ToggleButton { + id: usePassword + + checked: false + } + + } + + MatrixTextField { + id: passwordField + + Layout.margins: Nheko.paddingMedium + Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2 + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.columnSpan: 1 + Layout.fillWidth: true + visible: storeSecretsOnline.checked && usePassword.checked + echoMode: TextInput.Password + } + + Label { + Layout.margins: Nheko.paddingMedium + Layout.alignment: Qt.AlignLeft + Layout.columnSpan: 1 + Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2 + text: "Use online key backup.\nStore the keys for your messages securely encrypted online. In general you do want this, because it protects your messages from becoming unreadable, if you log out by accident. It does however carry a small security risk, if you ever share your recovery key by accident. Currently this also has some other weaknesses, that might allow the server to insert new keys into your backup. The server will however never be able to read your messages." + color: Nheko.colors.text + wrapMode: Text.Wrap + } + + Item { + Layout.margins: Nheko.paddingMedium + Layout.preferredHeight: storeSecretsOnline.height + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + Layout.fillWidth: true + + ToggleButton { + id: useOnlineKeyBackup + + checked: true + onClicked: console.log("Online key backup toggled: " + checked) + } + + } + + } + + } + + background: Rectangle { + color: Nheko.colors.window + border.color: Nheko.theme.separator + border.width: 1 + radius: Nheko.paddingSmall + } + + } + + Connections { + function onStatusChanged() { + console.log("STATUS CHANGED: " + SelfVerificationStatus.status); + if (SelfVerificationStatus.status == SelfVerificationStatus.NoMasterKey) + bootstrapCrosssigning.open(); + + } + + function onShowRecoveryKey(key) { + showRecoverKeyDialog.recoveryKey = key; + showRecoverKeyDialog.open(); + } + + function onSetupCompleted() { + successDialog.open(); + } + + function onSetupFailed(m) { + failureDialog.errorMessage = m; + failureDialog.open(); + } + + target: SelfVerificationStatus + } + +} diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml index 6c0a6da4..60154837 100644 --- a/resources/qml/delegates/Reply.qml +++ b/resources/qml/delegates/Reply.qml @@ -2,12 +2,12 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +import Qt.labs.platform 1.1 as Platform import QtQuick 2.12 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import QtQuick.Window 2.13 import im.nheko 1.0 -import Qt.labs.platform 1.1 as Platform Item { id: r @@ -66,14 +66,8 @@ Item { TapHandler { acceptedButtons: Qt.RightButton - onLongPressed: replyContextMenu.show( - reply.child.copyText, - reply.child.linkAt(eventPoint.position.x, eventPoint.position.y - userName_.implicitHeight) - ) - onSingleTapped: replyContextMenu.show( - reply.child.copyText, - reply.child.linkAt(eventPoint.position.x, eventPoint.position.y - userName_.implicitHeight) - ) + onLongPressed: replyContextMenu.show(reply.child.copyText, reply.child.linkAt(eventPoint.position.x, eventPoint.position.y - userName_.implicitHeight)) + onSingleTapped: replyContextMenu.show(reply.child.copyText, reply.child.linkAt(eventPoint.position.x, eventPoint.position.y - userName_.implicitHeight)) gesturePolicy: TapHandler.ReleaseWithinBounds } @@ -88,6 +82,7 @@ Item { onSingleTapped: chat.model.openUserProfile(userId) gesturePolicy: TapHandler.ReleaseWithinBounds } + } MessageDelegate { @@ -118,6 +113,7 @@ Item { width: parent.width isReply: true } + } Rectangle { diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml index c37314fd..11ad3aeb 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -43,4 +43,5 @@ MatrixText { anchors.fill: parent cursorShape: Qt.PointingHandCursor } + } diff --git a/resources/qml/dialogs/InputDialog.qml b/resources/qml/dialogs/InputDialog.qml index 1efdbcde..12211c60 100644 --- a/resources/qml/dialogs/InputDialog.qml +++ b/resources/qml/dialogs/InputDialog.qml @@ -12,6 +12,7 @@ ApplicationWindow { id: inputDialog property alias prompt: promptLabel.text + property alias echoMode: statusInput.echoMode property var onAccepted: undefined modality: Qt.NonModal diff --git a/resources/qml/dialogs/RoomDirectory.qml b/resources/qml/dialogs/RoomDirectory.qml index 5c27fc26..67842720 100644 --- a/resources/qml/dialogs/RoomDirectory.qml +++ b/resources/qml/dialogs/RoomDirectory.qml @@ -195,9 +195,9 @@ ApplicationWindow { MatrixTextField { id: chooseServer + Layout.minimumWidth: 0.3 * header.width Layout.maximumWidth: 0.3 * header.width - padding: Nheko.paddingMedium color: Nheko.colors.text placeholderText: qsTr("Choose custom homeserver") diff --git a/resources/qml/dialogs/UserProfile.qml b/resources/qml/dialogs/UserProfile.qml index d5442382..c921278e 100644 --- a/resources/qml/dialogs/UserProfile.qml +++ b/resources/qml/dialogs/UserProfile.qml @@ -35,8 +35,18 @@ ApplicationWindow { onActivated: userProfileDialog.close() } - ListView { + id: devicelist + + Layout.fillHeight: true + Layout.fillWidth: true + clip: true + spacing: 8 + boundsBehavior: Flickable.StopAtBounds + model: profile.deviceList + anchors.fill: parent + anchors.margins: 10 + footerPositioning: ListView.OverlayFooter ScrollHelper { flickable: parent @@ -46,16 +56,17 @@ ApplicationWindow { header: ColumnLayout { id: contentL - width: devicelist.width + width: devicelist.width spacing: 10 Avatar { + id: displayAvatar + url: profile.avatarUrl.replace("mxc://", "image://MxcImage/") height: 130 width: 130 displayName: profile.displayName - id: displayAvatar userid: profile.userid Layout.alignment: Qt.AlignHCenter onClicked: TimelineManager.openImageOverlay(profile.avatarUrl, "") @@ -72,6 +83,7 @@ ApplicationWindow { image: ":/icons/icons/ui/edit.png" onClicked: profile.changeAvatar() } + } Spinner { @@ -163,19 +175,22 @@ ApplicationWindow { Layout.alignment: Qt.AlignHCenter } - RowLayout { visible: !profile.isGlobalUserProfile Layout.alignment: Qt.AlignHCenter spacing: Nheko.paddingSmall + MatrixText { id: displayRoomname - text: qsTr("Room: %1").arg(profile.room?profile.room.roomName:"") + + text: qsTr("Room: %1").arg(profile.room ? profile.room.roomName : "") ToolTip.text: qsTr("This is a room-specific profile. The user's name and avatar may be different from their global versions.") ToolTip.visible: ma.hovered + HoverHandler { id: ma } + } ImageButton { @@ -185,6 +200,7 @@ ApplicationWindow { ToolTip.text: qsTr("Open the global profile for this user.") onClicked: profile.openGlobalProfile() } + } Button { @@ -254,27 +270,18 @@ ApplicationWindow { hoverEnabled: true ToolTip.visible: hovered ToolTip.text: qsTr("Refresh device list.") - onClicked: profile.refreshDevices(); + onClicked: profile.refreshDevices() } } + } - id: devicelist - Layout.fillHeight: true - Layout.fillWidth: true - clip: true - spacing: 8 - boundsBehavior: Flickable.StopAtBounds - model: profile.deviceList - anchors.fill: parent - anchors.margins: 10 - - delegate: RowLayout { required property int verificationStatus required property string deviceId required property string deviceName + width: devicelist.width spacing: 4 @@ -304,7 +311,7 @@ ApplicationWindow { Layout.preferredHeight: 16 Layout.preferredWidth: 16 source: { - switch (verificationStatus){ + switch (verificationStatus) { case VerificationStatus.VERIFIED: return "image://colorimage/:/icons/icons/ui/lock.png?green"; case VerificationStatus.UNVERIFIED: @@ -331,17 +338,19 @@ ApplicationWindow { } } - footerPositioning: ListView.OverlayFooter + footer: DialogButtonBox { z: 2 width: devicelist.width alignment: Qt.AlignRight standardButtons: DialogButtonBox.Ok onAccepted: userProfileDialog.close() + background: Rectangle { anchors.fill: parent color: Nheko.colors.window } + } } diff --git a/resources/res.qrc b/resources/res.qrc index 3bd301f7..3e5e6cc8 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -138,6 +138,7 @@ qml/TopBar.qml qml/QuickSwitcher.qml qml/ForwardCompleter.qml + qml/SelfVerificationCheck.qml qml/TypingIndicator.qml qml/NotificationWarning.qml qml/emoji/EmojiPicker.qml diff --git a/src/Cache.cpp b/src/Cache.cpp index ee0ca0c2..ea3dd525 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -201,6 +201,18 @@ Cache::Cache(const QString &userId, QObject *parent) { setup(); connect(this, &Cache::userKeysUpdate, this, &Cache::updateUserKeys, Qt::QueuedConnection); + connect( + this, + &Cache::verificationStatusChanged, + this, + [this](const std::string &u) { + if (u == localUserId_.toStdString()) { + auto status = verificationStatus(u); + if (status.unverified_device_count || !status.user_verified) + emit selfUnverified(); + } + }, + Qt::QueuedConnection); } void diff --git a/src/Cache_p.h b/src/Cache_p.h index 52375d38..f7db77d4 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -310,6 +310,7 @@ signals: void removeNotification(const QString &room_id, const QString &event_id); void userKeysUpdate(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery); void verificationStatusChanged(const std::string &userid); + void selfUnverified(); void secretChanged(const std::string name); private: diff --git a/src/SelfVerificationStatus.cpp b/src/SelfVerificationStatus.cpp new file mode 100644 index 00000000..d75a2109 --- /dev/null +++ b/src/SelfVerificationStatus.cpp @@ -0,0 +1,249 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "SelfVerificationStatus.h" + +#include "Cache_p.h" +#include "Logging.h" +#include "MainWindow.h" +#include "MatrixClient.h" +#include "Olm.h" +#include "ui/UIA.h" + +#include + +SelfVerificationStatus::SelfVerificationStatus(QObject *o) + : QObject(o) +{ + connect(MainWindow::instance(), &MainWindow::reload, this, [this] { + connect(cache::client(), + &Cache::selfUnverified, + this, + &SelfVerificationStatus::invalidate, + Qt::UniqueConnection); + invalidate(); + }); +} + +void +SelfVerificationStatus::setupCrosssigning(bool useSSSS, QString password, bool useOnlineKeyBackup) +{ + nhlog::db()->info("Clicked setup crossigning"); + + auto xsign_keys = olm::client()->create_crosssigning_keys(); + + if (!xsign_keys) { + nhlog::crypto()->critical("Failed to setup cross-signing keys!"); + emit setupFailed(tr("Failed to create keys for cross-signing!")); + return; + } + + cache::client()->storeSecret(mtx::secret_storage::secrets::cross_signing_master, + xsign_keys->private_master_key); + cache::client()->storeSecret(mtx::secret_storage::secrets::cross_signing_self_signing, + xsign_keys->private_self_signing_key); + cache::client()->storeSecret(mtx::secret_storage::secrets::cross_signing_user_signing, + xsign_keys->private_user_signing_key); + + std::optional okb; + if (useOnlineKeyBackup) { + okb = olm::client()->create_online_key_backup(xsign_keys->private_master_key); + if (!okb) { + nhlog::crypto()->critical("Failed to setup online key backup!"); + emit setupFailed(tr("Failed to create keys for online key backup!")); + return; + } + + cache::client()->storeSecret( + mtx::secret_storage::secrets::megolm_backup_v1, + mtx::crypto::bin2base64(mtx::crypto::to_string(okb->privateKey))); + + http::client()->post_backup_version( + okb->backupVersion.algorithm, + okb->backupVersion.auth_data, + [](const mtx::responses::Version &v, mtx::http::RequestErr e) { + if (e) { + nhlog::net()->error("error setting up online key backup: {} {} {} {}", + e->parse_error, + e->status_code, + e->error_code, + e->matrix_error.error); + } else { + nhlog::crypto()->info("Set up online key backup: '{}'", v.version); + } + }); + } + + std::optional ssss; + if (useSSSS) { + ssss = olm::client()->create_ssss_key(password.toStdString()); + if (!ssss) { + nhlog::crypto()->critical("Failed to setup secure server side secret storage!"); + emit setupFailed(tr("Failed to create keys secure server side secret storage!")); + return; + } + + auto master = mtx::crypto::PkSigning::from_seed(xsign_keys->private_master_key); + nlohmann::json j = ssss->keyDescription; + j.erase("signatures"); + ssss->keyDescription + .signatures[http::client()->user_id().to_string()]["ed25519:" + master.public_key()] = + master.sign(j.dump()); + + http::client()->upload_secret_storage_key( + ssss->keyDescription.name, ssss->keyDescription, [](mtx::http::RequestErr) {}); + http::client()->set_secret_storage_default_key(ssss->keyDescription.name, + [](mtx::http::RequestErr) {}); + + auto uploadSecret = [ssss](const std::string &key_name, const std::string &secret) { + mtx::secret_storage::Secret s; + s.encrypted[ssss->keyDescription.name] = + mtx::crypto::encrypt(secret, ssss->privateKey, key_name); + http::client()->upload_secret_storage_secret( + key_name, s, [key_name](mtx::http::RequestErr) { + nhlog::crypto()->info("Uploaded secret: {}", key_name); + }); + }; + + uploadSecret(mtx::secret_storage::secrets::cross_signing_master, + xsign_keys->private_master_key); + uploadSecret(mtx::secret_storage::secrets::cross_signing_self_signing, + xsign_keys->private_self_signing_key); + uploadSecret(mtx::secret_storage::secrets::cross_signing_user_signing, + xsign_keys->private_user_signing_key); + + if (okb) + uploadSecret(mtx::secret_storage::secrets::megolm_backup_v1, + mtx::crypto::bin2base64(mtx::crypto::to_string(okb->privateKey))); + } + + mtx::requests::DeviceSigningUpload device_sign{}; + device_sign.master_key = xsign_keys->master_key; + device_sign.self_signing_key = xsign_keys->self_signing_key; + device_sign.user_signing_key = xsign_keys->user_signing_key; + http::client()->device_signing_upload( + device_sign, + UIA::instance()->genericHandler(tr("Encryption Setup")), + [this, ssss, xsign_keys](mtx::http::RequestErr e) { + if (e) { + nhlog::crypto()->critical("Failed to upload cross signing keys: {}", + e->matrix_error.error); + + emit setupFailed(tr("Encryption setup failed: %1") + .arg(QString::fromStdString(e->matrix_error.error))); + return; + } + nhlog::crypto()->info("Crosssigning keys uploaded!"); + + auto deviceKeys = cache::client()->userKeys(http::client()->user_id().to_string()); + if (deviceKeys) { + auto myKey = deviceKeys->device_keys.at(http::client()->device_id()); + if (myKey.user_id == http::client()->user_id().to_string() && + myKey.device_id == http::client()->device_id() && + myKey.keys["ed25519:" + http::client()->device_id()] == + olm::client()->identity_keys().ed25519 && + myKey.keys["curve25519:" + http::client()->device_id()] == + olm::client()->identity_keys().curve25519) { + json j = myKey; + j.erase("signatures"); + j.erase("unsigned"); + + auto ssk = + mtx::crypto::PkSigning::from_seed(xsign_keys->private_self_signing_key); + myKey.signatures[http::client()->user_id().to_string()] + ["ed25519:" + ssk.public_key()] = ssk.sign(j.dump()); + mtx::requests::KeySignaturesUpload req; + req.signatures[http::client()->user_id().to_string()] + [http::client()->device_id()] = myKey; + + http::client()->keys_signatures_upload( + req, + [](const mtx::responses::KeySignaturesUpload &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error("failed to upload signatures: {},{}", + mtx::errors::to_string(err->matrix_error.errcode), + static_cast(err->status_code)); + } + + for (const auto &[user_id, tmp] : res.errors) + for (const auto &[key_id, e] : tmp) + nhlog::net()->error("signature error for user {} and key " + "id {}: {}, {}", + user_id, + key_id, + mtx::errors::to_string(e.errcode), + e.error); + }); + } + } + + if (ssss) { + auto k = QString::fromStdString(mtx::crypto::key_to_recoverykey(ssss->privateKey)); + + QString r; + for (int i = 0; i < k.size(); i += 4) + r += k.mid(i, 4) + " "; + + emit showRecoveryKey(r.trimmed()); + } else { + emit setupCompleted(); + } + }); +} + +void +SelfVerificationStatus::verifyMasterKey() +{ + nhlog::db()->info("Clicked verify master key"); +} + +void +SelfVerificationStatus::verifyUnverifiedDevices() +{ + nhlog::db()->info("Clicked verify unverified devices"); +} + +void +SelfVerificationStatus::invalidate() +{ + nhlog::db()->info("Invalidating self verification status"); + auto keys = cache::client()->userKeys(http::client()->user_id().to_string()); + if (!keys) { + cache::client()->query_keys(http::client()->user_id().to_string(), + [](const UserKeyCache &, mtx::http::RequestErr) {}); + return; + } + + if (keys->master_keys.keys.empty()) { + if (status_ != SelfVerificationStatus::NoMasterKey) { + this->status_ = SelfVerificationStatus::NoMasterKey; + emit statusChanged(); + } + return; + } + + auto verifStatus = cache::client()->verificationStatus(http::client()->user_id().to_string()); + + if (!verifStatus.user_verified) { + if (status_ != SelfVerificationStatus::UnverifiedMasterKey) { + this->status_ = SelfVerificationStatus::UnverifiedMasterKey; + emit statusChanged(); + } + return; + } + + if (verifStatus.unverified_device_count > 0) { + if (status_ != SelfVerificationStatus::UnverifiedDevices) { + this->status_ = SelfVerificationStatus::UnverifiedDevices; + emit statusChanged(); + } + return; + } + + if (status_ != SelfVerificationStatus::AllVerified) { + this->status_ = SelfVerificationStatus::AllVerified; + emit statusChanged(); + return; + } +} diff --git a/src/SelfVerificationStatus.h b/src/SelfVerificationStatus.h new file mode 100644 index 00000000..8cb54df6 --- /dev/null +++ b/src/SelfVerificationStatus.h @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +class SelfVerificationStatus : public QObject +{ + Q_OBJECT + + Q_PROPERTY(Status status READ status NOTIFY statusChanged) + +public: + SelfVerificationStatus(QObject *o = nullptr); + enum Status + { + AllVerified, + NoMasterKey, + UnverifiedMasterKey, + UnverifiedDevices, + }; + Q_ENUM(Status) + + Q_INVOKABLE void setupCrosssigning(bool useSSSS, QString password, bool useOnlineKeyBackup); + Q_INVOKABLE void verifyMasterKey(); + Q_INVOKABLE void verifyUnverifiedDevices(); + + Status status() const { return status_; } + +signals: + void statusChanged(); + void setupCompleted(); + void showRecoveryKey(QString key); + void setupFailed(QString message); + +public slots: + void invalidate(); + +private: + Status status_ = AllVerified; +}; diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 8a33dc2b..df8210d3 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -29,6 +29,7 @@ #include "ReadReceiptsModel.h" #include "RoomDirectoryModel.h" #include "RoomsModel.h" +#include "SelfVerificationStatus.h" #include "SingleImagePackModel.h" #include "UserSettingsPage.h" #include "UsersModel.h" @@ -40,6 +41,7 @@ #include "ui/NhekoCursorShape.h" #include "ui/NhekoDropArea.h" #include "ui/NhekoGlobalObject.h" +#include "ui/UIA.h" Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents) Q_DECLARE_METATYPE(std::vector) @@ -212,18 +214,9 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par "ReadReceiptsProxy needs to be instantiated on the C++ side"); static auto self = this; - qmlRegisterSingletonType( - "im.nheko", 1, 0, "MainWindow", [](QQmlEngine *, QJSEngine *) -> QObject * { - auto ptr = MainWindow::instance(); - QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership); - return ptr; - }); - qmlRegisterSingletonType( - "im.nheko", 1, 0, "TimelineManager", [](QQmlEngine *, QJSEngine *) -> QObject * { - auto ptr = self; - QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership); - return ptr; - }); + qmlRegisterSingletonInstance("im.nheko", 1, 0, "MainWindow", MainWindow::instance()); + qmlRegisterSingletonInstance("im.nheko", 1, 0, "TimelineManager", self); + qmlRegisterSingletonInstance("im.nheko", 1, 0, "UIA", UIA::instance()); qmlRegisterSingletonType( "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * { auto ptr = new FilteredRoomlistModel(self->rooms_); @@ -238,24 +231,11 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par &FilteredRoomlistModel::updateHiddenTagsAndSpaces); return ptr; }); - qmlRegisterSingletonType( - "im.nheko", 1, 0, "Communities", [](QQmlEngine *, QJSEngine *) -> QObject * { - auto ptr = self->communities_; - QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership); - return ptr; - }); - qmlRegisterSingletonType( - "im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * { - auto ptr = ChatPage::instance()->userSettings().data(); - QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership); - return ptr; - }); - qmlRegisterSingletonType( - "im.nheko", 1, 0, "CallManager", [](QQmlEngine *, QJSEngine *) -> QObject * { - auto ptr = ChatPage::instance()->callManager(); - QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership); - return ptr; - }); + qmlRegisterSingletonInstance("im.nheko", 1, 0, "Communities", self->communities_); + qmlRegisterSingletonInstance( + "im.nheko", 1, 0, "Settings", ChatPage::instance()->userSettings().data()); + qmlRegisterSingletonInstance( + "im.nheko", 1, 0, "CallManager", ChatPage::instance()->callManager()); qmlRegisterSingletonType( "im.nheko", 1, 0, "Clipboard", [](QQmlEngine *, QJSEngine *) -> QObject * { return new Clipboard(); @@ -264,6 +244,10 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par "im.nheko", 1, 0, "Nheko", [](QQmlEngine *, QJSEngine *) -> QObject * { return new Nheko(); }); + qmlRegisterSingletonType( + "im.nheko", 1, 0, "SelfVerificationStatus", [](QQmlEngine *, QJSEngine *) -> QObject * { + return new SelfVerificationStatus(); + }); qRegisterMetaType(); qRegisterMetaType>(); diff --git a/src/ui/UIA.cpp b/src/ui/UIA.cpp new file mode 100644 index 00000000..29161382 --- /dev/null +++ b/src/ui/UIA.cpp @@ -0,0 +1,136 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "UIA.h" + +#include + +#include +#include + +#include "Logging.h" +#include "MainWindow.h" +#include "dialogs/FallbackAuth.h" +#include "dialogs/ReCaptcha.h" + +UIA * +UIA::instance() +{ + static UIA uia; + return &uia; +} + +mtx::http::UIAHandler +UIA::genericHandler(QString context) +{ + return mtx::http::UIAHandler([this, context](const mtx::http::UIAHandler &h, + const mtx::user_interactive::Unauthorized &u) { + QTimer::singleShot(0, this, [this, h, u, context]() { + this->currentHandler = h; + this->currentStatus = u; + this->title_ = context; + emit titleChanged(); + + std::vector flows = u.flows; + + nhlog::ui()->info("Completed stages: {}", u.completed.size()); + + if (!u.completed.empty()) { + // Get rid of all flows which don't start with the sequence of + // stages that have already been completed. + flows.erase(std::remove_if(flows.begin(), + flows.end(), + [completed_stages = u.completed](auto flow) { + if (completed_stages.size() > flow.stages.size()) + return true; + for (size_t f = 0; f < completed_stages.size(); f++) + if (completed_stages[f] != flow.stages[f]) + return true; + return false; + }), + flows.end()); + } + + if (flows.empty()) { + nhlog::ui()->error("No available registration flows!"); + return; + } + + auto current_stage = flows.front().stages.at(u.completed.size()); + + if (current_stage == mtx::user_interactive::auth_types::password) { + emit password(); + } else if (current_stage == mtx::user_interactive::auth_types::recaptcha) { + auto captchaDialog = + new dialogs::ReCaptcha(QString::fromStdString(u.session), MainWindow::instance()); + captchaDialog->setWindowTitle(context); + + connect( + captchaDialog, &dialogs::ReCaptcha::confirmation, this, [captchaDialog, h, u]() { + captchaDialog->close(); + captchaDialog->deleteLater(); + h.next(mtx::user_interactive::Auth{u.session, + mtx::user_interactive::auth::Fallback{}}); + }); + + // connect( + // captchaDialog, &dialogs::ReCaptcha::cancel, this, &RegisterPage::errorOccurred); + + QTimer::singleShot(0, this, [captchaDialog]() { captchaDialog->show(); }); + + } else if (current_stage == mtx::user_interactive::auth_types::dummy) { + h.next( + mtx::user_interactive::Auth{u.session, mtx::user_interactive::auth::Dummy{}}); + + } else if (current_stage == mtx::user_interactive::auth_types::registration_token) { + bool ok; + QString token = + QInputDialog::getText(MainWindow::instance(), + context, + tr("Please enter a valid registration token."), + QLineEdit::Normal, + QString(), + &ok); + + if (ok) { + h.next(mtx::user_interactive::Auth{ + u.session, + mtx::user_interactive::auth::RegistrationToken{token.toStdString()}}); + } else { + // emit errorOccurred(); + } + } else { + // use fallback + auto dialog = new dialogs::FallbackAuth(QString::fromStdString(current_stage), + QString::fromStdString(u.session), + MainWindow::instance()); + dialog->setWindowTitle(context); + + connect(dialog, &dialogs::FallbackAuth::confirmation, this, [h, u, dialog]() { + dialog->close(); + dialog->deleteLater(); + h.next(mtx::user_interactive::Auth{u.session, + mtx::user_interactive::auth::Fallback{}}); + }); + + // connect(dialog, &dialogs::FallbackAuth::cancel, this, + // &RegisterPage::errorOccurred); + + dialog->show(); + } + }); + }); +} + +void +UIA::continuePassword(QString password) +{ + mtx::user_interactive::auth::Password p{}; + p.identifier_type = mtx::user_interactive::auth::Password::UserId; + p.password = password.toStdString(); + p.identifier_user = http::client()->user_id().to_string(); + + if (currentHandler) + currentHandler->next(mtx::user_interactive::Auth{currentStatus.session, p}); +} diff --git a/src/ui/UIA.h b/src/ui/UIA.h new file mode 100644 index 00000000..fb047451 --- /dev/null +++ b/src/ui/UIA.h @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include + +class UIA : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString title READ title NOTIFY titleChanged) + +public: + static UIA *instance(); + + UIA(QObject *parent = nullptr) + : QObject(parent) + {} + + mtx::http::UIAHandler genericHandler(QString context); + + QString title() const { return title_; } + +public slots: + void continuePassword(QString password); + +signals: + void password(); + + void titleChanged(); + +private: + std::optional currentHandler; + mtx::user_interactive::Unauthorized currentStatus; + QString title_; +};