Support bootstrapping crosssigning

Showing the bootstrap state and showing there are unverified devices is
still missing.
This commit is contained in:
Nicolas Werner 2021-09-18 00:21:14 +02:00
parent c514acb7c5
commit ad1e6c8298
No known key found for this signature in database
GPG key ID: C8D75E610773F2D9
20 changed files with 834 additions and 68 deletions

View file

@ -328,6 +328,7 @@ set(SRC_FILES
src/ui/Theme.cpp src/ui/Theme.cpp
src/ui/ThemeManager.cpp src/ui/ThemeManager.cpp
src/ui/ToggleButton.cpp src/ui/ToggleButton.cpp
src/ui/UIA.cpp
src/ui/UserProfile.cpp src/ui/UserProfile.cpp
# Generic notification stuff # Generic notification stuff
@ -365,6 +366,7 @@ set(SRC_FILES
src/RoomDirectoryModel.cpp src/RoomDirectoryModel.cpp
src/RoomsModel.cpp src/RoomsModel.cpp
src/Utils.cpp src/Utils.cpp
src/SelfVerificationStatus.cpp
src/WebRTCSession.cpp src/WebRTCSession.cpp
src/WelcomePage.cpp src/WelcomePage.cpp
src/main.cpp src/main.cpp
@ -385,7 +387,7 @@ if(USE_BUNDLED_MTXCLIENT)
FetchContent_Declare( FetchContent_Declare(
MatrixClient MatrixClient
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git 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_EXAMPLES OFF CACHE INTERNAL "")
set(BUILD_LIB_TESTS 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/Theme.h
src/ui/ThemeManager.h src/ui/ThemeManager.h
src/ui/ToggleButton.h src/ui/ToggleButton.h
src/ui/UIA.h
src/ui/UserProfile.h src/ui/UserProfile.h
src/notifications/Manager.h src/notifications/Manager.h
@ -573,6 +576,7 @@ qt5_wrap_cpp(MOC_HEADERS
src/UsersModel.h src/UsersModel.h
src/RoomDirectoryModel.h src/RoomDirectoryModel.h
src/RoomsModel.h src/RoomsModel.h
src/SelfVerificationStatus.h
src/WebRTCSession.h src/WebRTCSession.h
src/WelcomePage.h src/WelcomePage.h
src/ReadReceiptsModel.h src/ReadReceiptsModel.h

View file

@ -163,7 +163,7 @@ modules:
buildsystem: cmake-ninja buildsystem: cmake-ninja
name: mtxclient name: mtxclient
sources: sources:
- commit: 4a598632f432953f4dbfacf6cfed4f85a1c59c5a - commit: 8b56b466dbacde501ed9087d53bb4f51b297eca8
type: git type: git
url: https://github.com/Nheko-Reborn/mtxclient.git url: https://github.com/Nheko-Reborn/mtxclient.git
- config-opts: - config-opts:

View file

@ -42,6 +42,7 @@ Rectangle {
Image { Image {
id: identicon id: identicon
anchors.fill: parent anchors.fill: parent
visible: Settings.useIdenticon && img.status != Image.Ready visible: Settings.useIdenticon && img.status != Image.Ready
source: Settings.useIdenticon ? ("image://jdenticon/" + (userid !== "" ? userid : roomid) + "?radius=" + (Settings.avatarCircles ? 100 : 25)) : "" source: Settings.useIdenticon ? ("image://jdenticon/" + (userid !== "" ? userid : roomid) + "?radius=" + (Settings.avatarCircles ? 100 : 25)) : ""

View file

@ -2,12 +2,15 @@
// //
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.9 import QtQuick 2.15
import QtQuick.Controls 2.5 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import "components" import "components"
import im.nheko 1.0 import im.nheko 1.0
// this needs to be last
import QtQml 2.15
Rectangle { Rectangle {
id: chatPage id: chatPage
@ -41,6 +44,7 @@ Rectangle {
value: communityListC.preferredWidth value: communityListC.preferredWidth
when: !adaptiveView.singlePageMode when: !adaptiveView.singlePageMode
delayed: true delayed: true
restoreMode: Binding.RestoreBindingOrValue
} }
} }
@ -66,6 +70,7 @@ Rectangle {
value: roomListC.preferredWidth value: roomListC.preferredWidth
when: !adaptiveView.singlePageMode when: !adaptiveView.singlePageMode
delayed: true delayed: true
restoreMode: Binding.RestoreBindingOrValue
} }
} }

View file

@ -21,11 +21,10 @@ ScrollView {
ListView { ListView {
id: chat 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 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 model: room
// reuseItems still has a few bugs, see https://bugreports.qt.io/browse/QTBUG-95105 https://bugreports.qt.io/browse/QTBUG-95107 // 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() //onModelChanged: if (room) room.sendReset()
@ -415,8 +414,6 @@ ScrollView {
Loader { Loader {
id: section id: section
z: 4
property int parentWidth: parent.width property int parentWidth: parent.width
property string userId: wrapper.userId property string userId: wrapper.userId
property string previousMessageUserId: wrapper.previousMessageUserId property string previousMessageUserId: wrapper.previousMessageUserId
@ -425,6 +422,7 @@ ScrollView {
property string userName: wrapper.userName property string userName: wrapper.userName
property var timestamp: wrapper.timestamp property var timestamp: wrapper.timestamp
z: 4
active: previousMessageUserId !== undefined && previousMessageUserId !== userId || previousMessageDay !== day active: previousMessageUserId !== undefined && previousMessageUserId !== userId || previousMessageDay !== day
//asynchronous: true //asynchronous: true
sourceComponent: sectionHeader sourceComponent: sectionHeader
@ -685,6 +683,7 @@ ScrollView {
text: qsTr("&Go to quoted message") text: qsTr("&Go to quoted message")
onTriggered: chat.model.showEvent(eventId) onTriggered: chat.model.showEvent(eventId)
} }
} }
} }

View file

@ -195,6 +195,29 @@ Page {
target: CallManager 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 { ChatPage {
anchors.fill: parent anchors.fill: parent
} }

View file

@ -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
}
}

View file

@ -2,12 +2,12 @@
// //
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
import Qt.labs.platform 1.1 as Platform
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2 import QtQuick.Layouts 1.2
import QtQuick.Window 2.13 import QtQuick.Window 2.13
import im.nheko 1.0 import im.nheko 1.0
import Qt.labs.platform 1.1 as Platform
Item { Item {
id: r id: r
@ -66,14 +66,8 @@ Item {
TapHandler { TapHandler {
acceptedButtons: Qt.RightButton acceptedButtons: Qt.RightButton
onLongPressed: replyContextMenu.show( onLongPressed: replyContextMenu.show(reply.child.copyText, reply.child.linkAt(eventPoint.position.x, eventPoint.position.y - userName_.implicitHeight))
reply.child.copyText, onSingleTapped: replyContextMenu.show(reply.child.copyText, reply.child.linkAt(eventPoint.position.x, eventPoint.position.y - userName_.implicitHeight))
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 gesturePolicy: TapHandler.ReleaseWithinBounds
} }
@ -88,6 +82,7 @@ Item {
onSingleTapped: chat.model.openUserProfile(userId) onSingleTapped: chat.model.openUserProfile(userId)
gesturePolicy: TapHandler.ReleaseWithinBounds gesturePolicy: TapHandler.ReleaseWithinBounds
} }
} }
MessageDelegate { MessageDelegate {
@ -118,6 +113,7 @@ Item {
width: parent.width width: parent.width
isReply: true isReply: true
} }
} }
Rectangle { Rectangle {

View file

@ -43,4 +43,5 @@ MatrixText {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
} }
} }

View file

@ -12,6 +12,7 @@ ApplicationWindow {
id: inputDialog id: inputDialog
property alias prompt: promptLabel.text property alias prompt: promptLabel.text
property alias echoMode: statusInput.echoMode
property var onAccepted: undefined property var onAccepted: undefined
modality: Qt.NonModal modality: Qt.NonModal

View file

@ -195,9 +195,9 @@ ApplicationWindow {
MatrixTextField { MatrixTextField {
id: chooseServer id: chooseServer
Layout.minimumWidth: 0.3 * header.width Layout.minimumWidth: 0.3 * header.width
Layout.maximumWidth: 0.3 * header.width Layout.maximumWidth: 0.3 * header.width
padding: Nheko.paddingMedium padding: Nheko.paddingMedium
color: Nheko.colors.text color: Nheko.colors.text
placeholderText: qsTr("Choose custom homeserver") placeholderText: qsTr("Choose custom homeserver")

View file

@ -35,8 +35,18 @@ ApplicationWindow {
onActivated: userProfileDialog.close() onActivated: userProfileDialog.close()
} }
ListView { 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 { ScrollHelper {
flickable: parent flickable: parent
@ -46,16 +56,17 @@ ApplicationWindow {
header: ColumnLayout { header: ColumnLayout {
id: contentL id: contentL
width: devicelist.width
width: devicelist.width
spacing: 10 spacing: 10
Avatar { Avatar {
id: displayAvatar
url: profile.avatarUrl.replace("mxc://", "image://MxcImage/") url: profile.avatarUrl.replace("mxc://", "image://MxcImage/")
height: 130 height: 130
width: 130 width: 130
displayName: profile.displayName displayName: profile.displayName
id: displayAvatar
userid: profile.userid userid: profile.userid
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
onClicked: TimelineManager.openImageOverlay(profile.avatarUrl, "") onClicked: TimelineManager.openImageOverlay(profile.avatarUrl, "")
@ -72,6 +83,7 @@ ApplicationWindow {
image: ":/icons/icons/ui/edit.png" image: ":/icons/icons/ui/edit.png"
onClicked: profile.changeAvatar() onClicked: profile.changeAvatar()
} }
} }
Spinner { Spinner {
@ -163,19 +175,22 @@ ApplicationWindow {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }
RowLayout { RowLayout {
visible: !profile.isGlobalUserProfile visible: !profile.isGlobalUserProfile
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
spacing: Nheko.paddingSmall spacing: Nheko.paddingSmall
MatrixText { MatrixText {
id: displayRoomname 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.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 ToolTip.visible: ma.hovered
HoverHandler { HoverHandler {
id: ma id: ma
} }
} }
ImageButton { ImageButton {
@ -185,6 +200,7 @@ ApplicationWindow {
ToolTip.text: qsTr("Open the global profile for this user.") ToolTip.text: qsTr("Open the global profile for this user.")
onClicked: profile.openGlobalProfile() onClicked: profile.openGlobalProfile()
} }
} }
Button { Button {
@ -254,27 +270,18 @@ ApplicationWindow {
hoverEnabled: true hoverEnabled: true
ToolTip.visible: hovered ToolTip.visible: hovered
ToolTip.text: qsTr("Refresh device list.") 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 { delegate: RowLayout {
required property int verificationStatus required property int verificationStatus
required property string deviceId required property string deviceId
required property string deviceName required property string deviceName
width: devicelist.width width: devicelist.width
spacing: 4 spacing: 4
@ -331,17 +338,19 @@ ApplicationWindow {
} }
} }
footerPositioning: ListView.OverlayFooter
footer: DialogButtonBox { footer: DialogButtonBox {
z: 2 z: 2
width: devicelist.width width: devicelist.width
alignment: Qt.AlignRight alignment: Qt.AlignRight
standardButtons: DialogButtonBox.Ok standardButtons: DialogButtonBox.Ok
onAccepted: userProfileDialog.close() onAccepted: userProfileDialog.close()
background: Rectangle { background: Rectangle {
anchors.fill: parent anchors.fill: parent
color: Nheko.colors.window color: Nheko.colors.window
} }
} }
} }

View file

@ -138,6 +138,7 @@
<file>qml/TopBar.qml</file> <file>qml/TopBar.qml</file>
<file>qml/QuickSwitcher.qml</file> <file>qml/QuickSwitcher.qml</file>
<file>qml/ForwardCompleter.qml</file> <file>qml/ForwardCompleter.qml</file>
<file>qml/SelfVerificationCheck.qml</file>
<file>qml/TypingIndicator.qml</file> <file>qml/TypingIndicator.qml</file>
<file>qml/NotificationWarning.qml</file> <file>qml/NotificationWarning.qml</file>
<file>qml/emoji/EmojiPicker.qml</file> <file>qml/emoji/EmojiPicker.qml</file>

View file

@ -201,6 +201,18 @@ Cache::Cache(const QString &userId, QObject *parent)
{ {
setup(); setup();
connect(this, &Cache::userKeysUpdate, this, &Cache::updateUserKeys, Qt::QueuedConnection); 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 void

View file

@ -310,6 +310,7 @@ signals:
void removeNotification(const QString &room_id, const QString &event_id); void removeNotification(const QString &room_id, const QString &event_id);
void userKeysUpdate(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery); void userKeysUpdate(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery);
void verificationStatusChanged(const std::string &userid); void verificationStatusChanged(const std::string &userid);
void selfUnverified();
void secretChanged(const std::string name); void secretChanged(const std::string name);
private: private:

View file

@ -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 <mtx/responses/common.hpp>
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<mtx::crypto::OlmClient::OnlineKeyBackupSetup> 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<mtx::crypto::OlmClient::SSSSSetup> 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<int>(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;
}
}

View file

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QObject>
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;
};

View file

@ -29,6 +29,7 @@
#include "ReadReceiptsModel.h" #include "ReadReceiptsModel.h"
#include "RoomDirectoryModel.h" #include "RoomDirectoryModel.h"
#include "RoomsModel.h" #include "RoomsModel.h"
#include "SelfVerificationStatus.h"
#include "SingleImagePackModel.h" #include "SingleImagePackModel.h"
#include "UserSettingsPage.h" #include "UserSettingsPage.h"
#include "UsersModel.h" #include "UsersModel.h"
@ -40,6 +41,7 @@
#include "ui/NhekoCursorShape.h" #include "ui/NhekoCursorShape.h"
#include "ui/NhekoDropArea.h" #include "ui/NhekoDropArea.h"
#include "ui/NhekoGlobalObject.h" #include "ui/NhekoGlobalObject.h"
#include "ui/UIA.h"
Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents) Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents)
Q_DECLARE_METATYPE(std::vector<DeviceInfo>) Q_DECLARE_METATYPE(std::vector<DeviceInfo>)
@ -212,18 +214,9 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
"ReadReceiptsProxy needs to be instantiated on the C++ side"); "ReadReceiptsProxy needs to be instantiated on the C++ side");
static auto self = this; static auto self = this;
qmlRegisterSingletonType<MainWindow>( qmlRegisterSingletonInstance("im.nheko", 1, 0, "MainWindow", MainWindow::instance());
"im.nheko", 1, 0, "MainWindow", [](QQmlEngine *, QJSEngine *) -> QObject * { qmlRegisterSingletonInstance("im.nheko", 1, 0, "TimelineManager", self);
auto ptr = MainWindow::instance(); qmlRegisterSingletonInstance("im.nheko", 1, 0, "UIA", UIA::instance());
QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
return ptr;
});
qmlRegisterSingletonType<TimelineViewManager>(
"im.nheko", 1, 0, "TimelineManager", [](QQmlEngine *, QJSEngine *) -> QObject * {
auto ptr = self;
QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
return ptr;
});
qmlRegisterSingletonType<RoomlistModel>( qmlRegisterSingletonType<RoomlistModel>(
"im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * { "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * {
auto ptr = new FilteredRoomlistModel(self->rooms_); auto ptr = new FilteredRoomlistModel(self->rooms_);
@ -238,24 +231,11 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
&FilteredRoomlistModel::updateHiddenTagsAndSpaces); &FilteredRoomlistModel::updateHiddenTagsAndSpaces);
return ptr; return ptr;
}); });
qmlRegisterSingletonType<RoomlistModel>( qmlRegisterSingletonInstance("im.nheko", 1, 0, "Communities", self->communities_);
"im.nheko", 1, 0, "Communities", [](QQmlEngine *, QJSEngine *) -> QObject * { qmlRegisterSingletonInstance(
auto ptr = self->communities_; "im.nheko", 1, 0, "Settings", ChatPage::instance()->userSettings().data());
QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership); qmlRegisterSingletonInstance(
return ptr; "im.nheko", 1, 0, "CallManager", ChatPage::instance()->callManager());
});
qmlRegisterSingletonType<UserSettings>(
"im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * {
auto ptr = ChatPage::instance()->userSettings().data();
QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
return ptr;
});
qmlRegisterSingletonType<CallManager>(
"im.nheko", 1, 0, "CallManager", [](QQmlEngine *, QJSEngine *) -> QObject * {
auto ptr = ChatPage::instance()->callManager();
QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
return ptr;
});
qmlRegisterSingletonType<Clipboard>( qmlRegisterSingletonType<Clipboard>(
"im.nheko", 1, 0, "Clipboard", [](QQmlEngine *, QJSEngine *) -> QObject * { "im.nheko", 1, 0, "Clipboard", [](QQmlEngine *, QJSEngine *) -> QObject * {
return new Clipboard(); return new Clipboard();
@ -264,6 +244,10 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
"im.nheko", 1, 0, "Nheko", [](QQmlEngine *, QJSEngine *) -> QObject * { "im.nheko", 1, 0, "Nheko", [](QQmlEngine *, QJSEngine *) -> QObject * {
return new Nheko(); return new Nheko();
}); });
qmlRegisterSingletonType<SelfVerificationStatus>(
"im.nheko", 1, 0, "SelfVerificationStatus", [](QQmlEngine *, QJSEngine *) -> QObject * {
return new SelfVerificationStatus();
});
qRegisterMetaType<mtx::events::collections::TimelineEvents>(); qRegisterMetaType<mtx::events::collections::TimelineEvents>();
qRegisterMetaType<std::vector<DeviceInfo>>(); qRegisterMetaType<std::vector<DeviceInfo>>();

136
src/ui/UIA.cpp Normal file
View file

@ -0,0 +1,136 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "UIA.h"
#include <algorithm>
#include <QInputDialog>
#include <QTimer>
#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<mtx::user_interactive::Flow> 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});
}

40
src/ui/UIA.h Normal file
View file

@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QObject>
#include <MatrixClient.h>
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<mtx::http::UIAHandler> currentHandler;
mtx::user_interactive::Unauthorized currentStatus;
QString title_;
};