Merge pull request #270 from Chethan2k1/device-verification
Device verification and Cross-Signing
This commit is contained in:
commit
517a126a44
32
.travis.yml
32
.travis.yml
@ -63,21 +63,21 @@ matrix:
|
||||
env:
|
||||
- CXX=g++-8
|
||||
- CC=gcc-8
|
||||
- QT_PKG=59
|
||||
- QT_PKG=510
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
- sourceline: 'ppa:beineri/opt-qt597-xenial'
|
||||
- sourceline: 'ppa:beineri/opt-qt-5.10.1-xenial'
|
||||
packages:
|
||||
- g++-8
|
||||
- ninja-build
|
||||
- qt59base
|
||||
- qt59tools
|
||||
- qt59svg
|
||||
- qt59multimedia
|
||||
- qt59quickcontrols2
|
||||
- qt59graphicaleffects
|
||||
- qt510base
|
||||
- qt510tools
|
||||
- qt510svg
|
||||
- qt510multimedia
|
||||
- qt510quickcontrols2
|
||||
- qt510graphicaleffects
|
||||
- liblmdb-dev
|
||||
- libgl1-mesa-dev # needed for missing gl.h
|
||||
- os: linux
|
||||
@ -85,23 +85,23 @@ matrix:
|
||||
env:
|
||||
- CXX=clang++-6.0
|
||||
- CC=clang-6.0
|
||||
- QT_PKG=59
|
||||
- QT_PKG=510
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
- llvm-toolchain-xenial-6.0
|
||||
- sourceline: 'ppa:beineri/opt-qt597-xenial'
|
||||
- sourceline: 'ppa:beineri/opt-qt-5.10.1-xenial'
|
||||
packages:
|
||||
- clang++-6.0
|
||||
- g++-7
|
||||
- ninja-build
|
||||
- qt59base
|
||||
- qt59tools
|
||||
- qt59svg
|
||||
- qt59multimedia
|
||||
- qt59quickcontrols2
|
||||
- qt59graphicaleffects
|
||||
- qt510base
|
||||
- qt510tools
|
||||
- qt510svg
|
||||
- qt510multimedia
|
||||
- qt510quickcontrols2
|
||||
- qt510graphicaleffects
|
||||
- liblmdb-dev
|
||||
- libgl1-mesa-dev # needed for missing gl.h
|
||||
- os: linux
|
||||
|
@ -142,9 +142,9 @@ if (APPLE)
|
||||
endif(APPLE)
|
||||
|
||||
if (Qt5Widgets_FOUND)
|
||||
if (Qt5Widgets_VERSION VERSION_LESS 5.9.0)
|
||||
if (Qt5Widgets_VERSION VERSION_LESS 5.10.0)
|
||||
message(STATUS "Qt version ${Qt5Widgets_VERSION}")
|
||||
message(WARNING "Minimum supported Qt5 version is 5.9!")
|
||||
message(WARNING "Minimum supported Qt5 version is 5.10!")
|
||||
endif()
|
||||
endif(Qt5Widgets_FOUND)
|
||||
|
||||
@ -239,7 +239,6 @@ set(SRC_FILES
|
||||
src/dialogs/ReCaptcha.cpp
|
||||
src/dialogs/ReadReceipts.cpp
|
||||
src/dialogs/RoomSettings.cpp
|
||||
src/dialogs/UserProfile.cpp
|
||||
|
||||
# Emoji
|
||||
src/emoji/Category.cpp
|
||||
@ -278,6 +277,7 @@ set(SRC_FILES
|
||||
src/ui/ToggleButton.cpp
|
||||
src/ui/Theme.cpp
|
||||
src/ui/ThemeManager.cpp
|
||||
src/ui/UserProfile.cpp
|
||||
|
||||
src/AvatarProvider.cpp
|
||||
src/BlurhashProvider.cpp
|
||||
@ -287,6 +287,7 @@ set(SRC_FILES
|
||||
src/ColorImageProvider.cpp
|
||||
src/CommunitiesList.cpp
|
||||
src/CommunitiesListItem.cpp
|
||||
src/DeviceVerificationFlow.cpp
|
||||
src/EventAccessors.cpp
|
||||
src/InviteeItem.cpp
|
||||
src/Logging.cpp
|
||||
@ -339,7 +340,7 @@ if(USE_BUNDLED_MTXCLIENT)
|
||||
FetchContent_Declare(
|
||||
MatrixClient
|
||||
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
|
||||
GIT_TAG 21a55ba65d0712a441fbef2af2ede66771430247
|
||||
GIT_TAG ad5575bc24089dc385e97d9ace026414b618775c
|
||||
)
|
||||
FetchContent_MakeAvailable(MatrixClient)
|
||||
else()
|
||||
@ -451,7 +452,6 @@ qt5_wrap_cpp(MOC_HEADERS
|
||||
src/dialogs/ReCaptcha.h
|
||||
src/dialogs/ReadReceipts.h
|
||||
src/dialogs/RoomSettings.h
|
||||
src/dialogs/UserProfile.h
|
||||
|
||||
# Emoji
|
||||
src/emoji/Category.h
|
||||
@ -487,6 +487,7 @@ qt5_wrap_cpp(MOC_HEADERS
|
||||
src/ui/ToggleButton.h
|
||||
src/ui/Theme.h
|
||||
src/ui/ThemeManager.h
|
||||
src/ui/UserProfile.h
|
||||
|
||||
src/notifications/Manager.h
|
||||
|
||||
@ -497,6 +498,7 @@ qt5_wrap_cpp(MOC_HEADERS
|
||||
src/ChatPage.h
|
||||
src/CommunitiesList.h
|
||||
src/CommunitiesListItem.h
|
||||
src/DeviceVerificationFlow.h
|
||||
src/InviteeItem.h
|
||||
src/LoginPage.h
|
||||
src/MainWindow.h
|
||||
|
@ -174,7 +174,7 @@ sudo pacman -S qt5-base \
|
||||
##### Gentoo Linux
|
||||
|
||||
```bash
|
||||
sudo emerge -a ">=dev-qt/qtgui-5.9.0" media-libs/fontconfig
|
||||
sudo emerge -a ">=dev-qt/qtgui-5.10.0" media-libs/fontconfig
|
||||
```
|
||||
|
||||
##### Ubuntu 20.04
|
||||
|
@ -73,9 +73,9 @@
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"sha256": "3dbcbfd8c07e25f5e0d662b194d3a7772ef214358c49ada23c044c4747ce8b19",
|
||||
"sha256": "5197b3147cfcfaa67dd564db7b878e4a4b3d9f3443801722b3915cdeced656cb",
|
||||
"type": "archive",
|
||||
"url": "https://github.com/gabime/spdlog/archive/v1.1.0.tar.gz"
|
||||
"url": "https://github.com/gabime/spdlog/archive/v1.8.1.tar.gz"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -146,7 +146,7 @@
|
||||
"name": "mtxclient",
|
||||
"sources": [
|
||||
{
|
||||
"commit": "21a55ba65d0712a441fbef2af2ede66771430247",
|
||||
"commit": "ad5575bc24089dc385e97d9ace026414b618775c",
|
||||
"type": "git",
|
||||
"url": "https://github.com/Nheko-Reborn/mtxclient.git"
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import im.nheko 1.0
|
||||
|
||||
Rectangle {
|
||||
id: activeCallBar
|
||||
visible: timelineManager.callState != WebRTCState.DISCONNECTED
|
||||
visible: TimelineManager.callState != WebRTCState.DISCONNECTED
|
||||
color: "#2ECC71"
|
||||
implicitHeight: rowLayout.height + 8
|
||||
|
||||
@ -21,13 +21,13 @@ Rectangle {
|
||||
width: avatarSize
|
||||
height: avatarSize
|
||||
|
||||
url: timelineManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
displayName: timelineManager.callPartyName
|
||||
url: TimelineManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
displayName: TimelineManager.callPartyName
|
||||
}
|
||||
|
||||
Label {
|
||||
font.pointSize: fontMetrics.font.pointSize * 1.1
|
||||
text: " " + timelineManager.callPartyName + " "
|
||||
text: " " + TimelineManager.callPartyName + " "
|
||||
}
|
||||
|
||||
Image {
|
||||
@ -42,7 +42,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: timelineManager
|
||||
target: TimelineManager
|
||||
function onCallStateChanged(state) {
|
||||
switch (state) {
|
||||
case WebRTCState.INITIATING:
|
||||
@ -69,7 +69,7 @@ Rectangle {
|
||||
id: callTimer
|
||||
property int startTime
|
||||
interval: 1000
|
||||
running: timelineManager.callState == WebRTCState.CONNECTED
|
||||
running: TimelineManager.callState == WebRTCState.CONNECTED
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
var d = new Date()
|
||||
@ -93,15 +93,15 @@ Rectangle {
|
||||
width: 24
|
||||
height: 24
|
||||
buttonTextColor: "#000000"
|
||||
image: timelineManager.isMicMuted ?
|
||||
image: TimelineManager.isMicMuted ?
|
||||
":/icons/icons/ui/microphone-unmute.png" :
|
||||
":/icons/icons/ui/microphone-mute.png"
|
||||
|
||||
hoverEnabled: true
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: timelineManager.isMicMuted ? qsTr("Unmute Mic") : qsTr("Mute Mic")
|
||||
ToolTip.text: TimelineManager.isMicMuted ? qsTr("Unmute Mic") : qsTr("Mute Mic")
|
||||
|
||||
onClicked: timelineManager.toggleMicMute()
|
||||
onClicked: TimelineManager.toggleMicMute()
|
||||
}
|
||||
|
||||
Item {
|
||||
|
@ -2,11 +2,13 @@ import QtQuick 2.6
|
||||
import QtQuick.Controls 2.3
|
||||
import QtGraphicalEffects 1.0
|
||||
|
||||
import im.nheko 1.0
|
||||
|
||||
Rectangle {
|
||||
id: avatar
|
||||
width: 48
|
||||
height: 48
|
||||
radius: settings.avatarCircles ? height/2 : 3
|
||||
radius: Settings.avatarCircles ? height/2 : 3
|
||||
|
||||
property alias url: img.source
|
||||
property string userid
|
||||
@ -14,7 +16,7 @@ Rectangle {
|
||||
|
||||
Label {
|
||||
anchors.fill: parent
|
||||
text: timelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "")
|
||||
text: TimelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "")
|
||||
textFormat: Text.RichText
|
||||
font.pixelSize: avatar.height/2
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
@ -40,7 +42,7 @@ Rectangle {
|
||||
anchors.fill: parent
|
||||
width: avatar.width
|
||||
height: avatar.height
|
||||
radius: settings.avatarCircles ? height/2 : 3
|
||||
radius: Settings.avatarCircles ? height/2 : 3
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,8 +56,8 @@ Rectangle {
|
||||
|
||||
height: avatar.height / 6
|
||||
width: height
|
||||
radius: settings.avatarCircles ? height / 2 : height / 4
|
||||
color: switch (timelineManager.userPresence(userid)) {
|
||||
radius: Settings.avatarCircles ? height / 2 : height / 4
|
||||
color: switch (TimelineManager.userPresence(userid)) {
|
||||
case "online": return "#00cc66"
|
||||
case "unavailable": return "#ff9933"
|
||||
case "offline": // return "#a82353" don't show anything if offline, since it is confusing, if presence is disabled
|
||||
|
@ -1,6 +1,8 @@
|
||||
import QtQuick 2.5
|
||||
import QtQuick.Controls 2.3
|
||||
|
||||
import im.nheko 1.0
|
||||
|
||||
TextEdit {
|
||||
textFormat: TextEdit.RichText
|
||||
readOnly: true
|
||||
@ -11,13 +13,13 @@ TextEdit {
|
||||
|
||||
onLinkActivated: {
|
||||
if (/^https:\/\/matrix.to\/#\/(@.*)$/.test(link)) chat.model.openUserProfile(/^https:\/\/matrix.to\/#\/(@.*)$/.exec(link)[1])
|
||||
else if (/^https:\/\/matrix.to\/#\/(![^\/]*)$/.test(link)) timelineManager.setHistoryView(/^https:\/\/matrix.to\/#\/(!.*)$/.exec(link)[1])
|
||||
else if (/^https:\/\/matrix.to\/#\/(![^\/]*)$/.test(link)) TimelineManager.setHistoryView(/^https:\/\/matrix.to\/#\/(!.*)$/.exec(link)[1])
|
||||
else if (/^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.test(link)) {
|
||||
var match = /^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.exec(link)
|
||||
timelineManager.setHistoryView(match[1])
|
||||
TimelineManager.setHistoryView(match[1])
|
||||
chat.positionViewAtIndex(chat.model.idToIndex(match[2]), ListView.Contain)
|
||||
}
|
||||
else timelineManager.openLink(link)
|
||||
else TimelineManager.openLink(link)
|
||||
}
|
||||
MouseArea
|
||||
{
|
||||
|
@ -1,6 +1,8 @@
|
||||
import QtQuick 2.6
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
import im.nheko 1.0
|
||||
|
||||
// This class is for showing Reactions in the timeline row, not for
|
||||
// adding new reactions via the emoji picker
|
||||
Flow {
|
||||
@ -12,7 +14,6 @@ Flow {
|
||||
property real highlightLight: colors.highlight.hslLightness
|
||||
|
||||
property string eventId
|
||||
property string roomId
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
@ -33,8 +34,8 @@ Flow {
|
||||
ToolTip.text: modelData.users
|
||||
|
||||
onClicked: {
|
||||
console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + " in room " + reactionFlow.roomId + ". selfReactedEvent: " + modelData.selfReactedEvent)
|
||||
timelineManager.queueReactionMessage(reactionFlow.eventId, modelData.key)
|
||||
console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + ". selfReactedEvent: " + modelData.selfReactedEvent)
|
||||
TimelineManager.queueReactionMessage(reactionFlow.eventId, modelData.key)
|
||||
}
|
||||
|
||||
|
||||
@ -46,7 +47,7 @@ Flow {
|
||||
|
||||
TextMetrics {
|
||||
id: textMetrics
|
||||
font.family: settings.emojiFont
|
||||
font.family: Settings.emojiFont
|
||||
elide: Text.ElideRight
|
||||
elideWidth: 150
|
||||
text: modelData.key
|
||||
@ -56,7 +57,7 @@ Flow {
|
||||
anchors.baseline: reactionCounter.baseline
|
||||
id: reactionText
|
||||
text: textMetrics.elidedText + (textMetrics.elidedText == modelData.key ? "" : "…")
|
||||
font.family: settings.emojiFont
|
||||
font.family: Settings.emojiFont
|
||||
color: reaction.hovered ? colors.highlight : colors.text
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ Item {
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
color: (settings.messageHoverHighlight && parent.containsMouse) ? colors.base : "transparent"
|
||||
color: (Settings.messageHoverHighlight && parent.containsMouse) ? colors.base : "transparent"
|
||||
anchors.fill: row
|
||||
}
|
||||
RowLayout {
|
||||
@ -48,8 +48,8 @@ Item {
|
||||
// fancy reply, if this is a reply
|
||||
Reply {
|
||||
visible: model.replyTo
|
||||
modelData: chat.model.getDump(model.replyTo, model.id)
|
||||
userColor: timelineManager.userColor(modelData.userId, colors.window)
|
||||
modelData: chat.model.getDump(model.replyTo,model.id)
|
||||
userColor: TimelineManager.userColor(modelData.userId, colors.window)
|
||||
}
|
||||
|
||||
// actual message content
|
||||
@ -64,7 +64,6 @@ Item {
|
||||
Reactions {
|
||||
id: reactionRow
|
||||
reactions: model.reactions
|
||||
roomId: model.roomId
|
||||
eventId: model.id
|
||||
}
|
||||
}
|
||||
@ -84,7 +83,7 @@ Item {
|
||||
width: 16
|
||||
}
|
||||
EmojiButton {
|
||||
visible: settings.buttonsInTimeline
|
||||
visible: Settings.buttonsInTimeline
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop
|
||||
Layout.preferredHeight: 16
|
||||
width: 16
|
||||
@ -96,7 +95,7 @@ Item {
|
||||
event_id: model.id
|
||||
}
|
||||
ImageButton {
|
||||
visible: settings.buttonsInTimeline
|
||||
visible: Settings.buttonsInTimeline
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop
|
||||
Layout.preferredHeight: 16
|
||||
width: 16
|
||||
@ -112,7 +111,7 @@ Item {
|
||||
onClicked: chat.model.replyAction(model.id)
|
||||
}
|
||||
ImageButton {
|
||||
visible: settings.buttonsInTimeline
|
||||
visible: Settings.buttonsInTimeline
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop
|
||||
Layout.preferredHeight: 16
|
||||
width: 16
|
||||
|
@ -9,6 +9,7 @@ import im.nheko.EmojiModel 1.0
|
||||
|
||||
import "./delegates"
|
||||
import "./emoji"
|
||||
import "./device-verification"
|
||||
|
||||
Page {
|
||||
id: timelineRoot
|
||||
@ -90,7 +91,7 @@ Page {
|
||||
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
|
||||
height: visible ? implicitHeight : 0
|
||||
text: qsTr("Save as")
|
||||
onTriggered: timelineManager.timeline.saveMedia(messageContextMenu.eventId)
|
||||
onTriggered: TimelineManager.timeline.saveMedia(messageContextMenu.eventId)
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,8 +99,27 @@ Page {
|
||||
anchors.fill: parent
|
||||
color: colors.window
|
||||
|
||||
Component {
|
||||
id: deviceVerificationDialog
|
||||
DeviceVerification {}
|
||||
}
|
||||
Connections {
|
||||
target: TimelineManager
|
||||
function onNewDeviceVerificationRequest(flow,transactionId,userId,deviceId,isRequest) {
|
||||
var dialog = deviceVerificationDialog.createObject(timelineRoot, {flow: flow});
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: TimelineManager.timeline
|
||||
function onOpenProfile(profile) {
|
||||
var userProfile = userProfileComponent.createObject(timelineRoot,{profile: profile});
|
||||
userProfile.show();
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: !timelineManager.timeline && !timelineManager.isInitialSync
|
||||
visible: !TimelineManager.timeline && !TimelineManager.isInitialSync
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("No room open")
|
||||
font.pointSize: 24
|
||||
@ -109,7 +129,7 @@ Page {
|
||||
BusyIndicator {
|
||||
visible: running
|
||||
anchors.centerIn: parent
|
||||
running: timelineManager.isInitialSync
|
||||
running: TimelineManager.isInitialSync
|
||||
height: 200
|
||||
width: 200
|
||||
z: 3
|
||||
@ -128,7 +148,7 @@ Page {
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: timelineManager.openRoomSettings();
|
||||
onClicked: TimelineManager.openRoomSettings();
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
@ -149,14 +169,14 @@ Page {
|
||||
Layout.rowSpan: 2
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
visible: timelineManager.isNarrowView
|
||||
visible: TimelineManager.isNarrowView
|
||||
|
||||
image: ":/icons/icons/ui/angle-pointing-to-left.png"
|
||||
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Back to room list")
|
||||
|
||||
onClicked: timelineManager.backToRooms()
|
||||
onClicked: TimelineManager.backToRooms()
|
||||
}
|
||||
|
||||
Avatar {
|
||||
@ -173,7 +193,7 @@ Page {
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: timelineManager.openRoomSettings();
|
||||
onClicked: TimelineManager.openRoomSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,6 +201,7 @@ Page {
|
||||
Layout.fillWidth: true
|
||||
Layout.column: 2
|
||||
Layout.row: 0
|
||||
color: colors.text
|
||||
|
||||
font.pointSize: fontMetrics.font.pointSize * 1.1
|
||||
|
||||
@ -188,7 +209,7 @@ Page {
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: timelineManager.openRoomSettings();
|
||||
onClicked: TimelineManager.openRoomSettings();
|
||||
}
|
||||
}
|
||||
MatrixText {
|
||||
@ -220,19 +241,19 @@ Page {
|
||||
id: roomOptionsMenu
|
||||
MenuItem {
|
||||
text: qsTr("Invite users")
|
||||
onTriggered: timelineManager.openInviteUsersDialog();
|
||||
onTriggered: TimelineManager.openInviteUsersDialog();
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr("Members")
|
||||
onTriggered: timelineManager.openMemberListDialog();
|
||||
onTriggered: TimelineManager.openMemberListDialog();
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr("Leave room")
|
||||
onTriggered: timelineManager.openLeaveRoomDialog();
|
||||
onTriggered: TimelineManager.openLeaveRoomDialog();
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr("Settings")
|
||||
onTriggered: timelineManager.openRoomSettings();
|
||||
onTriggered: TimelineManager.openRoomSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -242,14 +263,14 @@ Page {
|
||||
ListView {
|
||||
id: chat
|
||||
|
||||
visible: !!timelineManager.timeline
|
||||
visible: TimelineManager.timeline != null
|
||||
|
||||
cacheBuffer: 400
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
model: timelineManager.timeline
|
||||
model: TimelineManager.timeline
|
||||
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
@ -293,7 +314,7 @@ Page {
|
||||
|
||||
onCountChanged: if (atYEnd) model.currentIndex = 0 // Mark last event as read, since we are at the bottom
|
||||
|
||||
property int delegateMaxWidth: (settings.timelineMaxWidth > 100 && (parent.width - settings.timelineMaxWidth) > scrollbar.width*2) ? settings.timelineMaxWidth : (parent.width - scrollbar.width*2)
|
||||
property int delegateMaxWidth: (Settings.timelineMaxWidth > 100 && (parent.width - Settings.timelineMaxWidth) > scrollbar.width*2) ? Settings.timelineMaxWidth : (parent.width - scrollbar.width*2)
|
||||
|
||||
delegate: Item {
|
||||
// This would normally be previousSection, but our model's order is inverted.
|
||||
@ -333,6 +354,11 @@ Page {
|
||||
}
|
||||
}
|
||||
|
||||
Component{
|
||||
id: userProfileComponent
|
||||
UserProfile{}
|
||||
}
|
||||
|
||||
section {
|
||||
property: "section"
|
||||
}
|
||||
@ -369,6 +395,7 @@ Page {
|
||||
color: colors.base
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
height: userName.height
|
||||
spacing: 8
|
||||
@ -390,26 +417,18 @@ Page {
|
||||
|
||||
Label {
|
||||
id: userName
|
||||
text: timelineManager.escapeEmoji(modelData.userName)
|
||||
color: timelineManager.userColor(modelData.userId, colors.window)
|
||||
text: TimelineManager.escapeEmoji(modelData.userName)
|
||||
color: TimelineManager.userColor(modelData.userId, colors.window)
|
||||
textFormat: Text.RichText
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: chat.model.openUserProfile(section.split(" ")[0])
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: chat.model.openUserProfile(modelData.userId)
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
propagateComposedEvents: true
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
color: colors.buttonText
|
||||
text: timelineManager.userStatus(modelData.userId)
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
width: chat.delegateMaxWidth - parent.spacing*2 - userName.implicitWidth - avatarSize
|
||||
font.italic: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -475,7 +494,7 @@ Page {
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
modelData: chat.model ? chat.model.getDump(chat.model.reply, chat.model.id) : {}
|
||||
userColor: timelineManager.userColor(modelData.userId, colors.window)
|
||||
userColor: TimelineManager.userColor(modelData.userId, colors.window)
|
||||
}
|
||||
|
||||
ImageButton {
|
||||
|
172
resources/qml/UserProfile.qml
Normal file
172
resources/qml/UserProfile.qml
Normal file
@ -0,0 +1,172 @@
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Window 2.3
|
||||
|
||||
import im.nheko 1.0
|
||||
|
||||
import "./device-verification"
|
||||
|
||||
ApplicationWindow{
|
||||
property var profile
|
||||
|
||||
id: userProfileDialog
|
||||
height: 650
|
||||
width: 420
|
||||
minimumHeight: 420
|
||||
|
||||
palette: colors
|
||||
|
||||
Component {
|
||||
id: deviceVerificationDialog
|
||||
DeviceVerification {}
|
||||
}
|
||||
|
||||
ColumnLayout{
|
||||
id: contentL
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
|
||||
spacing: 10
|
||||
|
||||
Avatar {
|
||||
url: profile.avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
height: 130
|
||||
width: 130
|
||||
displayName: profile.displayName
|
||||
userid: profile.userid
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Label {
|
||||
text: profile.displayName
|
||||
fontSizeMode: Text.HorizontalFit
|
||||
font.pixelSize: 20
|
||||
color: TimelineManager.userColor(profile.userid, colors.window)
|
||||
font.bold: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
text: profile.userid
|
||||
font.pixelSize: 15
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Button {
|
||||
id: verifyUserButton
|
||||
text: qsTr("Verify")
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
enabled: !profile.isUserVerified
|
||||
visible: !profile.isUserVerified
|
||||
|
||||
onClicked: profile.verify()
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 8
|
||||
|
||||
ImageButton {
|
||||
image:":/icons/icons/ui/do-not-disturb-rounded-sign.png"
|
||||
hoverEnabled: true
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Ban the user")
|
||||
onClicked: profile.banUser()
|
||||
}
|
||||
// ImageButton{
|
||||
// image:":/icons/icons/ui/volume-off-indicator.png"
|
||||
// Layout.margins: {
|
||||
// left: 5
|
||||
// right: 5
|
||||
// }
|
||||
// ToolTip.visible: hovered
|
||||
// ToolTip.text: qsTr("Ignore messages from this user")
|
||||
// onClicked : {
|
||||
// profile.ignoreUser()
|
||||
// }
|
||||
// }
|
||||
ImageButton{
|
||||
image:":/icons/icons/ui/black-bubble-speech.png"
|
||||
hoverEnabled: true
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Start a private chat")
|
||||
onClicked: profile.startChat()
|
||||
}
|
||||
ImageButton{
|
||||
image:":/icons/icons/ui/round-remove-button.png"
|
||||
hoverEnabled: true
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Kick the user")
|
||||
onClicked: profile.kickUser()
|
||||
}
|
||||
}
|
||||
|
||||
ListView{
|
||||
id: devicelist
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.minimumHeight: 200
|
||||
Layout.fillWidth: true
|
||||
|
||||
clip: true
|
||||
spacing: 8
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
model: profile.deviceList
|
||||
|
||||
delegate: RowLayout{
|
||||
width: devicelist.width
|
||||
spacing: 4
|
||||
|
||||
ColumnLayout{
|
||||
spacing: 0
|
||||
Text{
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
|
||||
elide: Text.ElideRight
|
||||
font.bold: true
|
||||
color: colors.text
|
||||
text: model.deviceId
|
||||
}
|
||||
Text{
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignRight
|
||||
|
||||
elide: Text.ElideRight
|
||||
color: colors.text
|
||||
text: model.deviceName
|
||||
}
|
||||
}
|
||||
|
||||
Image{
|
||||
Layout.preferredHeight: 16
|
||||
Layout.preferredWidth: 16
|
||||
|
||||
source: ((model.verificationStatus == VerificationStatus.VERIFIED)?"image://colorimage/:/icons/icons/ui/lock.png?green":
|
||||
((model.verificationStatus == VerificationStatus.UNVERIFIED)?"image://colorimage/:/icons/icons/ui/unlock.png?yellow":
|
||||
"image://colorimage/:/icons/icons/ui/unlock.png?red"))
|
||||
}
|
||||
Button{
|
||||
id: verifyButton
|
||||
text: (model.verificationStatus != VerificationStatus.VERIFIED)?"Verify":"Unverify"
|
||||
onClicked: {
|
||||
if(model.verificationStatus == VerificationStatus.VERIFIED){
|
||||
profile.unverify(model.deviceId)
|
||||
}else{
|
||||
profile.verify(model.deviceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer: DialogButtonBox {
|
||||
standardButtons: DialogButtonBox.Ok
|
||||
|
||||
onAccepted: userProfileDialog.close()
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import QtQuick 2.6
|
||||
import QtQuick.Layouts 1.2
|
||||
|
||||
import im.nheko 1.0
|
||||
|
||||
Item {
|
||||
height: row.height + 24
|
||||
width: parent ? parent.width : undefined
|
||||
@ -29,7 +31,7 @@ Item {
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: timelineManager.timeline.saveMedia(model.data.id)
|
||||
onClicked: TimelineManager.timeline.saveMedia(model.data.id)
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ Item {
|
||||
MouseArea {
|
||||
enabled: model.data.type == MtxEvent.ImageMessage && img.status == Image.Ready
|
||||
anchors.fill: parent
|
||||
onClicked: timelineManager.openImageOverlay(model.data.url, model.data.id)
|
||||
onClicked: TimelineManager.openImageOverlay(model.data.url, model.data.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,8 +36,8 @@ Item {
|
||||
DelegateChoice {
|
||||
roleValue: MtxEvent.EmoteMessage
|
||||
NoticeMessage {
|
||||
formatted: timelineManager.escapeEmoji(modelData.userName) + " " + model.data.formattedBody
|
||||
color: timelineManager.userColor(modelData.userId, colors.window)
|
||||
formatted: TimelineManager.escapeEmoji(modelData.userName) + " " + model.data.formattedBody
|
||||
color: TimelineManager.userColor(modelData.userId, colors.window)
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
@ -128,31 +128,85 @@ Item {
|
||||
// TODO: make a more complex formatter for the power levels.
|
||||
roleValue: MtxEvent.PowerLevels
|
||||
NoticeMessage {
|
||||
text: timelineManager.timeline.formatPowerLevelEvent(model.data.id)
|
||||
text: TimelineManager.timeline.formatPowerLevelEvent(model.data.id)
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: MtxEvent.RoomJoinRules
|
||||
NoticeMessage {
|
||||
text: timelineManager.timeline.formatJoinRuleEvent(model.data.id)
|
||||
text: TimelineManager.timeline.formatJoinRuleEvent(model.data.id)
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: MtxEvent.RoomHistoryVisibility
|
||||
NoticeMessage {
|
||||
text: timelineManager.timeline.formatHistoryVisibilityEvent(model.data.id)
|
||||
text: TimelineManager.timeline.formatHistoryVisibilityEvent(model.data.id)
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: MtxEvent.RoomGuestAccess
|
||||
NoticeMessage {
|
||||
text: timelineManager.timeline.formatGuestAccessEvent(model.data.id)
|
||||
text: TimelineManager.timeline.formatGuestAccessEvent(model.data.id)
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: MtxEvent.Member
|
||||
NoticeMessage {
|
||||
text: timelineManager.timeline.formatMemberEvent(model.data.id);
|
||||
text: TimelineManager.timeline.formatMemberEvent(model.data.id);
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: MtxEvent.KeyVerificationRequest
|
||||
NoticeMessage {
|
||||
text: "KeyVerificationRequest";
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: MtxEvent.KeyVerificationStart
|
||||
NoticeMessage {
|
||||
text: "KeyVerificationStart";
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: MtxEvent.KeyVerificationReady
|
||||
NoticeMessage {
|
||||
text: "KeyVerificationReady";
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: MtxEvent.KeyVerificationCancel
|
||||
NoticeMessage {
|
||||
text: "KeyVerificationCancel";
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: MtxEvent.KeyVerificationKey
|
||||
NoticeMessage {
|
||||
text: "KeyVerificationKey";
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: MtxEvent.KeyVerificationMac
|
||||
NoticeMessage {
|
||||
text: "KeyVerificationMac";
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: MtxEvent.KeyVerificationDone
|
||||
NoticeMessage {
|
||||
text: "KeyVerificationDone";
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: MtxEvent.KeyVerificationDone
|
||||
NoticeMessage {
|
||||
text: "KeyVerificationDone";
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: MtxEvent.KeyVerificationAccept
|
||||
NoticeMessage {
|
||||
text: "KeyVerificationAccept";
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
|
@ -106,7 +106,7 @@ Rectangle {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
switch (button.state) {
|
||||
case "": timelineManager.timeline.cacheMedia(model.data.id); break;
|
||||
case "": TimelineManager.timeline.cacheMedia(model.data.id); break;
|
||||
case "stopped":
|
||||
media.play(); console.log("play");
|
||||
button.state = "playing"
|
||||
@ -127,7 +127,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: timelineManager.timeline
|
||||
target: TimelineManager.timeline
|
||||
onMediaCached: {
|
||||
if (mxcUrl == model.data.url) {
|
||||
media.source = "file://" + cacheUrl
|
||||
|
@ -3,6 +3,8 @@ import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import im.nheko 1.0
|
||||
|
||||
Item {
|
||||
id: replyComponent
|
||||
|
||||
@ -26,7 +28,7 @@ Item {
|
||||
anchors.bottom: replyContainer.bottom
|
||||
width: 4
|
||||
|
||||
color: timelineManager.userColor(reply.modelData.userId, colors.window)
|
||||
color: TimelineManager.userColor(reply.modelData.userId, colors.window)
|
||||
}
|
||||
|
||||
Column {
|
||||
@ -37,7 +39,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: userName
|
||||
text: timelineManager.escapeEmoji(reply.modelData.userName)
|
||||
text: TimelineManager.escapeEmoji(reply.modelData.userName)
|
||||
color: replyComponent.userColor
|
||||
textFormat: Text.RichText
|
||||
|
||||
|
@ -1,10 +1,12 @@
|
||||
import ".."
|
||||
|
||||
import im.nheko 1.0
|
||||
|
||||
MatrixText {
|
||||
property string formatted: model.data.formattedBody
|
||||
text: "<style type=\"text/css\">a { color:"+colors.link+";}</style>" + formatted.replace("<pre>", "<pre style='white-space: pre-wrap'>")
|
||||
width: parent ? parent.width : undefined
|
||||
height: isReply ? Math.round(Math.min(timelineRoot.height / 8, implicitHeight)) : undefined
|
||||
clip: true
|
||||
font.pointSize: (settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? settings.fontSize * 3 : settings.fontSize
|
||||
font.pointSize: (Settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize
|
||||
}
|
||||
|
@ -0,0 +1,39 @@
|
||||
import QtQuick 2.3
|
||||
import QtQuick.Controls 2.10
|
||||
import QtQuick.Layouts 1.10
|
||||
|
||||
import im.nheko 1.0
|
||||
|
||||
Pane {
|
||||
property string title: qsTr("Awaiting Confirmation")
|
||||
ColumnLayout {
|
||||
spacing: 16
|
||||
Label {
|
||||
Layout.maximumWidth: 400
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
id: content
|
||||
text: qsTr("Waiting for other side to complete verification.")
|
||||
color:colors.text
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
BusyIndicator {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
RowLayout {
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
text: qsTr("Cancel")
|
||||
|
||||
onClicked: {
|
||||
flow.cancel();
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
97
resources/qml/device-verification/DeviceVerification.qml
Normal file
97
resources/qml/device-verification/DeviceVerification.qml
Normal file
@ -0,0 +1,97 @@
|
||||
import QtQuick 2.10
|
||||
import QtQuick.Controls 2.10
|
||||
import QtQuick.Window 2.10
|
||||
|
||||
import im.nheko 1.0
|
||||
|
||||
ApplicationWindow {
|
||||
property var flow
|
||||
|
||||
onClosing: TimelineManager.removeVerificationFlow(flow)
|
||||
|
||||
title: stack.currentItem.title
|
||||
id: dialog
|
||||
|
||||
flags: Qt.Dialog
|
||||
|
||||
palette: colors
|
||||
|
||||
height: stack.implicitHeight
|
||||
width: stack.implicitWidth
|
||||
|
||||
StackView {
|
||||
id: stack
|
||||
initialItem: newVerificationRequest
|
||||
implicitWidth: currentItem.implicitWidth
|
||||
implicitHeight: currentItem.implicitHeight
|
||||
}
|
||||
|
||||
Component{
|
||||
id: newVerificationRequest
|
||||
NewVerificationRequest {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: waiting
|
||||
Waiting {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: success
|
||||
Success {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: failed
|
||||
Failed {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: digitVerification
|
||||
DigitVerification {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: emojiVerification
|
||||
EmojiVerification {}
|
||||
}
|
||||
|
||||
Item {
|
||||
state: flow.state
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "PromptStartVerification"
|
||||
StateChangeScript { script: stack.replace(newVerificationRequest) }
|
||||
},
|
||||
State {
|
||||
name: "CompareEmoji"
|
||||
StateChangeScript { script: stack.replace(emojiVerification) }
|
||||
},
|
||||
State {
|
||||
name: "CompareNumber"
|
||||
StateChangeScript { script: stack.replace(digitVerification) }
|
||||
},
|
||||
State {
|
||||
name: "WaitingForKeys"
|
||||
StateChangeScript { script: stack.replace(waiting) }
|
||||
},
|
||||
State {
|
||||
name: "WaitingForOtherToAccept"
|
||||
StateChangeScript { script: stack.replace(waiting) }
|
||||
},
|
||||
State {
|
||||
name: "WaitingForMac"
|
||||
StateChangeScript { script: stack.replace(waiting) }
|
||||
},
|
||||
State {
|
||||
name: "Success"
|
||||
StateChangeScript { script: stack.replace(success) }
|
||||
},
|
||||
State {
|
||||
name: "Failed"
|
||||
StateChangeScript { script: stack.replace(failed); }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
60
resources/qml/device-verification/DigitVerification.qml
Normal file
60
resources/qml/device-verification/DigitVerification.qml
Normal file
@ -0,0 +1,60 @@
|
||||
import QtQuick 2.3
|
||||
import QtQuick.Controls 2.10
|
||||
import QtQuick.Layouts 1.10
|
||||
|
||||
import im.nheko 1.0
|
||||
|
||||
Pane {
|
||||
property string title: qsTr("Verification Code")
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 16
|
||||
Label {
|
||||
Layout.maximumWidth: 400
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
text: qsTr("Please verify the following digits. You should see the same numbers on both sides. If they differ, please press 'They do not match!' to abort verification!")
|
||||
color:colors.text
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Label {
|
||||
font.pixelSize: Qt.application.font.pixelSize * 2
|
||||
text: flow.sasList[0]
|
||||
color:colors.text
|
||||
}
|
||||
Label {
|
||||
font.pixelSize: Qt.application.font.pixelSize * 2
|
||||
text: flow.sasList[1]
|
||||
color:colors.text
|
||||
}
|
||||
Label {
|
||||
font.pixelSize: Qt.application.font.pixelSize * 2
|
||||
text: flow.sasList[2]
|
||||
color:colors.text
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
text: qsTr("They do not match!")
|
||||
|
||||
onClicked: {
|
||||
flow.cancel();
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
text: qsTr("They match!")
|
||||
|
||||
onClicked: flow.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
26
resources/qml/device-verification/EmojiElement.qml
Normal file
26
resources/qml/device-verification/EmojiElement.qml
Normal file
@ -0,0 +1,26 @@
|
||||
import QtQuick 2.3
|
||||
import QtQuick.Controls 2.10
|
||||
import QtQuick.Layouts 1.10
|
||||
|
||||
Rectangle {
|
||||
color: "red"
|
||||
implicitHeight: Qt.application.font.pixelSize * 4
|
||||
implicitWidth: col.width
|
||||
height: Qt.application.font.pixelSize * 4
|
||||
width: col.width
|
||||
ColumnLayout {
|
||||
id: col
|
||||
anchors.bottom: parent.bottom
|
||||
property var emoji: emojis.mapping[Math.floor(Math.random()*64)]
|
||||
Label {
|
||||
height: font.pixelSize * 2
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: col.emoji.emoji
|
||||
font.pixelSize: Qt.application.font.pixelSize * 2
|
||||
}
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
|
||||
text: col.emoji.description
|
||||
}
|
||||
}
|
||||
}
|
140
resources/qml/device-verification/EmojiVerification.qml
Normal file
140
resources/qml/device-verification/EmojiVerification.qml
Normal file
@ -0,0 +1,140 @@
|
||||
import QtQuick 2.3
|
||||
import QtQuick.Controls 2.10
|
||||
import QtQuick.Layouts 1.10
|
||||
|
||||
import im.nheko 1.0
|
||||
|
||||
Pane {
|
||||
property string title: qsTr("Verification Code")
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 16
|
||||
Label {
|
||||
Layout.maximumWidth: 400
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
text: qsTr("Please verify the following emoji. You should see the same emoji on both sides. If they differ, please press 'They do not match!' to abort verification!")
|
||||
color:colors.text
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
id: emojis
|
||||
property var mapping: [
|
||||
{"number": 0, "emoji": "🐶", "description": "Dog", "unicode": "U+1F436"},
|
||||
{"number": 1, "emoji": "🐱", "description": "Cat", "unicode": "U+1F431"},
|
||||
{"number": 2, "emoji": "🦁", "description": "Lion", "unicode": "U+1F981"},
|
||||
{"number": 3, "emoji": "🐎", "description": "Horse", "unicode": "U+1F40E"},
|
||||
{"number": 4, "emoji": "🦄", "description": "Unicorn", "unicode": "U+1F984"},
|
||||
{"number": 5, "emoji": "🐷", "description": "Pig", "unicode": "U+1F437"},
|
||||
{"number": 6, "emoji": "🐘", "description": "Elephant", "unicode": "U+1F418"},
|
||||
{"number": 7, "emoji": "🐰", "description": "Rabbit", "unicode": "U+1F430"},
|
||||
{"number": 8, "emoji": "🐼", "description": "Panda", "unicode": "U+1F43C"},
|
||||
{"number": 9, "emoji": "🐓", "description": "Rooster", "unicode": "U+1F413"},
|
||||
{"number": 10, "emoji": "🐧", "description": "Penguin", "unicode": "U+1F427"},
|
||||
{"number": 11, "emoji": "🐢", "description": "Turtle", "unicode": "U+1F422"},
|
||||
{"number": 12, "emoji": "🐟", "description": "Fish", "unicode": "U+1F41F"},
|
||||
{"number": 13, "emoji": "🐙", "description": "Octopus", "unicode": "U+1F419"},
|
||||
{"number": 14, "emoji": "🦋", "description": "Butterfly", "unicode": "U+1F98B"},
|
||||
{"number": 15, "emoji": "🌷", "description": "Flower", "unicode": "U+1F337"},
|
||||
{"number": 16, "emoji": "🌳", "description": "Tree", "unicode": "U+1F333"},
|
||||
{"number": 17, "emoji": "🌵", "description": "Cactus", "unicode": "U+1F335"},
|
||||
{"number": 18, "emoji": "🍄", "description": "Mushroom", "unicode": "U+1F344"},
|
||||
{"number": 19, "emoji": "🌏", "description": "Globe", "unicode": "U+1F30F"},
|
||||
{"number": 20, "emoji": "🌙", "description": "Moon", "unicode": "U+1F319"},
|
||||
{"number": 21, "emoji": "☁️", "description": "Cloud", "unicode": "U+2601U+FE0F"},
|
||||
{"number": 22, "emoji": "🔥", "description": "Fire", "unicode": "U+1F525"},
|
||||
{"number": 23, "emoji": "🍌", "description": "Banana", "unicode": "U+1F34C"},
|
||||
{"number": 24, "emoji": "🍎", "description": "Apple", "unicode": "U+1F34E"},
|
||||
{"number": 25, "emoji": "🍓", "description": "Strawberry", "unicode": "U+1F353"},
|
||||
{"number": 26, "emoji": "🌽", "description": "Corn", "unicode": "U+1F33D"},
|
||||
{"number": 27, "emoji": "🍕", "description": "Pizza", "unicode": "U+1F355"},
|
||||
{"number": 28, "emoji": "🎂", "description": "Cake", "unicode": "U+1F382"},
|
||||
{"number": 29, "emoji": "❤️", "description": "Heart", "unicode": "U+2764U+FE0F"},
|
||||
{"number": 30, "emoji": "😀", "description": "Smiley", "unicode": "U+1F600"},
|
||||
{"number": 31, "emoji": "🤖", "description": "Robot", "unicode": "U+1F916"},
|
||||
{"number": 32, "emoji": "🎩", "description": "Hat", "unicode": "U+1F3A9"},
|
||||
{"number": 33, "emoji": "👓", "description": "Glasses", "unicode": "U+1F453"},
|
||||
{"number": 34, "emoji": "🔧", "description": "Spanner", "unicode": "U+1F527"},
|
||||
{"number": 35, "emoji": "🎅", "description": "Santa", "unicode": "U+1F385"},
|
||||
{"number": 36, "emoji": "👍", "description": "Thumbs Up", "unicode": "U+1F44D"},
|
||||
{"number": 37, "emoji": "☂️", "description": "Umbrella", "unicode": "U+2602U+FE0F"},
|
||||
{"number": 38, "emoji": "⌛", "description": "Hourglass", "unicode": "U+231B"},
|
||||
{"number": 39, "emoji": "⏰", "description": "Clock", "unicode": "U+23F0"},
|
||||
{"number": 40, "emoji": "🎁", "description": "Gift", "unicode": "U+1F381"},
|
||||
{"number": 41, "emoji": "💡", "description": "Light Bulb", "unicode": "U+1F4A1"},
|
||||
{"number": 42, "emoji": "📕", "description": "Book", "unicode": "U+1F4D5"},
|
||||
{"number": 43, "emoji": "✏️", "description": "Pencil", "unicode": "U+270FU+FE0F"},
|
||||
{"number": 44, "emoji": "📎", "description": "Paperclip", "unicode": "U+1F4CE"},
|
||||
{"number": 45, "emoji": "✂️", "description": "Scissors", "unicode": "U+2702U+FE0F"},
|
||||
{"number": 46, "emoji": "🔒", "description": "Lock", "unicode": "U+1F512"},
|
||||
{"number": 47, "emoji": "🔑", "description": "Key", "unicode": "U+1F511"},
|
||||
{"number": 48, "emoji": "🔨", "description": "Hammer", "unicode": "U+1F528"},
|
||||
{"number": 49, "emoji": "☎️", "description": "Telephone", "unicode": "U+260EU+FE0F"},
|
||||
{"number": 50, "emoji": "🏁", "description": "Flag", "unicode": "U+1F3C1"},
|
||||
{"number": 51, "emoji": "🚂", "description": "Train", "unicode": "U+1F682"},
|
||||
{"number": 52, "emoji": "🚲", "description": "Bicycle", "unicode": "U+1F6B2"},
|
||||
{"number": 53, "emoji": "✈️", "description": "Aeroplane", "unicode": "U+2708U+FE0F"},
|
||||
{"number": 54, "emoji": "🚀", "description": "Rocket", "unicode": "U+1F680"},
|
||||
{"number": 55, "emoji": "🏆", "description": "Trophy", "unicode": "U+1F3C6"},
|
||||
{"number": 56, "emoji": "⚽", "description": "Ball", "unicode": "U+26BD"},
|
||||
{"number": 57, "emoji": "🎸", "description": "Guitar", "unicode": "U+1F3B8"},
|
||||
{"number": 58, "emoji": "🎺", "description": "Trumpet", "unicode": "U+1F3BA"},
|
||||
{"number": 59, "emoji": "🔔", "description": "Bell", "unicode": "U+1F514"},
|
||||
{"number": 60, "emoji": "⚓", "description": "Anchor", "unicode": "U+2693"},
|
||||
{"number": 61, "emoji": "🎧", "description": "Headphones", "unicode": "U+1F3A7"},
|
||||
{"number": 62, "emoji": "📁", "description": "Folder", "unicode": "U+1F4C1"},
|
||||
{"number": 63, "emoji": "📌", "description": "Pin", "unicode": "U+1F4CC"}
|
||||
]
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: 7
|
||||
delegate: Rectangle {
|
||||
color: "transparent"
|
||||
implicitHeight: Qt.application.font.pixelSize * 8
|
||||
implicitWidth: col.width
|
||||
ColumnLayout {
|
||||
id: col
|
||||
Layout.fillWidth: true
|
||||
anchors.bottom: parent.bottom
|
||||
property var emoji: emojis.mapping[flow.sasList[index]]
|
||||
Label {
|
||||
//height: font.pixelSize * 2
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: col.emoji.emoji
|
||||
font.pixelSize: Qt.application.font.pixelSize * 2
|
||||
font.family: Settings.emojiFont
|
||||
color:colors.text
|
||||
}
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
|
||||
text: col.emoji.description
|
||||
color:colors.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
text: qsTr("They do not match!")
|
||||
|
||||
onClicked: {
|
||||
flow.cancel();
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
text: qsTr("They match!")
|
||||
|
||||
onClicked: flow.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
44
resources/qml/device-verification/Failed.qml
Normal file
44
resources/qml/device-verification/Failed.qml
Normal file
@ -0,0 +1,44 @@
|
||||
import QtQuick 2.3
|
||||
import QtQuick.Controls 2.10
|
||||
import QtQuick.Layouts 1.10
|
||||
|
||||
import im.nheko 1.0
|
||||
|
||||
Pane {
|
||||
property string title: qsTr("Verification failed")
|
||||
ColumnLayout {
|
||||
spacing: 16
|
||||
Text {
|
||||
id: content
|
||||
|
||||
Layout.maximumWidth: 400
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
text: switch (flow.error) {
|
||||
case DeviceVerificationFlow.UnknownMethod: return qsTr("Other client does not support our verification protocol.")
|
||||
case DeviceVerificationFlow.MismatchedCommitment:
|
||||
case DeviceVerificationFlow.MismatchedSAS:
|
||||
case DeviceVerificationFlow.KeyMismatch: return qsTr("Key mismatch detected!")
|
||||
case DeviceVerificationFlow.Timeout: return qsTr("Device verification timed out.")
|
||||
case DeviceVerificationFlow.User: return qsTr("Other party canceled the verification.")
|
||||
case DeviceVerificationFlow.OutOfOrder: return qsTr("Device verification timed out.")
|
||||
default: return "Unknown verification error.";
|
||||
}
|
||||
color:colors.text
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
RowLayout {
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
text: qsTr("Close")
|
||||
|
||||
onClicked: dialog.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
44
resources/qml/device-verification/NewVerificationRequest.qml
Normal file
44
resources/qml/device-verification/NewVerificationRequest.qml
Normal file
@ -0,0 +1,44 @@
|
||||
import QtQuick 2.3
|
||||
import QtQuick.Controls 2.10
|
||||
import QtQuick.Layouts 1.10
|
||||
|
||||
import im.nheko 1.0
|
||||
|
||||
Pane {
|
||||
property string title: flow.sender ? qsTr("Send Device Verification Request") : qsTr("Recieved Device Verification Request")
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 16
|
||||
Label {
|
||||
Layout.maximumWidth: 400
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
text: flow.sender ?
|
||||
qsTr("To ensure that no malicious user can eavesdrop on your encrypted communications, you can verify this device.")
|
||||
: qsTr("The device was requested to be verified")
|
||||
color:colors.text
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
RowLayout {
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
text: flow.sender ? qsTr("Cancel") : qsTr("Deny")
|
||||
|
||||
onClicked: {
|
||||
flow.cancel();
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
text: flow.sender ? qsTr("Start verification") : qsTr("Accept")
|
||||
|
||||
onClicked: flow.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
31
resources/qml/device-verification/Success.qml
Normal file
31
resources/qml/device-verification/Success.qml
Normal file
@ -0,0 +1,31 @@
|
||||
import QtQuick 2.3
|
||||
import QtQuick.Controls 2.10
|
||||
import QtQuick.Layouts 1.10
|
||||
|
||||
Pane {
|
||||
property string title: qsTr("Successful Verification")
|
||||
ColumnLayout {
|
||||
spacing: 16
|
||||
Label {
|
||||
Layout.maximumWidth: 400
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
id: content
|
||||
text: qsTr("Verification successful! Both sides verified their devices!")
|
||||
color:colors.text
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
RowLayout {
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
text: qsTr("Close")
|
||||
|
||||
onClicked: dialog.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
45
resources/qml/device-verification/Waiting.qml
Normal file
45
resources/qml/device-verification/Waiting.qml
Normal file
@ -0,0 +1,45 @@
|
||||
import QtQuick 2.3
|
||||
import QtQuick.Controls 2.10
|
||||
import QtQuick.Layouts 1.10
|
||||
|
||||
import im.nheko 1.0
|
||||
|
||||
Pane {
|
||||
property string title: qsTr("Waiting for other party")
|
||||
ColumnLayout {
|
||||
spacing: 16
|
||||
Label {
|
||||
Layout.maximumWidth: 400
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
id: content
|
||||
text: switch (flow.state) {
|
||||
case "WaitingForOtherToAccept": return qsTr("Waiting for other side to accept the verification request.")
|
||||
case "WaitingForKeys": return qsTr("Waiting for other side to continue the verification request.")
|
||||
case "WaitingForMac": return qsTr("Waiting for other side to complete the verification request.")
|
||||
}
|
||||
|
||||
color: colors.text
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
BusyIndicator {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
palette: colors
|
||||
}
|
||||
RowLayout {
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
text: qsTr("Cancel")
|
||||
|
||||
onClicked: {
|
||||
flow.cancel();
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
66
resources/qml/device-verification/sas-emoji.json
Normal file
66
resources/qml/device-verification/sas-emoji.json
Normal file
@ -0,0 +1,66 @@
|
||||
[
|
||||
{"number": 0, "emoji": "🐶", "description": "Dog", "unicode": "U+1F436"},
|
||||
{"number": 1, "emoji": "🐱", "description": "Cat", "unicode": "U+1F431"},
|
||||
{"number": 2, "emoji": "🦁", "description": "Lion", "unicode": "U+1F981"},
|
||||
{"number": 3, "emoji": "🐎", "description": "Horse", "unicode": "U+1F40E"},
|
||||
{"number": 4, "emoji": "🦄", "description": "Unicorn", "unicode": "U+1F984"},
|
||||
{"number": 5, "emoji": "🐷", "description": "Pig", "unicode": "U+1F437"},
|
||||
{"number": 6, "emoji": "🐘", "description": "Elephant", "unicode": "U+1F418"},
|
||||
{"number": 7, "emoji": "🐰", "description": "Rabbit", "unicode": "U+1F430"},
|
||||
{"number": 8, "emoji": "🐼", "description": "Panda", "unicode": "U+1F43C"},
|
||||
{"number": 9, "emoji": "🐓", "description": "Rooster", "unicode": "U+1F413"},
|
||||
{"number": 10, "emoji": "🐧", "description": "Penguin", "unicode": "U+1F427"},
|
||||
{"number": 11, "emoji": "🐢", "description": "Turtle", "unicode": "U+1F422"},
|
||||
{"number": 12, "emoji": "🐟", "description": "Fish", "unicode": "U+1F41F"},
|
||||
{"number": 13, "emoji": "🐙", "description": "Octopus", "unicode": "U+1F419"},
|
||||
{"number": 14, "emoji": "🦋", "description": "Butterfly", "unicode": "U+1F98B"},
|
||||
{"number": 15, "emoji": "🌷", "description": "Flower", "unicode": "U+1F337"},
|
||||
{"number": 16, "emoji": "🌳", "description": "Tree", "unicode": "U+1F333"},
|
||||
{"number": 17, "emoji": "🌵", "description": "Cactus", "unicode": "U+1F335"},
|
||||
{"number": 18, "emoji": "🍄", "description": "Mushroom", "unicode": "U+1F344"},
|
||||
{"number": 19, "emoji": "🌏", "description": "Globe", "unicode": "U+1F30F"},
|
||||
{"number": 20, "emoji": "🌙", "description": "Moon", "unicode": "U+1F319"},
|
||||
{"number": 21, "emoji": "☁️", "description": "Cloud", "unicode": "U+2601U+FE0F"},
|
||||
{"number": 22, "emoji": "🔥", "description": "Fire", "unicode": "U+1F525"},
|
||||
{"number": 23, "emoji": "🍌", "description": "Banana", "unicode": "U+1F34C"},
|
||||
{"number": 24, "emoji": "🍎", "description": "Apple", "unicode": "U+1F34E"},
|
||||
{"number": 25, "emoji": "🍓", "description": "Strawberry", "unicode": "U+1F353"},
|
||||
{"number": 26, "emoji": "🌽", "description": "Corn", "unicode": "U+1F33D"},
|
||||
{"number": 27, "emoji": "🍕", "description": "Pizza", "unicode": "U+1F355"},
|
||||
{"number": 28, "emoji": "🎂", "description": "Cake", "unicode": "U+1F382"},
|
||||
{"number": 29, "emoji": "❤️", "description": "Heart", "unicode": "U+2764U+FE0F"},
|
||||
{"number": 30, "emoji": "😀", "description": "Smiley", "unicode": "U+1F600"},
|
||||
{"number": 31, "emoji": "🤖", "description": "Robot", "unicode": "U+1F916"},
|
||||
{"number": 32, "emoji": "🎩", "description": "Hat", "unicode": "U+1F3A9"},
|
||||
{"number": 33, "emoji": "👓", "description": "Glasses", "unicode": "U+1F453"},
|
||||
{"number": 34, "emoji": "🔧", "description": "Spanner", "unicode": "U+1F527"},
|
||||
{"number": 35, "emoji": "🎅", "description": "Santa", "unicode": "U+1F385"},
|
||||
{"number": 36, "emoji": "👍", "description": "Thumbs Up", "unicode": "U+1F44D"},
|
||||
{"number": 37, "emoji": "☂️", "description": "Umbrella", "unicode": "U+2602U+FE0F"},
|
||||
{"number": 38, "emoji": "⌛", "description": "Hourglass", "unicode": "U+231B"},
|
||||
{"number": 39, "emoji": "⏰", "description": "Clock", "unicode": "U+23F0"},
|
||||
{"number": 40, "emoji": "🎁", "description": "Gift", "unicode": "U+1F381"},
|
||||
{"number": 41, "emoji": "💡", "description": "Light Bulb", "unicode": "U+1F4A1"},
|
||||
{"number": 42, "emoji": "📕", "description": "Book", "unicode": "U+1F4D5"},
|
||||
{"number": 43, "emoji": "✏️", "description": "Pencil", "unicode": "U+270FU+FE0F"},
|
||||
{"number": 44, "emoji": "📎", "description": "Paperclip", "unicode": "U+1F4CE"},
|
||||
{"number": 45, "emoji": "✂️", "description": "Scissors", "unicode": "U+2702U+FE0F"},
|
||||
{"number": 46, "emoji": "🔒", "description": "Lock", "unicode": "U+1F512"},
|
||||
{"number": 47, "emoji": "🔑", "description": "Key", "unicode": "U+1F511"},
|
||||
{"number": 48, "emoji": "🔨", "description": "Hammer", "unicode": "U+1F528"},
|
||||
{"number": 49, "emoji": "☎️", "description": "Telephone", "unicode": "U+260EU+FE0F"},
|
||||
{"number": 50, "emoji": "🏁", "description": "Flag", "unicode": "U+1F3C1"},
|
||||
{"number": 51, "emoji": "🚂", "description": "Train", "unicode": "U+1F682"},
|
||||
{"number": 52, "emoji": "🚲", "description": "Bicycle", "unicode": "U+1F6B2"},
|
||||
{"number": 53, "emoji": "✈️", "description": "Aeroplane", "unicode": "U+2708U+FE0F"},
|
||||
{"number": 54, "emoji": "🚀", "description": "Rocket", "unicode": "U+1F680"},
|
||||
{"number": 55, "emoji": "🏆", "description": "Trophy", "unicode": "U+1F3C6"},
|
||||
{"number": 56, "emoji": "⚽", "description": "Ball", "unicode": "U+26BD"},
|
||||
{"number": 57, "emoji": "🎸", "description": "Guitar", "unicode": "U+1F3B8"},
|
||||
{"number": 58, "emoji": "🎺", "description": "Trumpet", "unicode": "U+1F3BA"},
|
||||
{"number": 59, "emoji": "🔔", "description": "Bell", "unicode": "U+1F514"},
|
||||
{"number": 60, "emoji": "⚓", "description": "Anchor", "unicode": "U+2693"},
|
||||
{"number": 61, "emoji": "🎧", "description": "Headphones", "unicode": "U+1F3A7"},
|
||||
{"number": 62, "emoji": "📁", "description": "Folder", "unicode": "U+1F4C1"},
|
||||
{"number": 63, "emoji": "📌", "description": "Pin", "unicode": "U+1F4CC"}
|
||||
]
|
@ -73,7 +73,7 @@ Popup {
|
||||
contentItem: Text {
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.family: settings.emojiFont
|
||||
font.family: Settings.emojiFont
|
||||
|
||||
font.pixelSize: 36
|
||||
text: model.unicode
|
||||
@ -104,7 +104,7 @@ Popup {
|
||||
onClicked: {
|
||||
console.debug("Picked " + model.unicode + "in response to " + emojiPopup.event_id)
|
||||
emojiPopup.close()
|
||||
timelineManager.queueReactionMessage(emojiPopup.event_id, model.unicode)
|
||||
TimelineManager.queueReactionMessage(emojiPopup.event_id, model.unicode)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -132,6 +132,7 @@
|
||||
<file>qml/TimelineRow.qml</file>
|
||||
<file>qml/emoji/EmojiButton.qml</file>
|
||||
<file>qml/emoji/EmojiPicker.qml</file>
|
||||
<file>qml/UserProfile.qml</file>
|
||||
<file>qml/delegates/MessageDelegate.qml</file>
|
||||
<file>qml/delegates/TextMessage.qml</file>
|
||||
<file>qml/delegates/NoticeMessage.qml</file>
|
||||
@ -141,6 +142,13 @@
|
||||
<file>qml/delegates/Pill.qml</file>
|
||||
<file>qml/delegates/Placeholder.qml</file>
|
||||
<file>qml/delegates/Reply.qml</file>
|
||||
<file>qml/device-verification/Waiting.qml</file>
|
||||
<file>qml/device-verification/DeviceVerification.qml</file>
|
||||
<file>qml/device-verification/DigitVerification.qml</file>
|
||||
<file>qml/device-verification/EmojiVerification.qml</file>
|
||||
<file>qml/device-verification/NewVerificationRequest.qml</file>
|
||||
<file>qml/device-verification/Failed.qml</file>
|
||||
<file>qml/device-verification/Success.qml</file>
|
||||
</qresource>
|
||||
<qresource prefix="/media">
|
||||
<file>media/ring.ogg</file>
|
||||
|
446
src/Cache.cpp
446
src/Cache.cpp
@ -31,8 +31,10 @@
|
||||
|
||||
#include "Cache.h"
|
||||
#include "Cache_p.h"
|
||||
#include "ChatPage.h"
|
||||
#include "EventAccessors.h"
|
||||
#include "Logging.h"
|
||||
#include "MatrixClient.h"
|
||||
#include "Olm.h"
|
||||
#include "Utils.h"
|
||||
|
||||
@ -89,6 +91,7 @@ Q_DECLARE_METATYPE(RoomMember)
|
||||
Q_DECLARE_METATYPE(mtx::responses::Timeline)
|
||||
Q_DECLARE_METATYPE(RoomSearchResult)
|
||||
Q_DECLARE_METATYPE(RoomInfo)
|
||||
Q_DECLARE_METATYPE(mtx::responses::QueryKeys)
|
||||
|
||||
namespace {
|
||||
std::unique_ptr<Cache> instance_ = nullptr;
|
||||
@ -153,6 +156,7 @@ Cache::Cache(const QString &userId, QObject *parent)
|
||||
, localUserId_{userId}
|
||||
{
|
||||
setup();
|
||||
connect(this, &Cache::userKeysUpdate, this, &Cache::updateUserKeys, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void
|
||||
@ -368,6 +372,25 @@ Cache::updateOutboundMegolmSession(const std::string &room_id, int message_index
|
||||
txn.commit();
|
||||
}
|
||||
|
||||
void
|
||||
Cache::dropOutboundMegolmSession(const std::string &room_id)
|
||||
{
|
||||
using namespace mtx::crypto;
|
||||
|
||||
if (!outboundMegolmSessionExists(room_id))
|
||||
return;
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx);
|
||||
session_storage.group_outbound_session_data.erase(room_id);
|
||||
session_storage.group_outbound_sessions.erase(room_id);
|
||||
|
||||
auto txn = lmdb::txn::begin(env_);
|
||||
lmdb::dbi_del(txn, outboundMegolmSessionDb_, lmdb::val(room_id), nullptr);
|
||||
txn.commit();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Cache::saveOutboundMegolmSession(const std::string &room_id,
|
||||
const OutboundGroupSessionData &data,
|
||||
@ -683,11 +706,14 @@ Cache::nextBatchToken() const
|
||||
auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
|
||||
lmdb::val token;
|
||||
|
||||
lmdb::dbi_get(txn, syncStateDb_, NEXT_BATCH_KEY, token);
|
||||
auto result = lmdb::dbi_get(txn, syncStateDb_, NEXT_BATCH_KEY, token);
|
||||
|
||||
txn.commit();
|
||||
|
||||
return std::string(token.data(), token.size());
|
||||
if (result)
|
||||
return std::string(token.data(), token.size());
|
||||
else
|
||||
return "";
|
||||
}
|
||||
|
||||
void
|
||||
@ -995,6 +1021,8 @@ Cache::saveState(const mtx::responses::Sync &res)
|
||||
using namespace mtx::events;
|
||||
auto user_id = this->localUserId_.toStdString();
|
||||
|
||||
auto currentBatchToken = nextBatchToken();
|
||||
|
||||
auto txn = lmdb::txn::begin(env_);
|
||||
|
||||
setNextBatchToken(txn, res.next_batch);
|
||||
@ -1012,6 +1040,8 @@ Cache::saveState(const mtx::responses::Sync &res)
|
||||
ev);
|
||||
}
|
||||
|
||||
auto userKeyCacheDb = getUserKeysDb(txn);
|
||||
|
||||
// Save joined rooms
|
||||
for (const auto &room : res.rooms.join) {
|
||||
auto statesdb = getStatesDb(txn, room.first);
|
||||
@ -1085,6 +1115,9 @@ Cache::saveState(const mtx::responses::Sync &res)
|
||||
|
||||
savePresence(txn, res.presence);
|
||||
|
||||
markUserKeysOutOfDate(txn, userKeyCacheDb, res.device_lists.changed, currentBatchToken);
|
||||
deleteUserKeys(txn, userKeyCacheDb, res.device_lists.left);
|
||||
|
||||
removeLeftRooms(txn, res.rooms.leave);
|
||||
|
||||
txn.commit();
|
||||
@ -3073,6 +3106,378 @@ Cache::statusMessage(const std::string &user_id)
|
||||
return status_msg;
|
||||
}
|
||||
|
||||
void
|
||||
to_json(json &j, const UserKeyCache &info)
|
||||
{
|
||||
j["device_keys"] = info.device_keys;
|
||||
j["master_keys"] = info.master_keys;
|
||||
j["user_signing_keys"] = info.user_signing_keys;
|
||||
j["self_signing_keys"] = info.self_signing_keys;
|
||||
j["updated_at"] = info.updated_at;
|
||||
j["last_changed"] = info.last_changed;
|
||||
}
|
||||
|
||||
void
|
||||
from_json(const json &j, UserKeyCache &info)
|
||||
{
|
||||
info.device_keys = j.value("device_keys", std::map<std::string, mtx::crypto::DeviceKeys>{});
|
||||
info.master_keys = j.value("master_keys", mtx::crypto::CrossSigningKeys{});
|
||||
info.user_signing_keys = j.value("user_signing_keys", mtx::crypto::CrossSigningKeys{});
|
||||
info.self_signing_keys = j.value("self_signing_keys", mtx::crypto::CrossSigningKeys{});
|
||||
info.updated_at = j.value("updated_at", "");
|
||||
info.last_changed = j.value("last_changed", "");
|
||||
}
|
||||
|
||||
std::optional<UserKeyCache>
|
||||
Cache::userKeys(const std::string &user_id)
|
||||
{
|
||||
lmdb::val keys;
|
||||
|
||||
try {
|
||||
auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
|
||||
auto db = getUserKeysDb(txn);
|
||||
auto res = lmdb::dbi_get(txn, db, lmdb::val(user_id), keys);
|
||||
|
||||
if (res) {
|
||||
return json::parse(std::string_view(keys.data(), keys.size()))
|
||||
.get<UserKeyCache>();
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
} catch (std::exception &) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery)
|
||||
{
|
||||
auto txn = lmdb::txn::begin(env_);
|
||||
auto db = getUserKeysDb(txn);
|
||||
|
||||
std::map<std::string, UserKeyCache> updates;
|
||||
|
||||
for (const auto &[user, keys] : keyQuery.device_keys)
|
||||
updates[user].device_keys = keys;
|
||||
for (const auto &[user, keys] : keyQuery.master_keys)
|
||||
updates[user].master_keys = keys;
|
||||
for (const auto &[user, keys] : keyQuery.user_signing_keys)
|
||||
updates[user].user_signing_keys = keys;
|
||||
for (const auto &[user, keys] : keyQuery.self_signing_keys)
|
||||
updates[user].self_signing_keys = keys;
|
||||
|
||||
for (auto &[user, update] : updates) {
|
||||
lmdb::val oldKeys;
|
||||
auto res = lmdb::dbi_get(txn, db, lmdb::val(user), oldKeys);
|
||||
|
||||
if (res) {
|
||||
auto last_changed =
|
||||
json::parse(std::string_view(oldKeys.data(), oldKeys.size()))
|
||||
.get<UserKeyCache>()
|
||||
.last_changed;
|
||||
// skip if we are tracking this and expect it to be up to date with the last
|
||||
// sync token
|
||||
if (!last_changed.empty() && last_changed != sync_token)
|
||||
continue;
|
||||
}
|
||||
lmdb::dbi_put(txn, db, lmdb::val(user), lmdb::val(json(update).dump()));
|
||||
}
|
||||
|
||||
txn.commit();
|
||||
|
||||
std::map<std::string, VerificationStatus> tmp;
|
||||
const auto local_user = utils::localUser().toStdString();
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx);
|
||||
for (auto &[user_id, update] : updates) {
|
||||
(void)update;
|
||||
if (user_id == local_user) {
|
||||
std::swap(tmp, verification_storage.status);
|
||||
} else {
|
||||
verification_storage.status.erase(user_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (auto &[user_id, update] : updates) {
|
||||
(void)update;
|
||||
if (user_id == local_user) {
|
||||
for (const auto &[user, status] : tmp) {
|
||||
(void)status;
|
||||
emit verificationStatusChanged(user);
|
||||
}
|
||||
} else {
|
||||
emit verificationStatusChanged(user_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Cache::deleteUserKeys(lmdb::txn &txn, lmdb::dbi &db, const std::vector<std::string> &user_ids)
|
||||
{
|
||||
for (const auto &user_id : user_ids)
|
||||
lmdb::dbi_del(txn, db, lmdb::val(user_id), nullptr);
|
||||
}
|
||||
|
||||
void
|
||||
Cache::markUserKeysOutOfDate(lmdb::txn &txn,
|
||||
lmdb::dbi &db,
|
||||
const std::vector<std::string> &user_ids,
|
||||
const std::string &sync_token)
|
||||
{
|
||||
mtx::requests::QueryKeys query;
|
||||
query.token = sync_token;
|
||||
|
||||
for (const auto &user : user_ids) {
|
||||
lmdb::val oldKeys;
|
||||
auto res = lmdb::dbi_get(txn, db, lmdb::val(user), oldKeys);
|
||||
|
||||
if (!res)
|
||||
continue;
|
||||
|
||||
auto cacheEntry =
|
||||
json::parse(std::string_view(oldKeys.data(), oldKeys.size())).get<UserKeyCache>();
|
||||
cacheEntry.last_changed = sync_token;
|
||||
lmdb::dbi_put(txn, db, lmdb::val(user), lmdb::val(json(cacheEntry).dump()));
|
||||
|
||||
query.device_keys[user] = {};
|
||||
}
|
||||
|
||||
if (!query.device_keys.empty())
|
||||
http::client()->query_keys(query,
|
||||
[this, sync_token](const mtx::responses::QueryKeys &keys,
|
||||
mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
nhlog::net()->warn(
|
||||
"failed to query device keys: {} {}",
|
||||
err->matrix_error.error,
|
||||
static_cast<int>(err->status_code));
|
||||
return;
|
||||
}
|
||||
|
||||
emit userKeysUpdate(sync_token, keys);
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
to_json(json &j, const VerificationCache &info)
|
||||
{
|
||||
j["device_verified"] = info.device_verified;
|
||||
j["device_blocked"] = info.device_blocked;
|
||||
}
|
||||
|
||||
void
|
||||
from_json(const json &j, VerificationCache &info)
|
||||
{
|
||||
info.device_verified = j.at("device_verified").get<std::vector<std::string>>();
|
||||
info.device_blocked = j.at("device_blocked").get<std::vector<std::string>>();
|
||||
}
|
||||
|
||||
std::optional<VerificationCache>
|
||||
Cache::verificationCache(const std::string &user_id)
|
||||
{
|
||||
lmdb::val verifiedVal;
|
||||
|
||||
auto txn = lmdb::txn::begin(env_);
|
||||
auto db = getVerificationDb(txn);
|
||||
|
||||
try {
|
||||
VerificationCache verified_state;
|
||||
auto res = lmdb::dbi_get(txn, db, lmdb::val(user_id), verifiedVal);
|
||||
if (res) {
|
||||
verified_state =
|
||||
json::parse(std::string_view(verifiedVal.data(), verifiedVal.size()));
|
||||
return verified_state;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
} catch (std::exception &) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Cache::markDeviceVerified(const std::string &user_id, const std::string &key)
|
||||
{
|
||||
lmdb::val val;
|
||||
|
||||
auto txn = lmdb::txn::begin(env_);
|
||||
auto db = getVerificationDb(txn);
|
||||
|
||||
try {
|
||||
VerificationCache verified_state;
|
||||
auto res = lmdb::dbi_get(txn, db, lmdb::val(user_id), val);
|
||||
if (res) {
|
||||
verified_state = json::parse(std::string_view(val.data(), val.size()));
|
||||
}
|
||||
|
||||
for (const auto &device : verified_state.device_verified)
|
||||
if (device == key)
|
||||
return;
|
||||
|
||||
verified_state.device_verified.push_back(key);
|
||||
lmdb::dbi_put(txn, db, lmdb::val(user_id), lmdb::val(json(verified_state).dump()));
|
||||
txn.commit();
|
||||
} catch (std::exception &) {
|
||||
}
|
||||
|
||||
const auto local_user = utils::localUser().toStdString();
|
||||
std::map<std::string, VerificationStatus> tmp;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx);
|
||||
if (user_id == local_user) {
|
||||
std::swap(tmp, verification_storage.status);
|
||||
} else {
|
||||
verification_storage.status.erase(user_id);
|
||||
}
|
||||
}
|
||||
if (user_id == local_user) {
|
||||
for (const auto &[user, status] : tmp) {
|
||||
(void)status;
|
||||
emit verificationStatusChanged(user);
|
||||
}
|
||||
} else {
|
||||
emit verificationStatusChanged(user_id);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Cache::markDeviceUnverified(const std::string &user_id, const std::string &key)
|
||||
{
|
||||
lmdb::val val;
|
||||
|
||||
auto txn = lmdb::txn::begin(env_);
|
||||
auto db = getVerificationDb(txn);
|
||||
|
||||
try {
|
||||
VerificationCache verified_state;
|
||||
auto res = lmdb::dbi_get(txn, db, lmdb::val(user_id), val);
|
||||
if (res) {
|
||||
verified_state = json::parse(std::string_view(val.data(), val.size()));
|
||||
}
|
||||
|
||||
verified_state.device_verified.erase(
|
||||
std::remove(verified_state.device_verified.begin(),
|
||||
verified_state.device_verified.end(),
|
||||
key),
|
||||
verified_state.device_verified.end());
|
||||
|
||||
lmdb::dbi_put(txn, db, lmdb::val(user_id), lmdb::val(json(verified_state).dump()));
|
||||
txn.commit();
|
||||
} catch (std::exception &) {
|
||||
}
|
||||
|
||||
const auto local_user = utils::localUser().toStdString();
|
||||
std::map<std::string, VerificationStatus> tmp;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx);
|
||||
if (user_id == local_user) {
|
||||
std::swap(tmp, verification_storage.status);
|
||||
} else {
|
||||
verification_storage.status.erase(user_id);
|
||||
}
|
||||
}
|
||||
if (user_id == local_user) {
|
||||
for (const auto &[user, status] : tmp) {
|
||||
(void)status;
|
||||
emit verificationStatusChanged(user);
|
||||
}
|
||||
} else {
|
||||
emit verificationStatusChanged(user_id);
|
||||
}
|
||||
}
|
||||
|
||||
VerificationStatus
|
||||
Cache::verificationStatus(const std::string &user_id)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx);
|
||||
if (verification_storage.status.count(user_id))
|
||||
return verification_storage.status.at(user_id);
|
||||
|
||||
VerificationStatus status;
|
||||
|
||||
if (auto verifCache = verificationCache(user_id)) {
|
||||
status.verified_devices = verifCache->device_verified;
|
||||
}
|
||||
|
||||
const auto local_user = utils::localUser().toStdString();
|
||||
|
||||
if (user_id == local_user)
|
||||
status.verified_devices.push_back(http::client()->device_id());
|
||||
|
||||
verification_storage.status[user_id] = status;
|
||||
|
||||
auto verifyAtLeastOneSig = [](const auto &toVerif,
|
||||
const std::map<std::string, std::string> &keys,
|
||||
const std::string &keyOwner) {
|
||||
if (!toVerif.signatures.count(keyOwner))
|
||||
return false;
|
||||
|
||||
for (const auto &[key_id, signature] : toVerif.signatures.at(keyOwner)) {
|
||||
if (!keys.count(key_id))
|
||||
continue;
|
||||
|
||||
if (mtx::crypto::ed25519_verify_signature(
|
||||
keys.at(key_id), json(toVerif), signature))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
try {
|
||||
// for local user verify this device_key -> our master_key -> our self_signing_key
|
||||
// -> our device_keys
|
||||
//
|
||||
// for other user verify this device_key -> our master_key -> our user_signing_key
|
||||
// -> their master_key -> their self_signing_key -> their device_keys
|
||||
//
|
||||
// This means verifying the other user adds 2 extra steps,verifying our user_signing
|
||||
// key and their master key
|
||||
auto ourKeys = userKeys(local_user);
|
||||
auto theirKeys = userKeys(user_id);
|
||||
if (!ourKeys || !theirKeys)
|
||||
return status;
|
||||
|
||||
if (!mtx::crypto::ed25519_verify_signature(
|
||||
olm::client()->identity_keys().ed25519,
|
||||
json(ourKeys->master_keys),
|
||||
ourKeys->master_keys.signatures.at(local_user)
|
||||
.at("ed25519:" + http::client()->device_id())))
|
||||
return status;
|
||||
|
||||
auto master_keys = ourKeys->master_keys.keys;
|
||||
|
||||
if (user_id != local_user) {
|
||||
if (!verifyAtLeastOneSig(
|
||||
ourKeys->user_signing_keys, master_keys, local_user))
|
||||
return status;
|
||||
|
||||
if (!verifyAtLeastOneSig(
|
||||
theirKeys->master_keys, ourKeys->user_signing_keys.keys, local_user))
|
||||
return status;
|
||||
|
||||
master_keys = theirKeys->master_keys.keys;
|
||||
}
|
||||
|
||||
status.user_verified = true;
|
||||
|
||||
if (!verifyAtLeastOneSig(theirKeys->self_signing_keys, master_keys, user_id))
|
||||
return status;
|
||||
|
||||
for (const auto &[device, device_key] : theirKeys->device_keys) {
|
||||
(void)device;
|
||||
if (verifyAtLeastOneSig(
|
||||
device_key, theirKeys->self_signing_keys.keys, user_id))
|
||||
status.verified_devices.push_back(device_key.device_id);
|
||||
}
|
||||
|
||||
verification_storage.status[user_id] = status;
|
||||
return status;
|
||||
} catch (std::exception &) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
to_json(json &j, const RoomInfo &info)
|
||||
{
|
||||
@ -3195,6 +3600,7 @@ init(const QString &user_id)
|
||||
qRegisterMetaType<QMap<QString, RoomInfo>>();
|
||||
qRegisterMetaType<std::map<QString, RoomInfo>>();
|
||||
qRegisterMetaType<std::map<QString, mtx::responses::Timeline>>();
|
||||
qRegisterMetaType<mtx::responses::QueryKeys>();
|
||||
|
||||
instance_ = std::make_unique<Cache>(user_id);
|
||||
}
|
||||
@ -3262,6 +3668,37 @@ populateMembers()
|
||||
instance_->populateMembers();
|
||||
}
|
||||
|
||||
// user cache stores user keys
|
||||
std::optional<UserKeyCache>
|
||||
userKeys(const std::string &user_id)
|
||||
{
|
||||
return instance_->userKeys(user_id);
|
||||
}
|
||||
void
|
||||
updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery)
|
||||
{
|
||||
instance_->updateUserKeys(sync_token, keyQuery);
|
||||
}
|
||||
|
||||
// device & user verification cache
|
||||
std::optional<VerificationStatus>
|
||||
verificationStatus(const std::string &user_id)
|
||||
{
|
||||
return instance_->verificationStatus(user_id);
|
||||
}
|
||||
|
||||
void
|
||||
markDeviceVerified(const std::string &user_id, const std::string &device)
|
||||
{
|
||||
instance_->markDeviceVerified(user_id, device);
|
||||
}
|
||||
|
||||
void
|
||||
markDeviceUnverified(const std::string &user_id, const std::string &device)
|
||||
{
|
||||
instance_->markDeviceUnverified(user_id, device);
|
||||
}
|
||||
|
||||
std::vector<std::string>
|
||||
joinedRooms()
|
||||
{
|
||||
@ -3595,6 +4032,11 @@ updateOutboundMegolmSession(const std::string &room_id, int message_index)
|
||||
{
|
||||
instance_->updateOutboundMegolmSession(room_id, message_index);
|
||||
}
|
||||
void
|
||||
dropOutboundMegolmSession(const std::string &room_id)
|
||||
{
|
||||
instance_->dropOutboundMegolmSession(room_id);
|
||||
}
|
||||
|
||||
void
|
||||
importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys)
|
||||
|
16
src/Cache.h
16
src/Cache.h
@ -60,6 +60,20 @@ presenceState(const std::string &user_id);
|
||||
std::string
|
||||
statusMessage(const std::string &user_id);
|
||||
|
||||
// user cache stores user keys
|
||||
std::optional<UserKeyCache>
|
||||
userKeys(const std::string &user_id);
|
||||
void
|
||||
updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery);
|
||||
|
||||
// device & user verification cache
|
||||
std::optional<VerificationStatus>
|
||||
verificationStatus(const std::string &user_id);
|
||||
void
|
||||
markDeviceVerified(const std::string &user_id, const std::string &device);
|
||||
void
|
||||
markDeviceUnverified(const std::string &user_id, const std::string &device);
|
||||
|
||||
//! Load saved data for the display names & avatars.
|
||||
void
|
||||
populateMembers();
|
||||
@ -255,6 +269,8 @@ bool
|
||||
outboundMegolmSessionExists(const std::string &room_id) noexcept;
|
||||
void
|
||||
updateOutboundMegolmSession(const std::string &room_id, int message_index);
|
||||
void
|
||||
dropOutboundMegolmSession(const std::string &room_id);
|
||||
|
||||
void
|
||||
importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys);
|
||||
|
@ -65,3 +65,53 @@ struct OlmSessionStorage
|
||||
std::mutex group_outbound_mtx;
|
||||
std::mutex group_inbound_mtx;
|
||||
};
|
||||
|
||||
//! Verification status of a single user
|
||||
struct VerificationStatus
|
||||
{
|
||||
//! True, if the users master key is verified
|
||||
bool user_verified = false;
|
||||
//! List of all devices marked as verified
|
||||
std::vector<std::string> verified_devices;
|
||||
};
|
||||
|
||||
//! In memory cache of verification status
|
||||
struct VerificationStorage
|
||||
{
|
||||
//! mapping of user to verification status
|
||||
std::map<std::string, VerificationStatus> status;
|
||||
std::mutex verification_storage_mtx;
|
||||
};
|
||||
|
||||
// this will store the keys of the user with whom a encrypted room is shared with
|
||||
struct UserKeyCache
|
||||
{
|
||||
//! Device id to device keys
|
||||
std::map<std::string, mtx::crypto::DeviceKeys> device_keys;
|
||||
//! corss signing keys
|
||||
mtx::crypto::CrossSigningKeys master_keys, user_signing_keys, self_signing_keys;
|
||||
//! Sync token when nheko last fetched the keys
|
||||
std::string updated_at;
|
||||
//! Sync token when the keys last changed. updated != last_changed means they are outdated.
|
||||
std::string last_changed;
|
||||
};
|
||||
|
||||
void
|
||||
to_json(nlohmann::json &j, const UserKeyCache &info);
|
||||
void
|
||||
from_json(const nlohmann::json &j, UserKeyCache &info);
|
||||
|
||||
// the reason these are stored in a seperate cache rather than storing it in the user cache is
|
||||
// UserKeyCache stores only keys of users with which encrypted room is shared
|
||||
struct VerificationCache
|
||||
{
|
||||
//! list of verified device_ids with device-verification
|
||||
std::vector<std::string> device_verified;
|
||||
//! list of devices the user blocks
|
||||
std::vector<std::string> device_blocked;
|
||||
};
|
||||
|
||||
void
|
||||
to_json(nlohmann::json &j, const VerificationCache &info);
|
||||
void
|
||||
from_json(const nlohmann::json &j, VerificationCache &info);
|
||||
|
@ -54,6 +54,23 @@ public:
|
||||
mtx::presence::PresenceState presenceState(const std::string &user_id);
|
||||
std::string statusMessage(const std::string &user_id);
|
||||
|
||||
// user cache stores user keys
|
||||
std::optional<UserKeyCache> userKeys(const std::string &user_id);
|
||||
void updateUserKeys(const std::string &sync_token,
|
||||
const mtx::responses::QueryKeys &keyQuery);
|
||||
void markUserKeysOutOfDate(lmdb::txn &txn,
|
||||
lmdb::dbi &db,
|
||||
const std::vector<std::string> &user_ids,
|
||||
const std::string &sync_token);
|
||||
void deleteUserKeys(lmdb::txn &txn,
|
||||
lmdb::dbi &db,
|
||||
const std::vector<std::string> &user_ids);
|
||||
|
||||
// device & user verification cache
|
||||
VerificationStatus verificationStatus(const std::string &user_id);
|
||||
void markDeviceVerified(const std::string &user_id, const std::string &device);
|
||||
void markDeviceUnverified(const std::string &user_id, const std::string &device);
|
||||
|
||||
static void removeDisplayName(const QString &room_id, const QString &user_id);
|
||||
static void removeAvatarUrl(const QString &room_id, const QString &user_id);
|
||||
|
||||
@ -233,6 +250,7 @@ public:
|
||||
OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id);
|
||||
bool outboundMegolmSessionExists(const std::string &room_id) noexcept;
|
||||
void updateOutboundMegolmSession(const std::string &room_id, int message_index);
|
||||
void dropOutboundMegolmSession(const std::string &room_id);
|
||||
|
||||
void importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys);
|
||||
mtx::crypto::ExportedSessionKeys exportSessionKeys();
|
||||
@ -262,6 +280,9 @@ signals:
|
||||
void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
|
||||
void roomReadStatus(const std::map<QString, bool> &status);
|
||||
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);
|
||||
|
||||
private:
|
||||
//! Save an invited room.
|
||||
@ -527,6 +548,16 @@ private:
|
||||
return lmdb::dbi::open(txn, "presence", MDB_CREATE);
|
||||
}
|
||||
|
||||
lmdb::dbi getUserKeysDb(lmdb::txn &txn)
|
||||
{
|
||||
return lmdb::dbi::open(txn, "user_key", MDB_CREATE);
|
||||
}
|
||||
|
||||
lmdb::dbi getVerificationDb(lmdb::txn &txn)
|
||||
{
|
||||
return lmdb::dbi::open(txn, "verified", MDB_CREATE);
|
||||
}
|
||||
|
||||
//! Retrieves or creates the database that stores the open OLM sessions between our device
|
||||
//! and the given curve25519 key which represents another device.
|
||||
//!
|
||||
@ -545,6 +576,8 @@ private:
|
||||
return QString::fromStdString(event.state_key);
|
||||
}
|
||||
|
||||
std::optional<VerificationCache> verificationCache(const std::string &user_id);
|
||||
|
||||
void setNextBatchToken(lmdb::txn &txn, const std::string &token);
|
||||
void setNextBatchToken(lmdb::txn &txn, const QString &token);
|
||||
|
||||
@ -569,6 +602,7 @@ private:
|
||||
static QHash<QString, QString> AvatarUrls;
|
||||
|
||||
OlmSessionStorage session_storage;
|
||||
VerificationStorage verification_storage;
|
||||
};
|
||||
|
||||
namespace cache {
|
||||
|
@ -26,6 +26,7 @@
|
||||
#include "Cache.h"
|
||||
#include "Cache_p.h"
|
||||
#include "ChatPage.h"
|
||||
#include "DeviceVerificationFlow.h"
|
||||
#include "EventAccessors.h"
|
||||
#include "Logging.h"
|
||||
#include "MainWindow.h"
|
||||
@ -159,6 +160,10 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
|
||||
view_manager_,
|
||||
&TimelineViewManager::clearCurrentRoomTimeline);
|
||||
|
||||
connect(text_input_, &TextInputWidget::rotateMegolmSession, this, [this]() {
|
||||
cache::dropOutboundMegolmSession(current_room_.toStdString());
|
||||
});
|
||||
|
||||
connect(
|
||||
new QShortcut(QKeySequence("Ctrl+Down"), this), &QShortcut::activated, this, [this]() {
|
||||
if (isVisible())
|
||||
@ -1456,6 +1461,46 @@ ChatPage::initiateLogout()
|
||||
emit showOverlayProgressBar();
|
||||
}
|
||||
|
||||
void
|
||||
ChatPage::query_keys(const std::string &user_id,
|
||||
std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb)
|
||||
{
|
||||
auto cache_ = cache::userKeys(user_id);
|
||||
|
||||
if (cache_.has_value()) {
|
||||
if (!cache_->updated_at.empty() && cache_->updated_at == cache_->last_changed) {
|
||||
cb(cache_.value(), {});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
mtx::requests::QueryKeys req;
|
||||
req.device_keys[user_id] = {};
|
||||
|
||||
std::string last_changed;
|
||||
if (cache_)
|
||||
last_changed = cache_->last_changed;
|
||||
req.token = last_changed;
|
||||
|
||||
http::client()->query_keys(req,
|
||||
[cb, user_id, last_changed](const mtx::responses::QueryKeys &res,
|
||||
mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
nhlog::net()->warn(
|
||||
"failed to query device keys: {},{}",
|
||||
err->matrix_error.errcode,
|
||||
static_cast<int>(err->status_code));
|
||||
cb({}, err);
|
||||
return;
|
||||
}
|
||||
|
||||
cache::updateUserKeys(last_changed, res);
|
||||
|
||||
auto keys = cache::userKeys(user_id);
|
||||
cb(keys.value_or(UserKeyCache{}), err);
|
||||
});
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void
|
||||
ChatPage::connectCallMessage()
|
||||
|
@ -19,6 +19,7 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <optional>
|
||||
#include <stack>
|
||||
#include <variant>
|
||||
|
||||
#include <mtx/common.hpp>
|
||||
@ -34,6 +35,7 @@
|
||||
#include <QTimer>
|
||||
#include <QWidget>
|
||||
|
||||
#include "CacheCryptoStructs.h"
|
||||
#include "CacheStructs.h"
|
||||
#include "CallManager.h"
|
||||
#include "CommunitiesList.h"
|
||||
@ -50,6 +52,8 @@ class TextInputWidget;
|
||||
class TimelineViewManager;
|
||||
class UserInfoWidget;
|
||||
class UserSettings;
|
||||
class NotificationsManager;
|
||||
class TimelineModel;
|
||||
|
||||
constexpr int CONSENSUS_TIMEOUT = 1000;
|
||||
constexpr int SHOW_CONTENT_TIMEOUT = 3000;
|
||||
@ -85,6 +89,8 @@ public:
|
||||
//! Show the room/group list (if it was visible).
|
||||
void showSideBars();
|
||||
void initiateLogout();
|
||||
void query_keys(const std::string &req,
|
||||
std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb);
|
||||
void focusMessageInput();
|
||||
|
||||
QString status() const;
|
||||
@ -161,6 +167,24 @@ signals:
|
||||
void themeChanged();
|
||||
void decryptSidebarChanged();
|
||||
|
||||
//! Signals for device verificaiton
|
||||
void receivedDeviceVerificationAccept(
|
||||
const mtx::events::msg::KeyVerificationAccept &message);
|
||||
void receivedDeviceVerificationRequest(
|
||||
const mtx::events::msg::KeyVerificationRequest &message,
|
||||
std::string sender);
|
||||
void receivedRoomDeviceVerificationRequest(
|
||||
const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &message,
|
||||
TimelineModel *model);
|
||||
void receivedDeviceVerificationCancel(
|
||||
const mtx::events::msg::KeyVerificationCancel &message);
|
||||
void receivedDeviceVerificationKey(const mtx::events::msg::KeyVerificationKey &message);
|
||||
void receivedDeviceVerificationMac(const mtx::events::msg::KeyVerificationMac &message);
|
||||
void receivedDeviceVerificationStart(const mtx::events::msg::KeyVerificationStart &message,
|
||||
std::string sender);
|
||||
void receivedDeviceVerificationReady(const mtx::events::msg::KeyVerificationReady &message);
|
||||
void receivedDeviceVerificationDone(const mtx::events::msg::KeyVerificationDone &message);
|
||||
|
||||
private slots:
|
||||
void showUnreadMessageNotification(int count);
|
||||
void logout();
|
||||
|
794
src/DeviceVerificationFlow.cpp
Normal file
794
src/DeviceVerificationFlow.cpp
Normal file
@ -0,0 +1,794 @@
|
||||
#include "DeviceVerificationFlow.h"
|
||||
|
||||
#include "Cache.h"
|
||||
#include "ChatPage.h"
|
||||
#include "Logging.h"
|
||||
#include "timeline/TimelineModel.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QTimer>
|
||||
#include <iostream>
|
||||
|
||||
static constexpr int TIMEOUT = 2 * 60 * 1000; // 2 minutes
|
||||
|
||||
namespace msgs = mtx::events::msg;
|
||||
|
||||
static mtx::events::msg::KeyVerificationMac
|
||||
key_verification_mac(mtx::crypto::SAS *sas,
|
||||
mtx::identifiers::User sender,
|
||||
const std::string &senderDevice,
|
||||
mtx::identifiers::User receiver,
|
||||
const std::string &receiverDevice,
|
||||
const std::string &transactionId,
|
||||
std::map<std::string, std::string> keys);
|
||||
|
||||
DeviceVerificationFlow::DeviceVerificationFlow(QObject *,
|
||||
DeviceVerificationFlow::Type flow_type,
|
||||
TimelineModel *model,
|
||||
QString userID,
|
||||
QString deviceId_)
|
||||
: sender(false)
|
||||
, type(flow_type)
|
||||
, deviceId(deviceId_)
|
||||
, model_(model)
|
||||
{
|
||||
timeout = new QTimer(this);
|
||||
timeout->setSingleShot(true);
|
||||
this->sas = olm::client()->sas_init();
|
||||
this->isMacVerified = false;
|
||||
|
||||
auto user_id = userID.toStdString();
|
||||
this->toClient = mtx::identifiers::parse<mtx::identifiers::User>(user_id);
|
||||
ChatPage::instance()->query_keys(
|
||||
user_id, [user_id, this](const UserKeyCache &res, mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
nhlog::net()->warn("failed to query device keys: {},{}",
|
||||
err->matrix_error.errcode,
|
||||
static_cast<int>(err->status_code));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this->deviceId.isEmpty() &&
|
||||
(res.device_keys.find(deviceId.toStdString()) == res.device_keys.end())) {
|
||||
nhlog::net()->warn("no devices retrieved {}", user_id);
|
||||
return;
|
||||
}
|
||||
|
||||
this->their_keys = res;
|
||||
});
|
||||
|
||||
ChatPage::instance()->query_keys(
|
||||
http::client()->user_id().to_string(),
|
||||
[this](const UserKeyCache &res, mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
nhlog::net()->warn("failed to query device keys: {},{}",
|
||||
err->matrix_error.errcode,
|
||||
static_cast<int>(err->status_code));
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.master_keys.keys.empty())
|
||||
return;
|
||||
|
||||
if (auto status =
|
||||
cache::verificationStatus(http::client()->user_id().to_string());
|
||||
status && status->user_verified)
|
||||
this->our_trusted_master_key = res.master_keys.keys.begin()->second;
|
||||
});
|
||||
|
||||
if (model) {
|
||||
connect(this->model_,
|
||||
&TimelineModel::updateFlowEventId,
|
||||
this,
|
||||
[this](std::string event_id_) {
|
||||
this->relation.rel_type = mtx::common::RelationType::Reference;
|
||||
this->relation.event_id = event_id_;
|
||||
this->transaction_id = event_id_;
|
||||
});
|
||||
}
|
||||
|
||||
connect(timeout, &QTimer::timeout, this, [this]() {
|
||||
if (state_ != Success && state_ != Failed)
|
||||
this->cancelVerification(DeviceVerificationFlow::Error::Timeout);
|
||||
});
|
||||
|
||||
connect(ChatPage::instance(),
|
||||
&ChatPage::receivedDeviceVerificationStart,
|
||||
this,
|
||||
&DeviceVerificationFlow::handleStartMessage);
|
||||
connect(ChatPage::instance(),
|
||||
&ChatPage::receivedDeviceVerificationAccept,
|
||||
this,
|
||||
[this](const mtx::events::msg::KeyVerificationAccept &msg) {
|
||||
if (msg.transaction_id.has_value()) {
|
||||
if (msg.transaction_id.value() != this->transaction_id)
|
||||
return;
|
||||
} else if (msg.relates_to.has_value()) {
|
||||
if (msg.relates_to.value().event_id != this->relation.event_id)
|
||||
return;
|
||||
}
|
||||
if ((msg.key_agreement_protocol == "curve25519-hkdf-sha256") &&
|
||||
(msg.hash == "sha256") &&
|
||||
(msg.message_authentication_code == "hkdf-hmac-sha256")) {
|
||||
this->commitment = msg.commitment;
|
||||
if (std::find(msg.short_authentication_string.begin(),
|
||||
msg.short_authentication_string.end(),
|
||||
mtx::events::msg::SASMethods::Emoji) !=
|
||||
msg.short_authentication_string.end()) {
|
||||
this->method = mtx::events::msg::SASMethods::Emoji;
|
||||
} else {
|
||||
this->method = mtx::events::msg::SASMethods::Decimal;
|
||||
}
|
||||
this->mac_method = msg.message_authentication_code;
|
||||
this->sendVerificationKey();
|
||||
} else {
|
||||
this->cancelVerification(
|
||||
DeviceVerificationFlow::Error::UnknownMethod);
|
||||
}
|
||||
});
|
||||
|
||||
connect(ChatPage::instance(),
|
||||
&ChatPage::receivedDeviceVerificationCancel,
|
||||
this,
|
||||
[this](const mtx::events::msg::KeyVerificationCancel &msg) {
|
||||
if (msg.transaction_id.has_value()) {
|
||||
if (msg.transaction_id.value() != this->transaction_id)
|
||||
return;
|
||||
} else if (msg.relates_to.has_value()) {
|
||||
if (msg.relates_to.value().event_id != this->relation.event_id)
|
||||
return;
|
||||
}
|
||||
error_ = User;
|
||||
emit errorChanged();
|
||||
setState(Failed);
|
||||
});
|
||||
|
||||
connect(ChatPage::instance(),
|
||||
&ChatPage::receivedDeviceVerificationKey,
|
||||
this,
|
||||
[this](const mtx::events::msg::KeyVerificationKey &msg) {
|
||||
if (msg.transaction_id.has_value()) {
|
||||
if (msg.transaction_id.value() != this->transaction_id)
|
||||
return;
|
||||
} else if (msg.relates_to.has_value()) {
|
||||
if (msg.relates_to.value().event_id != this->relation.event_id)
|
||||
return;
|
||||
}
|
||||
|
||||
if (sender) {
|
||||
if (state_ != WaitingForOtherToAccept) {
|
||||
this->cancelVerification(OutOfOrder);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (state_ != WaitingForKeys) {
|
||||
this->cancelVerification(OutOfOrder);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this->sas->set_their_key(msg.key);
|
||||
std::string info;
|
||||
if (this->sender == true) {
|
||||
info = "MATRIX_KEY_VERIFICATION_SAS|" +
|
||||
http::client()->user_id().to_string() + "|" +
|
||||
http::client()->device_id() + "|" + this->sas->public_key() +
|
||||
"|" + this->toClient.to_string() + "|" +
|
||||
this->deviceId.toStdString() + "|" + msg.key + "|" +
|
||||
this->transaction_id;
|
||||
} else {
|
||||
info = "MATRIX_KEY_VERIFICATION_SAS|" + this->toClient.to_string() +
|
||||
"|" + this->deviceId.toStdString() + "|" + msg.key + "|" +
|
||||
http::client()->user_id().to_string() + "|" +
|
||||
http::client()->device_id() + "|" + this->sas->public_key() +
|
||||
"|" + this->transaction_id;
|
||||
}
|
||||
|
||||
nhlog::ui()->info("Info is: '{}'", info);
|
||||
|
||||
if (this->sender == false) {
|
||||
this->sendVerificationKey();
|
||||
} else {
|
||||
if (this->commitment !=
|
||||
mtx::crypto::bin2base64_unpadded(
|
||||
mtx::crypto::sha256(msg.key + this->canonical_json.dump()))) {
|
||||
this->cancelVerification(
|
||||
DeviceVerificationFlow::Error::MismatchedCommitment);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->method == mtx::events::msg::SASMethods::Emoji) {
|
||||
this->sasList = this->sas->generate_bytes_emoji(info);
|
||||
setState(CompareEmoji);
|
||||
} else if (this->method == mtx::events::msg::SASMethods::Decimal) {
|
||||
this->sasList = this->sas->generate_bytes_decimal(info);
|
||||
setState(CompareNumber);
|
||||
}
|
||||
});
|
||||
|
||||
connect(
|
||||
ChatPage::instance(),
|
||||
&ChatPage::receivedDeviceVerificationMac,
|
||||
this,
|
||||
[this](const mtx::events::msg::KeyVerificationMac &msg) {
|
||||
if (msg.transaction_id.has_value()) {
|
||||
if (msg.transaction_id.value() != this->transaction_id)
|
||||
return;
|
||||
} else if (msg.relates_to.has_value()) {
|
||||
if (msg.relates_to.value().event_id != this->relation.event_id)
|
||||
return;
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> key_list;
|
||||
std::string key_string;
|
||||
for (const auto &mac : msg.mac) {
|
||||
for (const auto &[deviceid, key] : their_keys.device_keys) {
|
||||
(void)deviceid;
|
||||
if (key.keys.count(mac.first))
|
||||
key_list[mac.first] = key.keys.at(mac.first);
|
||||
}
|
||||
|
||||
if (their_keys.master_keys.keys.count(mac.first))
|
||||
key_list[mac.first] = their_keys.master_keys.keys[mac.first];
|
||||
if (their_keys.user_signing_keys.keys.count(mac.first))
|
||||
key_list[mac.first] =
|
||||
their_keys.user_signing_keys.keys[mac.first];
|
||||
if (their_keys.self_signing_keys.keys.count(mac.first))
|
||||
key_list[mac.first] =
|
||||
their_keys.self_signing_keys.keys[mac.first];
|
||||
}
|
||||
auto macs = key_verification_mac(sas.get(),
|
||||
toClient,
|
||||
this->deviceId.toStdString(),
|
||||
http::client()->user_id(),
|
||||
http::client()->device_id(),
|
||||
this->transaction_id,
|
||||
key_list);
|
||||
|
||||
for (const auto &[key, mac] : macs.mac) {
|
||||
if (mac != msg.mac.at(key)) {
|
||||
this->cancelVerification(
|
||||
DeviceVerificationFlow::Error::KeyMismatch);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.keys == macs.keys) {
|
||||
mtx::requests::KeySignaturesUpload req;
|
||||
if (utils::localUser().toStdString() == this->toClient.to_string()) {
|
||||
// self verification, sign master key with device key, if we
|
||||
// verified it
|
||||
for (const auto &mac : msg.mac) {
|
||||
if (their_keys.master_keys.keys.count(mac.first)) {
|
||||
json j = their_keys.master_keys;
|
||||
j.erase("signatures");
|
||||
j.erase("unsigned");
|
||||
mtx::crypto::CrossSigningKeys master_key = j;
|
||||
master_key
|
||||
.signatures[utils::localUser().toStdString()]
|
||||
["ed25519:" +
|
||||
http::client()->device_id()] =
|
||||
olm::client()->sign_message(j.dump());
|
||||
req.signatures[utils::localUser().toStdString()]
|
||||
[master_key.keys.at(mac.first)] =
|
||||
master_key;
|
||||
}
|
||||
}
|
||||
// TODO(Nico): Sign their device key with self signing key
|
||||
} else {
|
||||
// TODO(Nico): Sign their master key with user signing key
|
||||
}
|
||||
|
||||
if (!req.signatures.empty()) {
|
||||
http::client()->keys_signatures_upload(
|
||||
req,
|
||||
[](const mtx::responses::KeySignaturesUpload &res,
|
||||
mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
nhlog::net()->error(
|
||||
"failed to upload signatures: {},{}",
|
||||
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,
|
||||
e.errcode,
|
||||
e.error);
|
||||
});
|
||||
}
|
||||
|
||||
this->isMacVerified = true;
|
||||
this->acceptDevice();
|
||||
} else {
|
||||
this->cancelVerification(DeviceVerificationFlow::Error::KeyMismatch);
|
||||
}
|
||||
});
|
||||
|
||||
connect(ChatPage::instance(),
|
||||
&ChatPage::receivedDeviceVerificationReady,
|
||||
this,
|
||||
[this](const mtx::events::msg::KeyVerificationReady &msg) {
|
||||
if (!sender) {
|
||||
if (msg.from_device != http::client()->device_id()) {
|
||||
error_ = User;
|
||||
emit errorChanged();
|
||||
setState(Failed);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.transaction_id.has_value()) {
|
||||
if (msg.transaction_id.value() != this->transaction_id)
|
||||
return;
|
||||
} else if ((msg.relates_to.has_value() && sender)) {
|
||||
if (msg.relates_to.value().event_id != this->relation.event_id)
|
||||
return;
|
||||
else {
|
||||
this->deviceId = QString::fromStdString(msg.from_device);
|
||||
}
|
||||
}
|
||||
this->startVerificationRequest();
|
||||
});
|
||||
|
||||
connect(ChatPage::instance(),
|
||||
&ChatPage::receivedDeviceVerificationDone,
|
||||
this,
|
||||
[this](const mtx::events::msg::KeyVerificationDone &msg) {
|
||||
if (msg.transaction_id.has_value()) {
|
||||
if (msg.transaction_id.value() != this->transaction_id)
|
||||
return;
|
||||
} else if (msg.relates_to.has_value()) {
|
||||
if (msg.relates_to.value().event_id != this->relation.event_id)
|
||||
return;
|
||||
}
|
||||
nhlog::ui()->info("Flow done on other side");
|
||||
});
|
||||
|
||||
timeout->start(TIMEOUT);
|
||||
}
|
||||
|
||||
QString
|
||||
DeviceVerificationFlow::state()
|
||||
{
|
||||
switch (state_) {
|
||||
case PromptStartVerification:
|
||||
return "PromptStartVerification";
|
||||
case CompareEmoji:
|
||||
return "CompareEmoji";
|
||||
case CompareNumber:
|
||||
return "CompareNumber";
|
||||
case WaitingForKeys:
|
||||
return "WaitingForKeys";
|
||||
case WaitingForOtherToAccept:
|
||||
return "WaitingForOtherToAccept";
|
||||
case WaitingForMac:
|
||||
return "WaitingForMac";
|
||||
case Success:
|
||||
return "Success";
|
||||
case Failed:
|
||||
return "Failed";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
DeviceVerificationFlow::next()
|
||||
{
|
||||
if (sender) {
|
||||
switch (state_) {
|
||||
case PromptStartVerification:
|
||||
sendVerificationRequest();
|
||||
break;
|
||||
case CompareEmoji:
|
||||
case CompareNumber:
|
||||
sendVerificationMac();
|
||||
break;
|
||||
case WaitingForKeys:
|
||||
case WaitingForOtherToAccept:
|
||||
case WaitingForMac:
|
||||
case Success:
|
||||
case Failed:
|
||||
nhlog::db()->error("verification: Invalid state transition!");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
switch (state_) {
|
||||
case PromptStartVerification:
|
||||
if (canonical_json.is_null())
|
||||
sendVerificationReady();
|
||||
else // legacy path without request and ready
|
||||
acceptVerificationRequest();
|
||||
break;
|
||||
case CompareEmoji:
|
||||
[[fallthrough]];
|
||||
case CompareNumber:
|
||||
sendVerificationMac();
|
||||
break;
|
||||
case WaitingForKeys:
|
||||
case WaitingForOtherToAccept:
|
||||
case WaitingForMac:
|
||||
case Success:
|
||||
case Failed:
|
||||
nhlog::db()->error("verification: Invalid state transition!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString
|
||||
DeviceVerificationFlow::getUserId()
|
||||
{
|
||||
return QString::fromStdString(this->toClient.to_string());
|
||||
}
|
||||
|
||||
QString
|
||||
DeviceVerificationFlow::getDeviceId()
|
||||
{
|
||||
return this->deviceId;
|
||||
}
|
||||
|
||||
bool
|
||||
DeviceVerificationFlow::getSender()
|
||||
{
|
||||
return this->sender;
|
||||
}
|
||||
|
||||
std::vector<int>
|
||||
DeviceVerificationFlow::getSasList()
|
||||
{
|
||||
return this->sasList;
|
||||
}
|
||||
|
||||
void
|
||||
DeviceVerificationFlow::setEventId(std::string event_id_)
|
||||
{
|
||||
this->relation.rel_type = mtx::common::RelationType::Reference;
|
||||
this->relation.event_id = event_id_;
|
||||
this->transaction_id = event_id_;
|
||||
}
|
||||
|
||||
void
|
||||
DeviceVerificationFlow::handleStartMessage(const mtx::events::msg::KeyVerificationStart &msg,
|
||||
std::string)
|
||||
{
|
||||
if (msg.transaction_id.has_value()) {
|
||||
if (msg.transaction_id.value() != this->transaction_id)
|
||||
return;
|
||||
} else if (msg.relates_to.has_value()) {
|
||||
if (msg.relates_to.value().event_id != this->relation.event_id)
|
||||
return;
|
||||
}
|
||||
if ((std::find(msg.key_agreement_protocols.begin(),
|
||||
msg.key_agreement_protocols.end(),
|
||||
"curve25519-hkdf-sha256") != msg.key_agreement_protocols.end()) &&
|
||||
(std::find(msg.hashes.begin(), msg.hashes.end(), "sha256") != msg.hashes.end()) &&
|
||||
(std::find(msg.message_authentication_codes.begin(),
|
||||
msg.message_authentication_codes.end(),
|
||||
"hkdf-hmac-sha256") != msg.message_authentication_codes.end())) {
|
||||
if (std::find(msg.short_authentication_string.begin(),
|
||||
msg.short_authentication_string.end(),
|
||||
mtx::events::msg::SASMethods::Emoji) !=
|
||||
msg.short_authentication_string.end()) {
|
||||
this->method = mtx::events::msg::SASMethods::Emoji;
|
||||
} else if (std::find(msg.short_authentication_string.begin(),
|
||||
msg.short_authentication_string.end(),
|
||||
mtx::events::msg::SASMethods::Decimal) !=
|
||||
msg.short_authentication_string.end()) {
|
||||
this->method = mtx::events::msg::SASMethods::Decimal;
|
||||
} else {
|
||||
this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod);
|
||||
return;
|
||||
}
|
||||
if (!sender)
|
||||
this->canonical_json = nlohmann::json(msg);
|
||||
else {
|
||||
if (utils::localUser().toStdString() < this->toClient.to_string()) {
|
||||
this->canonical_json = nlohmann::json(msg);
|
||||
}
|
||||
}
|
||||
|
||||
if (state_ != PromptStartVerification)
|
||||
this->acceptVerificationRequest();
|
||||
} else {
|
||||
this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod);
|
||||
}
|
||||
}
|
||||
|
||||
//! accepts a verification
|
||||
void
|
||||
DeviceVerificationFlow::acceptVerificationRequest()
|
||||
{
|
||||
mtx::events::msg::KeyVerificationAccept req;
|
||||
|
||||
req.method = mtx::events::msg::VerificationMethods::SASv1;
|
||||
req.key_agreement_protocol = "curve25519-hkdf-sha256";
|
||||
req.hash = "sha256";
|
||||
req.message_authentication_code = "hkdf-hmac-sha256";
|
||||
if (this->method == mtx::events::msg::SASMethods::Emoji)
|
||||
req.short_authentication_string = {mtx::events::msg::SASMethods::Emoji};
|
||||
else if (this->method == mtx::events::msg::SASMethods::Decimal)
|
||||
req.short_authentication_string = {mtx::events::msg::SASMethods::Decimal};
|
||||
req.commitment = mtx::crypto::bin2base64_unpadded(
|
||||
mtx::crypto::sha256(this->sas->public_key() + this->canonical_json.dump()));
|
||||
|
||||
send(req);
|
||||
setState(WaitingForKeys);
|
||||
}
|
||||
//! responds verification request
|
||||
void
|
||||
DeviceVerificationFlow::sendVerificationReady()
|
||||
{
|
||||
mtx::events::msg::KeyVerificationReady req;
|
||||
|
||||
req.from_device = http::client()->device_id();
|
||||
req.methods = {mtx::events::msg::VerificationMethods::SASv1};
|
||||
|
||||
send(req);
|
||||
setState(WaitingForKeys);
|
||||
}
|
||||
//! accepts a verification
|
||||
void
|
||||
DeviceVerificationFlow::sendVerificationDone()
|
||||
{
|
||||
mtx::events::msg::KeyVerificationDone req;
|
||||
|
||||
send(req);
|
||||
}
|
||||
//! starts the verification flow
|
||||
void
|
||||
DeviceVerificationFlow::startVerificationRequest()
|
||||
{
|
||||
mtx::events::msg::KeyVerificationStart req;
|
||||
|
||||
req.from_device = http::client()->device_id();
|
||||
req.method = mtx::events::msg::VerificationMethods::SASv1;
|
||||
req.key_agreement_protocols = {"curve25519-hkdf-sha256"};
|
||||
req.hashes = {"sha256"};
|
||||
req.message_authentication_codes = {"hkdf-hmac-sha256"};
|
||||
req.short_authentication_string = {mtx::events::msg::SASMethods::Decimal,
|
||||
mtx::events::msg::SASMethods::Emoji};
|
||||
|
||||
if (this->type == DeviceVerificationFlow::Type::ToDevice) {
|
||||
mtx::requests::ToDeviceMessages<mtx::events::msg::KeyVerificationStart> body;
|
||||
req.transaction_id = this->transaction_id;
|
||||
this->canonical_json = nlohmann::json(req);
|
||||
} else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
|
||||
req.relates_to = this->relation;
|
||||
this->canonical_json = nlohmann::json(req);
|
||||
}
|
||||
send(req);
|
||||
setState(WaitingForOtherToAccept);
|
||||
}
|
||||
//! sends a verification request
|
||||
void
|
||||
DeviceVerificationFlow::sendVerificationRequest()
|
||||
{
|
||||
mtx::events::msg::KeyVerificationRequest req;
|
||||
|
||||
req.from_device = http::client()->device_id();
|
||||
req.methods = {mtx::events::msg::VerificationMethods::SASv1};
|
||||
|
||||
if (this->type == DeviceVerificationFlow::Type::ToDevice) {
|
||||
QDateTime currentTime = QDateTime::currentDateTimeUtc();
|
||||
|
||||
req.timestamp = (uint64_t)currentTime.toMSecsSinceEpoch();
|
||||
|
||||
} else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
|
||||
req.to = this->toClient.to_string();
|
||||
req.msgtype = "m.key.verification.request";
|
||||
req.body = "User is requesting to verify keys with you. However, your client does "
|
||||
"not support this method, so you will need to use the legacy method of "
|
||||
"key verification.";
|
||||
}
|
||||
|
||||
send(req);
|
||||
setState(WaitingForOtherToAccept);
|
||||
}
|
||||
//! cancels a verification flow
|
||||
void
|
||||
DeviceVerificationFlow::cancelVerification(DeviceVerificationFlow::Error error_code)
|
||||
{
|
||||
mtx::events::msg::KeyVerificationCancel req;
|
||||
|
||||
if (error_code == DeviceVerificationFlow::Error::UnknownMethod) {
|
||||
req.code = "m.unknown_method";
|
||||
req.reason = "unknown method received";
|
||||
} else if (error_code == DeviceVerificationFlow::Error::MismatchedCommitment) {
|
||||
req.code = "m.mismatched_commitment";
|
||||
req.reason = "commitment didn't match";
|
||||
} else if (error_code == DeviceVerificationFlow::Error::MismatchedSAS) {
|
||||
req.code = "m.mismatched_sas";
|
||||
req.reason = "sas didn't match";
|
||||
} else if (error_code == DeviceVerificationFlow::Error::KeyMismatch) {
|
||||
req.code = "m.key_match";
|
||||
req.reason = "keys did not match";
|
||||
} else if (error_code == DeviceVerificationFlow::Error::Timeout) {
|
||||
req.code = "m.timeout";
|
||||
req.reason = "timed out";
|
||||
} else if (error_code == DeviceVerificationFlow::Error::User) {
|
||||
req.code = "m.user";
|
||||
req.reason = "user cancelled the verification";
|
||||
} else if (error_code == DeviceVerificationFlow::Error::OutOfOrder) {
|
||||
req.code = "m.unexpected_message";
|
||||
req.reason = "received messages out of order";
|
||||
}
|
||||
|
||||
this->error_ = error_code;
|
||||
emit errorChanged();
|
||||
this->setState(Failed);
|
||||
|
||||
send(req);
|
||||
}
|
||||
//! sends the verification key
|
||||
void
|
||||
DeviceVerificationFlow::sendVerificationKey()
|
||||
{
|
||||
mtx::events::msg::KeyVerificationKey req;
|
||||
|
||||
req.key = this->sas->public_key();
|
||||
|
||||
send(req);
|
||||
}
|
||||
|
||||
mtx::events::msg::KeyVerificationMac
|
||||
key_verification_mac(mtx::crypto::SAS *sas,
|
||||
mtx::identifiers::User sender,
|
||||
const std::string &senderDevice,
|
||||
mtx::identifiers::User receiver,
|
||||
const std::string &receiverDevice,
|
||||
const std::string &transactionId,
|
||||
std::map<std::string, std::string> keys)
|
||||
{
|
||||
mtx::events::msg::KeyVerificationMac req;
|
||||
|
||||
std::string info = "MATRIX_KEY_VERIFICATION_MAC" + sender.to_string() + senderDevice +
|
||||
receiver.to_string() + receiverDevice + transactionId;
|
||||
|
||||
std::string key_list;
|
||||
bool first = true;
|
||||
for (const auto &[key_id, key] : keys) {
|
||||
req.mac[key_id] = sas->calculate_mac(key, info + key_id);
|
||||
|
||||
if (!first)
|
||||
key_list += ",";
|
||||
key_list += key_id;
|
||||
first = false;
|
||||
}
|
||||
|
||||
req.keys = sas->calculate_mac(key_list, info + "KEY_IDS");
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
//! sends the mac of the keys
|
||||
void
|
||||
DeviceVerificationFlow::sendVerificationMac()
|
||||
{
|
||||
std::map<std::string, std::string> key_list;
|
||||
key_list["ed25519:" + http::client()->device_id()] = olm::client()->identity_keys().ed25519;
|
||||
|
||||
// send our master key, if we trust it
|
||||
if (!this->our_trusted_master_key.empty())
|
||||
key_list["ed25519:" + our_trusted_master_key] = our_trusted_master_key;
|
||||
|
||||
mtx::events::msg::KeyVerificationMac req =
|
||||
key_verification_mac(sas.get(),
|
||||
http::client()->user_id(),
|
||||
http::client()->device_id(),
|
||||
this->toClient,
|
||||
this->deviceId.toStdString(),
|
||||
this->transaction_id,
|
||||
key_list);
|
||||
|
||||
send(req);
|
||||
|
||||
setState(WaitingForMac);
|
||||
acceptDevice();
|
||||
}
|
||||
//! Completes the verification flow
|
||||
void
|
||||
DeviceVerificationFlow::acceptDevice()
|
||||
{
|
||||
if (!isMacVerified) {
|
||||
setState(WaitingForMac);
|
||||
} else if (state_ == WaitingForMac) {
|
||||
cache::markDeviceVerified(this->toClient.to_string(), this->deviceId.toStdString());
|
||||
this->sendVerificationDone();
|
||||
setState(Success);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
DeviceVerificationFlow::unverify()
|
||||
{
|
||||
cache::markDeviceUnverified(this->toClient.to_string(), this->deviceId.toStdString());
|
||||
|
||||
emit refreshProfile();
|
||||
}
|
||||
|
||||
QSharedPointer<DeviceVerificationFlow>
|
||||
DeviceVerificationFlow::NewInRoomVerification(QObject *parent_,
|
||||
TimelineModel *timelineModel_,
|
||||
const mtx::events::msg::KeyVerificationRequest &msg,
|
||||
QString other_user_,
|
||||
QString event_id_)
|
||||
{
|
||||
QSharedPointer<DeviceVerificationFlow> flow(
|
||||
new DeviceVerificationFlow(parent_,
|
||||
Type::RoomMsg,
|
||||
timelineModel_,
|
||||
other_user_,
|
||||
QString::fromStdString(msg.from_device)));
|
||||
|
||||
flow->setEventId(event_id_.toStdString());
|
||||
|
||||
if (std::find(msg.methods.begin(),
|
||||
msg.methods.end(),
|
||||
mtx::events::msg::VerificationMethods::SASv1) == msg.methods.end()) {
|
||||
flow->cancelVerification(UnknownMethod);
|
||||
}
|
||||
|
||||
return flow;
|
||||
}
|
||||
QSharedPointer<DeviceVerificationFlow>
|
||||
DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_,
|
||||
const mtx::events::msg::KeyVerificationRequest &msg,
|
||||
QString other_user_,
|
||||
QString txn_id_)
|
||||
{
|
||||
QSharedPointer<DeviceVerificationFlow> flow(new DeviceVerificationFlow(
|
||||
parent_, Type::ToDevice, nullptr, other_user_, QString::fromStdString(msg.from_device)));
|
||||
flow->transaction_id = txn_id_.toStdString();
|
||||
|
||||
if (std::find(msg.methods.begin(),
|
||||
msg.methods.end(),
|
||||
mtx::events::msg::VerificationMethods::SASv1) == msg.methods.end()) {
|
||||
flow->cancelVerification(UnknownMethod);
|
||||
}
|
||||
|
||||
return flow;
|
||||
}
|
||||
QSharedPointer<DeviceVerificationFlow>
|
||||
DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_,
|
||||
const mtx::events::msg::KeyVerificationStart &msg,
|
||||
QString other_user_,
|
||||
QString txn_id_)
|
||||
{
|
||||
QSharedPointer<DeviceVerificationFlow> flow(new DeviceVerificationFlow(
|
||||
parent_, Type::ToDevice, nullptr, other_user_, QString::fromStdString(msg.from_device)));
|
||||
flow->transaction_id = txn_id_.toStdString();
|
||||
|
||||
flow->handleStartMessage(msg, "");
|
||||
|
||||
return flow;
|
||||
}
|
||||
QSharedPointer<DeviceVerificationFlow>
|
||||
DeviceVerificationFlow::InitiateUserVerification(QObject *parent_,
|
||||
TimelineModel *timelineModel_,
|
||||
QString userid)
|
||||
{
|
||||
QSharedPointer<DeviceVerificationFlow> flow(
|
||||
new DeviceVerificationFlow(parent_, Type::RoomMsg, timelineModel_, userid, ""));
|
||||
flow->sender = true;
|
||||
return flow;
|
||||
}
|
||||
QSharedPointer<DeviceVerificationFlow>
|
||||
DeviceVerificationFlow::InitiateDeviceVerification(QObject *parent_, QString userid, QString device)
|
||||
{
|
||||
QSharedPointer<DeviceVerificationFlow> flow(
|
||||
new DeviceVerificationFlow(parent_, Type::ToDevice, nullptr, userid, device));
|
||||
|
||||
flow->sender = true;
|
||||
flow->transaction_id = http::client()->generate_txn_id();
|
||||
|
||||
return flow;
|
||||
}
|
235
src/DeviceVerificationFlow.h
Normal file
235
src/DeviceVerificationFlow.h
Normal file
@ -0,0 +1,235 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include <mtx/responses/crypto.hpp>
|
||||
|
||||
#include "CacheCryptoStructs.h"
|
||||
#include "Logging.h"
|
||||
#include "MatrixClient.h"
|
||||
#include "Olm.h"
|
||||
#include "timeline/TimelineModel.h"
|
||||
|
||||
class QTimer;
|
||||
|
||||
using sas_ptr = std::unique_ptr<mtx::crypto::SAS>;
|
||||
|
||||
// clang-format off
|
||||
/*
|
||||
* Stolen from fluffy chat :D
|
||||
*
|
||||
* State | +-------------+ +-----------+ |
|
||||
* | | AliceDevice | | BobDevice | |
|
||||
* | | (sender) | | | |
|
||||
* | +-------------+ +-----------+ |
|
||||
* promptStartVerify | | | |
|
||||
* | o | (m.key.verification.request) | |
|
||||
* | p |-------------------------------->| (ASK FOR VERIFICATION REQUEST) |
|
||||
* waitForOtherAccept | t | | | promptStartVerify
|
||||
* && | i | (m.key.verification.ready) | |
|
||||
* no commitment | o |<--------------------------------| |
|
||||
* && | n | | |
|
||||
* no canonical_json | a | (m.key.verification.start) | | waitingForKeys
|
||||
* | l |<--------------------------------| Not sending to prevent the glare resolve| && no commitment
|
||||
* | | | | && no canonical_json
|
||||
* | | m.key.verification.start | |
|
||||
* waitForOtherAccept | |-------------------------------->| (IF NOT ALREADY ASKED, |
|
||||
* && | | | ASK FOR VERIFICATION REQUEST) | promptStartVerify, if not accepted
|
||||
* canonical_json | | m.key.verification.accept | |
|
||||
* | |<--------------------------------| |
|
||||
* waitForOtherAccept | | | | waitingForKeys
|
||||
* && | | m.key.verification.key | | && canonical_json
|
||||
* commitment | |-------------------------------->| | && commitment
|
||||
* | | | |
|
||||
* | | m.key.verification.key | |
|
||||
* | |<--------------------------------| |
|
||||
* compareEmoji/Number| | | | compareEmoji/Number
|
||||
* | | COMPARE EMOJI / NUMBERS | |
|
||||
* | | | |
|
||||
* waitingForMac | | m.key.verification.mac | | waitingForMac
|
||||
* | success |<------------------------------->| success |
|
||||
* | | | |
|
||||
* success/fail | | m.key.verification.done | | success/fail
|
||||
* | |<------------------------------->| |
|
||||
*/
|
||||
// clang-format on
|
||||
class DeviceVerificationFlow : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QString state READ state NOTIFY stateChanged)
|
||||
Q_PROPERTY(Error error READ error NOTIFY errorChanged)
|
||||
Q_PROPERTY(QString userId READ getUserId CONSTANT)
|
||||
Q_PROPERTY(QString deviceId READ getDeviceId CONSTANT)
|
||||
Q_PROPERTY(bool sender READ getSender CONSTANT)
|
||||
Q_PROPERTY(std::vector<int> sasList READ getSasList CONSTANT)
|
||||
|
||||
public:
|
||||
enum State
|
||||
{
|
||||
PromptStartVerification,
|
||||
WaitingForOtherToAccept,
|
||||
WaitingForKeys,
|
||||
CompareEmoji,
|
||||
CompareNumber,
|
||||
WaitingForMac,
|
||||
Success,
|
||||
Failed,
|
||||
};
|
||||
Q_ENUM(State)
|
||||
|
||||
enum Type
|
||||
{
|
||||
ToDevice,
|
||||
RoomMsg
|
||||
};
|
||||
|
||||
enum Error
|
||||
{
|
||||
UnknownMethod,
|
||||
MismatchedCommitment,
|
||||
MismatchedSAS,
|
||||
KeyMismatch,
|
||||
Timeout,
|
||||
User,
|
||||
OutOfOrder,
|
||||
};
|
||||
Q_ENUM(Error)
|
||||
|
||||
static QSharedPointer<DeviceVerificationFlow> NewInRoomVerification(
|
||||
QObject *parent_,
|
||||
TimelineModel *timelineModel_,
|
||||
const mtx::events::msg::KeyVerificationRequest &msg,
|
||||
QString other_user_,
|
||||
QString event_id_);
|
||||
static QSharedPointer<DeviceVerificationFlow> NewToDeviceVerification(
|
||||
QObject *parent_,
|
||||
const mtx::events::msg::KeyVerificationRequest &msg,
|
||||
QString other_user_,
|
||||
QString txn_id_);
|
||||
static QSharedPointer<DeviceVerificationFlow> NewToDeviceVerification(
|
||||
QObject *parent_,
|
||||
const mtx::events::msg::KeyVerificationStart &msg,
|
||||
QString other_user_,
|
||||
QString txn_id_);
|
||||
static QSharedPointer<DeviceVerificationFlow>
|
||||
InitiateUserVerification(QObject *parent_, TimelineModel *timelineModel_, QString userid);
|
||||
static QSharedPointer<DeviceVerificationFlow> InitiateDeviceVerification(QObject *parent,
|
||||
QString userid,
|
||||
QString device);
|
||||
|
||||
// getters
|
||||
QString state();
|
||||
Error error() { return error_; }
|
||||
QString getUserId();
|
||||
QString getDeviceId();
|
||||
bool getSender();
|
||||
std::vector<int> getSasList();
|
||||
QString transactionId() { return QString::fromStdString(this->transaction_id); }
|
||||
// setters
|
||||
void setDeviceId(QString deviceID);
|
||||
void setEventId(std::string event_id);
|
||||
|
||||
void callback_fn(const UserKeyCache &res, mtx::http::RequestErr err, std::string user_id);
|
||||
|
||||
public slots:
|
||||
//! unverifies a device
|
||||
void unverify();
|
||||
//! Continues the flow
|
||||
void next();
|
||||
//! Cancel the flow
|
||||
void cancel() { cancelVerification(User); }
|
||||
|
||||
signals:
|
||||
void refreshProfile();
|
||||
void stateChanged();
|
||||
void errorChanged();
|
||||
|
||||
private:
|
||||
DeviceVerificationFlow(QObject *,
|
||||
DeviceVerificationFlow::Type flow_type,
|
||||
TimelineModel *model,
|
||||
QString userID,
|
||||
QString deviceId_);
|
||||
void setState(State state)
|
||||
{
|
||||
if (state != state_) {
|
||||
state_ = state;
|
||||
emit stateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void handleStartMessage(const mtx::events::msg::KeyVerificationStart &msg, std::string);
|
||||
//! sends a verification request
|
||||
void sendVerificationRequest();
|
||||
//! accepts a verification request
|
||||
void sendVerificationReady();
|
||||
//! completes the verification flow();
|
||||
void sendVerificationDone();
|
||||
//! accepts a verification
|
||||
void acceptVerificationRequest();
|
||||
//! starts the verification flow
|
||||
void startVerificationRequest();
|
||||
//! cancels a verification flow
|
||||
void cancelVerification(DeviceVerificationFlow::Error error_code);
|
||||
//! sends the verification key
|
||||
void sendVerificationKey();
|
||||
//! sends the mac of the keys
|
||||
void sendVerificationMac();
|
||||
//! Completes the verification flow
|
||||
void acceptDevice();
|
||||
|
||||
std::string transaction_id;
|
||||
|
||||
bool sender;
|
||||
Type type;
|
||||
mtx::identifiers::User toClient;
|
||||
QString deviceId;
|
||||
|
||||
// public part of our master key, when trusted or empty
|
||||
std::string our_trusted_master_key;
|
||||
|
||||
mtx::events::msg::SASMethods method = mtx::events::msg::SASMethods::Emoji;
|
||||
QTimer *timeout = nullptr;
|
||||
sas_ptr sas;
|
||||
std::string mac_method;
|
||||
std::string commitment;
|
||||
nlohmann::json canonical_json;
|
||||
|
||||
std::vector<int> sasList;
|
||||
UserKeyCache their_keys;
|
||||
TimelineModel *model_;
|
||||
mtx::common::RelatesTo relation;
|
||||
|
||||
State state_ = PromptStartVerification;
|
||||
Error error_ = UnknownMethod;
|
||||
|
||||
bool isMacVerified = false;
|
||||
|
||||
template<typename T>
|
||||
void send(T msg)
|
||||
{
|
||||
if (this->type == DeviceVerificationFlow::Type::ToDevice) {
|
||||
mtx::requests::ToDeviceMessages<T> body;
|
||||
msg.transaction_id = this->transaction_id;
|
||||
body[this->toClient][deviceId.toStdString()] = msg;
|
||||
|
||||
http::client()->send_to_device<T>(
|
||||
this->transaction_id, body, [](mtx::http::RequestErr err) {
|
||||
if (err)
|
||||
nhlog::net()->warn(
|
||||
"failed to send verification to_device message: {} {}",
|
||||
err->matrix_error.error,
|
||||
static_cast<int>(err->status_code));
|
||||
});
|
||||
} else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
|
||||
if constexpr (!std::is_same_v<T, mtx::events::msg::KeyVerificationRequest>)
|
||||
msg.relates_to = this->relation;
|
||||
(model_)->sendMessageEvent(msg, mtx::events::to_device_content_to_type<T>);
|
||||
}
|
||||
|
||||
nhlog::net()->debug(
|
||||
"Sent verification step: {} in state: {}",
|
||||
mtx::events::to_string(mtx::events::to_device_content_to_type<T>),
|
||||
state().toStdString());
|
||||
}
|
||||
};
|
@ -39,8 +39,15 @@ struct EventMsgType
|
||||
template<class T>
|
||||
mtx::events::MessageType operator()(const mtx::events::Event<T> &e)
|
||||
{
|
||||
if constexpr (is_detected<msgtype_t, T>::value)
|
||||
return mtx::events::getMessageType(e.content.msgtype);
|
||||
if constexpr (is_detected<msgtype_t, T>::value) {
|
||||
if constexpr (std::is_same_v<std::optional<std::string>,
|
||||
std::remove_cv_t<decltype(e.content.msgtype)>>)
|
||||
return mtx::events::getMessageType(e.content.msgtype.value());
|
||||
else if constexpr (std::is_same_v<
|
||||
std::string,
|
||||
std::remove_cv_t<decltype(e.content.msgtype)>>)
|
||||
return mtx::events::getMessageType(e.content.msgtype);
|
||||
}
|
||||
return mtx::events::MessageType::Unknown;
|
||||
}
|
||||
};
|
||||
@ -97,8 +104,15 @@ struct EventBody
|
||||
template<class T>
|
||||
std::string operator()(const mtx::events::Event<T> &e)
|
||||
{
|
||||
if constexpr (is_detected<body_t, T>::value)
|
||||
return e.content.body;
|
||||
if constexpr (is_detected<body_t, T>::value) {
|
||||
if constexpr (std::is_same_v<std::optional<std::string>,
|
||||
std::remove_cv_t<decltype(e.content.body)>>)
|
||||
return e.content.body ? e.content.body.value() : "";
|
||||
else if constexpr (std::is_same_v<
|
||||
std::string,
|
||||
std::remove_cv_t<decltype(e.content.body)>>)
|
||||
return e.content.body;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
@ -328,15 +328,6 @@ MainWindow::hasActiveUser()
|
||||
settings.contains("auth/user_id");
|
||||
}
|
||||
|
||||
void
|
||||
MainWindow::openUserProfile(const QString &user_id, const QString &room_id)
|
||||
{
|
||||
auto dialog = new dialogs::UserProfile(this);
|
||||
dialog->init(user_id, room_id);
|
||||
|
||||
showDialog(dialog);
|
||||
}
|
||||
|
||||
void
|
||||
MainWindow::openRoomSettings(const QString &room_id)
|
||||
{
|
||||
|
@ -25,7 +25,6 @@
|
||||
#include <QSystemTrayIcon>
|
||||
|
||||
#include "UserSettingsPage.h"
|
||||
#include "dialogs/UserProfile.h"
|
||||
#include "ui/OverlayModal.h"
|
||||
|
||||
#include "jdenticoninterface.h"
|
||||
@ -76,7 +75,6 @@ public:
|
||||
void openLogoutDialog();
|
||||
void openRoomSettings(const QString &room_id);
|
||||
void openMemberListDialog(const QString &room_id);
|
||||
void openUserProfile(const QString &user_id, const QString &room_id);
|
||||
void openReadReceiptsDialog(const QString &event_id);
|
||||
|
||||
void hideOverlay();
|
||||
|
217
src/Olm.cpp
217
src/Olm.cpp
@ -1,9 +1,12 @@
|
||||
#include <QObject>
|
||||
#include <variant>
|
||||
|
||||
#include "Olm.h"
|
||||
|
||||
#include "Cache.h"
|
||||
#include "Cache_p.h"
|
||||
#include "ChatPage.h"
|
||||
#include "DeviceVerificationFlow.h"
|
||||
#include "Logging.h"
|
||||
#include "MatrixClient.h"
|
||||
#include "Utils.h"
|
||||
@ -28,7 +31,6 @@ handle_to_device_messages(const std::vector<mtx::events::collections::DeviceEven
|
||||
{
|
||||
if (msgs.empty())
|
||||
return;
|
||||
|
||||
nhlog::crypto()->info("received {} to_device messages", msgs.size());
|
||||
nlohmann::json j_msg;
|
||||
|
||||
@ -53,6 +55,10 @@ handle_to_device_messages(const std::vector<mtx::events::collections::DeviceEven
|
||||
nhlog::crypto()->warn("validation error for olm message: {} {}",
|
||||
e.what(),
|
||||
j_msg.dump(2));
|
||||
|
||||
nhlog::crypto()->warn("validation error for olm message: {} {}",
|
||||
e.what(),
|
||||
j_msg.dump(2));
|
||||
}
|
||||
|
||||
} else if (msg_type == to_string(mtx::events::EventType::RoomKeyRequest)) {
|
||||
@ -71,6 +77,43 @@ handle_to_device_messages(const std::vector<mtx::events::collections::DeviceEven
|
||||
e.what(),
|
||||
j_msg.dump(2));
|
||||
}
|
||||
} else if (msg_type == to_string(mtx::events::EventType::KeyVerificationAccept)) {
|
||||
auto message = std::get<
|
||||
mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationAccept>>(msg);
|
||||
ChatPage::instance()->receivedDeviceVerificationAccept(message.content);
|
||||
} else if (msg_type == to_string(mtx::events::EventType::KeyVerificationRequest)) {
|
||||
auto message = std::get<
|
||||
mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationRequest>>(msg);
|
||||
ChatPage::instance()->receivedDeviceVerificationRequest(message.content,
|
||||
message.sender);
|
||||
} else if (msg_type == to_string(mtx::events::EventType::KeyVerificationCancel)) {
|
||||
auto message = std::get<
|
||||
mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationCancel>>(msg);
|
||||
ChatPage::instance()->receivedDeviceVerificationCancel(message.content);
|
||||
} else if (msg_type == to_string(mtx::events::EventType::KeyVerificationKey)) {
|
||||
auto message =
|
||||
std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationKey>>(
|
||||
msg);
|
||||
ChatPage::instance()->receivedDeviceVerificationKey(message.content);
|
||||
} else if (msg_type == to_string(mtx::events::EventType::KeyVerificationMac)) {
|
||||
auto message =
|
||||
std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationMac>>(
|
||||
msg);
|
||||
ChatPage::instance()->receivedDeviceVerificationMac(message.content);
|
||||
} else if (msg_type == to_string(mtx::events::EventType::KeyVerificationStart)) {
|
||||
auto message = std::get<
|
||||
mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationStart>>(msg);
|
||||
ChatPage::instance()->receivedDeviceVerificationStart(message.content,
|
||||
message.sender);
|
||||
} else if (msg_type == to_string(mtx::events::EventType::KeyVerificationReady)) {
|
||||
auto message = std::get<
|
||||
mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationReady>>(msg);
|
||||
ChatPage::instance()->receivedDeviceVerificationReady(message.content);
|
||||
} else if (msg_type == to_string(mtx::events::EventType::KeyVerificationDone)) {
|
||||
auto message =
|
||||
std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationDone>>(
|
||||
msg);
|
||||
ChatPage::instance()->receivedDeviceVerificationDone(message.content);
|
||||
} else {
|
||||
nhlog::crypto()->warn("unhandled event: {}", j_msg.dump(2));
|
||||
}
|
||||
@ -95,23 +138,76 @@ handle_olm_message(const OlmMessage &msg)
|
||||
|
||||
auto payload = try_olm_decryption(msg.sender_key, cipher.second);
|
||||
|
||||
if (payload.is_null()) {
|
||||
// Check for PRE_KEY message
|
||||
if (cipher.second.type == 0) {
|
||||
payload = handle_pre_key_olm_message(
|
||||
msg.sender, msg.sender_key, cipher.second);
|
||||
} else {
|
||||
nhlog::crypto()->error("Undecryptable olm message!");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!payload.is_null()) {
|
||||
nhlog::crypto()->debug("decrypted olm payload: {}", payload.dump(2));
|
||||
create_inbound_megolm_session(msg.sender, msg.sender_key, payload);
|
||||
return;
|
||||
}
|
||||
std::string msg_type = payload["type"];
|
||||
|
||||
// Not a PRE_KEY message
|
||||
if (cipher.second.type != 0) {
|
||||
// TODO: log that it should have matched something
|
||||
return;
|
||||
if (msg_type == to_string(mtx::events::EventType::KeyVerificationAccept)) {
|
||||
ChatPage::instance()->receivedDeviceVerificationAccept(
|
||||
payload["content"]);
|
||||
return;
|
||||
} else if (msg_type ==
|
||||
to_string(mtx::events::EventType::KeyVerificationRequest)) {
|
||||
ChatPage::instance()->receivedDeviceVerificationRequest(
|
||||
payload["content"], payload["sender"]);
|
||||
return;
|
||||
} else if (msg_type ==
|
||||
to_string(mtx::events::EventType::KeyVerificationCancel)) {
|
||||
ChatPage::instance()->receivedDeviceVerificationCancel(
|
||||
payload["content"]);
|
||||
return;
|
||||
} else if (msg_type ==
|
||||
to_string(mtx::events::EventType::KeyVerificationKey)) {
|
||||
ChatPage::instance()->receivedDeviceVerificationKey(
|
||||
payload["content"]);
|
||||
return;
|
||||
} else if (msg_type ==
|
||||
to_string(mtx::events::EventType::KeyVerificationMac)) {
|
||||
ChatPage::instance()->receivedDeviceVerificationMac(
|
||||
payload["content"]);
|
||||
return;
|
||||
} else if (msg_type ==
|
||||
to_string(mtx::events::EventType::KeyVerificationStart)) {
|
||||
ChatPage::instance()->receivedDeviceVerificationStart(
|
||||
payload["content"], payload["sender"]);
|
||||
return;
|
||||
} else if (msg_type ==
|
||||
to_string(mtx::events::EventType::KeyVerificationReady)) {
|
||||
ChatPage::instance()->receivedDeviceVerificationReady(
|
||||
payload["content"]);
|
||||
return;
|
||||
} else if (msg_type ==
|
||||
to_string(mtx::events::EventType::KeyVerificationDone)) {
|
||||
ChatPage::instance()->receivedDeviceVerificationDone(
|
||||
payload["content"]);
|
||||
return;
|
||||
} else if (msg_type == to_string(mtx::events::EventType::RoomKey)) {
|
||||
mtx::events::DeviceEvent<mtx::events::msg::RoomKey> roomKey =
|
||||
payload;
|
||||
create_inbound_megolm_session(roomKey, msg.sender_key);
|
||||
return;
|
||||
} else if (msg_type ==
|
||||
to_string(mtx::events::EventType::ForwardedRoomKey)) {
|
||||
mtx::events::DeviceEvent<mtx::events::msg::ForwardedRoomKey>
|
||||
roomKey = payload;
|
||||
import_inbound_megolm_session(roomKey);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
handle_pre_key_olm_message(msg.sender, msg.sender_key, cipher.second);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
nlohmann::json
|
||||
handle_pre_key_olm_message(const std::string &sender,
|
||||
const std::string &sender_key,
|
||||
const mtx::events::msg::OlmCipherContent &content)
|
||||
@ -129,14 +225,14 @@ handle_pre_key_olm_message(const std::string &sender,
|
||||
} catch (const mtx::crypto::olm_exception &e) {
|
||||
nhlog::crypto()->critical(
|
||||
"failed to create inbound session with {}: {}", sender, e.what());
|
||||
return;
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!mtx::crypto::matches_inbound_session_from(
|
||||
inbound_session.get(), sender_key, content.body)) {
|
||||
nhlog::crypto()->warn("inbound olm session doesn't match sender's key ({})",
|
||||
sender);
|
||||
return;
|
||||
return {};
|
||||
}
|
||||
|
||||
mtx::crypto::BinaryBuf output;
|
||||
@ -146,7 +242,7 @@ handle_pre_key_olm_message(const std::string &sender,
|
||||
} catch (const mtx::crypto::olm_exception &e) {
|
||||
nhlog::crypto()->critical(
|
||||
"failed to decrypt olm message {}: {}", content.body, e.what());
|
||||
return;
|
||||
return {};
|
||||
}
|
||||
|
||||
auto plaintext = json::parse(std::string((char *)output.data(), output.size()));
|
||||
@ -159,7 +255,7 @@ handle_pre_key_olm_message(const std::string &sender,
|
||||
"failed to save inbound olm session from {}: {}", sender, e.what());
|
||||
}
|
||||
|
||||
create_inbound_megolm_session(sender, sender_key, plaintext);
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
mtx::events::msg::Encrypted
|
||||
@ -169,10 +265,15 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id,
|
||||
|
||||
// relations shouldn't be encrypted...
|
||||
mtx::common::ReplyRelatesTo relation;
|
||||
mtx::common::RelatesTo r_relation;
|
||||
|
||||
if (body["content"].contains("m.relates_to") &&
|
||||
body["content"]["m.relates_to"].contains("m.in_reply_to")) {
|
||||
relation = body["content"]["m.relates_to"];
|
||||
body["content"].erase("m.relates_to");
|
||||
} else if (body["content"]["m.relates_to"].contains("event_id")) {
|
||||
r_relation = body["content"]["m.relates_to"];
|
||||
body["content"].erase("m.relates_to");
|
||||
}
|
||||
|
||||
// Always check before for existence.
|
||||
@ -181,12 +282,13 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id,
|
||||
|
||||
// Prepare the m.room.encrypted event.
|
||||
msg::Encrypted data;
|
||||
data.ciphertext = std::string((char *)payload.data(), payload.size());
|
||||
data.sender_key = olm::client()->identity_keys().curve25519;
|
||||
data.session_id = res.data.session_id;
|
||||
data.device_id = device_id;
|
||||
data.algorithm = MEGOLM_ALGO;
|
||||
data.relates_to = relation;
|
||||
data.ciphertext = std::string((char *)payload.data(), payload.size());
|
||||
data.sender_key = olm::client()->identity_keys().curve25519;
|
||||
data.session_id = res.data.session_id;
|
||||
data.device_id = device_id;
|
||||
data.algorithm = MEGOLM_ALGO;
|
||||
data.relates_to = relation;
|
||||
data.r_relates_to = r_relation;
|
||||
|
||||
auto message_index = olm_outbound_group_session_message_index(res.session);
|
||||
nhlog::crypto()->debug("next message_index {}", message_index);
|
||||
@ -229,10 +331,12 @@ try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCip
|
||||
}
|
||||
|
||||
try {
|
||||
return json::parse(std::string((char *)text.data(), text.size()));
|
||||
return json::parse(std::string_view((char *)text.data(), text.size()));
|
||||
} catch (const json::exception &e) {
|
||||
nhlog::crypto()->critical("failed to parse the decrypted session msg: {}",
|
||||
e.what());
|
||||
nhlog::crypto()->critical(
|
||||
"failed to parse the decrypted session msg: {} {}",
|
||||
e.what(),
|
||||
std::string_view((char *)text.data(), text.size()));
|
||||
}
|
||||
}
|
||||
|
||||
@ -240,29 +344,17 @@ try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCip
|
||||
}
|
||||
|
||||
void
|
||||
create_inbound_megolm_session(const std::string &sender,
|
||||
const std::string &sender_key,
|
||||
const nlohmann::json &payload)
|
||||
create_inbound_megolm_session(const mtx::events::DeviceEvent<mtx::events::msg::RoomKey> &roomKey,
|
||||
const std::string &sender_key)
|
||||
{
|
||||
std::string room_id, session_id, session_key;
|
||||
|
||||
try {
|
||||
room_id = payload.at("content").at("room_id");
|
||||
session_id = payload.at("content").at("session_id");
|
||||
session_key = payload.at("content").at("session_key");
|
||||
} catch (const nlohmann::json::exception &e) {
|
||||
nhlog::crypto()->critical(
|
||||
"failed to parse plaintext olm message: {} {}", e.what(), payload.dump(2));
|
||||
return;
|
||||
}
|
||||
|
||||
MegolmSessionIndex index;
|
||||
index.room_id = room_id;
|
||||
index.session_id = session_id;
|
||||
index.room_id = roomKey.content.room_id;
|
||||
index.session_id = roomKey.content.session_id;
|
||||
index.sender_key = sender_key;
|
||||
|
||||
try {
|
||||
auto megolm_session = olm::client()->init_inbound_group_session(session_key);
|
||||
auto megolm_session =
|
||||
olm::client()->init_inbound_group_session(roomKey.content.session_key);
|
||||
cache::saveInboundMegolmSession(index, std::move(megolm_session));
|
||||
} catch (const lmdb::error &e) {
|
||||
nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what());
|
||||
@ -272,7 +364,34 @@ create_inbound_megolm_session(const std::string &sender,
|
||||
return;
|
||||
}
|
||||
|
||||
nhlog::crypto()->info("established inbound megolm session ({}, {})", room_id, sender);
|
||||
nhlog::crypto()->info(
|
||||
"established inbound megolm session ({}, {})", roomKey.content.room_id, roomKey.sender);
|
||||
}
|
||||
|
||||
void
|
||||
import_inbound_megolm_session(
|
||||
const mtx::events::DeviceEvent<mtx::events::msg::ForwardedRoomKey> &roomKey)
|
||||
{
|
||||
MegolmSessionIndex index;
|
||||
index.room_id = roomKey.content.room_id;
|
||||
index.session_id = roomKey.content.session_id;
|
||||
index.sender_key = roomKey.content.sender_key;
|
||||
|
||||
try {
|
||||
auto megolm_session =
|
||||
olm::client()->import_inbound_group_session(roomKey.content.session_key);
|
||||
cache::saveInboundMegolmSession(index, std::move(megolm_session));
|
||||
} catch (const lmdb::error &e) {
|
||||
nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what());
|
||||
return;
|
||||
} catch (const mtx::crypto::olm_exception &e) {
|
||||
nhlog::crypto()->critical("failed to import inbound megolm session: {}", e.what());
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO(Nico): Reload messages encrypted with this key.
|
||||
nhlog::crypto()->info(
|
||||
"established inbound megolm session ({}, {})", roomKey.content.room_id, roomKey.sender);
|
||||
}
|
||||
|
||||
void
|
||||
@ -373,6 +492,10 @@ handle_key_request_message(const mtx::events::DeviceEvent<mtx::events::msg::KeyR
|
||||
if (!cache::outboundMegolmSessionExists(req.content.room_id)) {
|
||||
nhlog::crypto()->warn("requested session not found in room: {}",
|
||||
req.content.room_id);
|
||||
|
||||
nhlog::crypto()->warn("requested session not found in room: {}",
|
||||
req.content.room_id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -477,9 +600,11 @@ send_megolm_key_to_device(const std::string &user_id,
|
||||
->create_room_key_event(UserId(user_id), pks.ed25519, payload)
|
||||
.dump();
|
||||
|
||||
mtx::requests::ClaimKeys claim_keys;
|
||||
claim_keys.one_time_keys[user_id][device_id] = mtx::crypto::SIGNED_CURVE25519;
|
||||
|
||||
http::client()->claim_keys(
|
||||
user_id,
|
||||
{device_id},
|
||||
claim_keys,
|
||||
[room_key, user_id, device_id, pks](const mtx::responses::ClaimKeys &res,
|
||||
mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
|
10
src/Olm.h
10
src/Olm.h
@ -71,11 +71,13 @@ handle_olm_message(const OlmMessage &msg);
|
||||
|
||||
//! Establish a new inbound megolm session with the decrypted payload from olm.
|
||||
void
|
||||
create_inbound_megolm_session(const std::string &sender,
|
||||
const std::string &sender_key,
|
||||
const nlohmann::json &payload);
|
||||
|
||||
create_inbound_megolm_session(const mtx::events::DeviceEvent<mtx::events::msg::RoomKey> &roomKey,
|
||||
const std::string &sender_key);
|
||||
void
|
||||
import_inbound_megolm_session(
|
||||
const mtx::events::DeviceEvent<mtx::events::msg::ForwardedRoomKey> &roomKey);
|
||||
|
||||
nlohmann::json
|
||||
handle_pre_key_olm_message(const std::string &sender,
|
||||
const std::string &sender_key,
|
||||
const mtx::events::msg::OlmCipherContent &content);
|
||||
|
@ -709,6 +709,8 @@ TextInputWidget::command(QString command, QString args)
|
||||
emit sendTextMessage("ノ┬─┬ノ ︵ ( \\o°o)\\");
|
||||
} else if (command == "clear-timeline") {
|
||||
emit clearRoomTimeline();
|
||||
} else if (command == "rotate-megolm-session") {
|
||||
emit rotateMegolmSession();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -186,6 +186,7 @@ signals:
|
||||
void sendBanRoomRequest(const QString &userid, const QString &reason);
|
||||
void sendUnbanRoomRequest(const QString &userid, const QString &reason);
|
||||
void changeRoomNick(const QString &displayname);
|
||||
void rotateMegolmSession();
|
||||
|
||||
void startedTyping();
|
||||
void stoppedTyping();
|
||||
|
@ -418,7 +418,6 @@ getPayloadType(const std::string &sdp, const std::string &name)
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool
|
||||
|
@ -131,5 +131,4 @@ AcceptCall::AcceptCall(const QString &caller,
|
||||
emit close();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -33,5 +33,4 @@ private:
|
||||
QPushButton *rejectBtn_;
|
||||
std::vector<std::string> audioDevices_;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -100,5 +100,4 @@ PlaceCall::PlaceCall(const QString &callee,
|
||||
emit close();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -33,5 +33,4 @@ private:
|
||||
QPushButton *cancelBtn_;
|
||||
std::vector<std::string> audioDevices_;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -60,5 +60,4 @@ private:
|
||||
EmojiCategory category_ = EmojiCategory::Search;
|
||||
emoji::Provider emoji_provider_;
|
||||
};
|
||||
|
||||
}
|
@ -33,5 +33,4 @@ private:
|
||||
return shortname.replace(" ", "-").replace(":", "-").replace("--", "-").toLower();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
#include "Cache.h"
|
||||
#include "Cache_p.h"
|
||||
#include "ChatPage.h"
|
||||
#include "EventAccessors.h"
|
||||
#include "Logging.h"
|
||||
#include "MatrixClient.h"
|
||||
@ -53,9 +54,8 @@ EventStore::EventStore(std::string room_id, QObject *)
|
||||
&EventStore::oldMessagesRetrieved,
|
||||
this,
|
||||
[this](const mtx::responses::Messages &res) {
|
||||
//
|
||||
uint64_t newFirst = cache::client()->saveOldMessages(room_id_, res);
|
||||
if (newFirst == first && !res.chunk.empty())
|
||||
if (newFirst == first)
|
||||
fetchMore();
|
||||
else {
|
||||
emit beginInsertRows(toExternalIdx(newFirst),
|
||||
@ -97,8 +97,8 @@ EventStore::EventStore(std::string room_id, QObject *)
|
||||
room_id_,
|
||||
txn_id,
|
||||
e.content,
|
||||
[this, txn_id](const mtx::responses::EventId &event_id,
|
||||
mtx::http::RequestErr err) {
|
||||
[this, txn_id, e](const mtx::responses::EventId &event_id,
|
||||
mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
const int status_code =
|
||||
static_cast<int>(err->status_code);
|
||||
@ -110,7 +110,21 @@ EventStore::EventStore(std::string room_id, QObject *)
|
||||
emit messageFailed(txn_id);
|
||||
return;
|
||||
}
|
||||
|
||||
emit messageSent(txn_id, event_id.event_id.to_string());
|
||||
if constexpr (std::is_same_v<
|
||||
decltype(e.content),
|
||||
mtx::events::msg::Encrypted>) {
|
||||
auto event =
|
||||
decryptEvent({room_id_, e.event_id}, e);
|
||||
if (auto dec =
|
||||
std::get_if<mtx::events::RoomEvent<
|
||||
mtx::events::msg::
|
||||
KeyVerificationRequest>>(event)) {
|
||||
emit updateFlowEventId(
|
||||
event_id.event_id.to_string());
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
event->data);
|
||||
@ -265,9 +279,78 @@ EventStore::handleSync(const mtx::responses::Timeline &events)
|
||||
emit dataChanged(toExternalIdx(*idx), toExternalIdx(*idx));
|
||||
}
|
||||
}
|
||||
|
||||
// decrypting and checking some encrypted messages
|
||||
if (auto encrypted =
|
||||
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
|
||||
&event)) {
|
||||
mtx::events::collections::TimelineEvents *d_event =
|
||||
decryptEvent({room_id_, encrypted->event_id}, *encrypted);
|
||||
if (std::visit(
|
||||
[](auto e) { return (e.sender != utils::localUser().toStdString()); },
|
||||
*d_event)) {
|
||||
handle_room_verification(*d_event);
|
||||
}
|
||||
// else {
|
||||
// // only the key.verification.ready sent by localuser's other
|
||||
// device
|
||||
// // is of significance as it is used for detecting accepted request
|
||||
// if (std::get_if<mtx::events::RoomEvent<
|
||||
// mtx::events::msg::KeyVerificationReady>>(d_event)) {
|
||||
// auto msg = std::get_if<mtx::events::RoomEvent<
|
||||
// mtx::events::msg::KeyVerificationReady>>(d_event);
|
||||
// ChatPage::instance()->receivedDeviceVerificationReady(
|
||||
// msg->content);
|
||||
// }
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
template<class... Ts>
|
||||
struct overloaded : Ts...
|
||||
{
|
||||
using Ts::operator()...;
|
||||
};
|
||||
template<class... Ts>
|
||||
overloaded(Ts...) -> overloaded<Ts...>;
|
||||
}
|
||||
|
||||
void
|
||||
EventStore::handle_room_verification(mtx::events::collections::TimelineEvents event)
|
||||
{
|
||||
std::visit(
|
||||
overloaded{
|
||||
[this](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg) {
|
||||
emit startDMVerification(msg);
|
||||
},
|
||||
[](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationCancel> &msg) {
|
||||
ChatPage::instance()->receivedDeviceVerificationCancel(msg.content);
|
||||
},
|
||||
[](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationAccept> &msg) {
|
||||
ChatPage::instance()->receivedDeviceVerificationAccept(msg.content);
|
||||
},
|
||||
[](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationKey> &msg) {
|
||||
ChatPage::instance()->receivedDeviceVerificationKey(msg.content);
|
||||
},
|
||||
[](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationMac> &msg) {
|
||||
ChatPage::instance()->receivedDeviceVerificationMac(msg.content);
|
||||
},
|
||||
[](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationReady> &msg) {
|
||||
ChatPage::instance()->receivedDeviceVerificationReady(msg.content);
|
||||
},
|
||||
[](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationDone> &msg) {
|
||||
ChatPage::instance()->receivedDeviceVerificationDone(msg.content);
|
||||
},
|
||||
[](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationStart> &msg) {
|
||||
ChatPage::instance()->receivedDeviceVerificationStart(msg.content, msg.sender);
|
||||
},
|
||||
[](const auto &) {},
|
||||
},
|
||||
event);
|
||||
}
|
||||
|
||||
QVariantList
|
||||
EventStore::reactions(const std::string &event_id)
|
||||
{
|
||||
@ -289,13 +372,14 @@ EventStore::reactions(const std::string &event_id)
|
||||
continue;
|
||||
|
||||
if (auto reaction = std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(
|
||||
related_event)) {
|
||||
auto &agg = aggregation[reaction->content.relates_to.key];
|
||||
related_event);
|
||||
reaction && reaction->content.relates_to.key) {
|
||||
auto &agg = aggregation[reaction->content.relates_to.key.value()];
|
||||
|
||||
if (agg.count == 0) {
|
||||
Reaction temp{};
|
||||
temp.key_ =
|
||||
QString::fromStdString(reaction->content.relates_to.key);
|
||||
QString::fromStdString(reaction->content.relates_to.key.value());
|
||||
reactions.push_back(temp);
|
||||
}
|
||||
|
||||
@ -407,11 +491,12 @@ EventStore::decryptEvent(const IdIndex &idx,
|
||||
|
||||
auto decryptionResult = olm::decryptEvent(index, e);
|
||||
|
||||
mtx::events::RoomEvent<mtx::events::msg::Notice> dummy;
|
||||
dummy.origin_server_ts = e.origin_server_ts;
|
||||
dummy.event_id = e.event_id;
|
||||
dummy.sender = e.sender;
|
||||
|
||||
if (decryptionResult.error) {
|
||||
mtx::events::RoomEvent<mtx::events::msg::Notice> dummy;
|
||||
dummy.origin_server_ts = e.origin_server_ts;
|
||||
dummy.event_id = e.event_id;
|
||||
dummy.sender = e.sender;
|
||||
switch (*decryptionResult.error) {
|
||||
case olm::DecryptionErrorCode::MissingSession:
|
||||
dummy.content.body =
|
||||
@ -484,6 +569,66 @@ EventStore::decryptEvent(const IdIndex &idx,
|
||||
return asCacheEntry(std::move(dummy));
|
||||
}
|
||||
|
||||
std::string msg_str;
|
||||
try {
|
||||
auto session = cache::client()->getInboundMegolmSession(index);
|
||||
auto res = olm::client()->decrypt_group_message(session, e.content.ciphertext);
|
||||
msg_str = std::string((char *)res.data.data(), res.data.size());
|
||||
} catch (const lmdb::error &e) {
|
||||
nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})",
|
||||
index.room_id,
|
||||
index.session_id,
|
||||
index.sender_key,
|
||||
e.what());
|
||||
dummy.content.body =
|
||||
tr("-- Decryption Error (failed to retrieve megolm keys from db) --",
|
||||
"Placeholder, when the message can't be decrypted, because the DB "
|
||||
"access "
|
||||
"failed.")
|
||||
.toStdString();
|
||||
return asCacheEntry(std::move(dummy));
|
||||
} catch (const mtx::crypto::olm_exception &e) {
|
||||
nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}",
|
||||
index.room_id,
|
||||
index.session_id,
|
||||
index.sender_key,
|
||||
e.what());
|
||||
dummy.content.body =
|
||||
tr("-- Decryption Error (%1) --",
|
||||
"Placeholder, when the message can't be decrypted. In this case, the "
|
||||
"Olm "
|
||||
"decrytion returned an error, which is passed as %1.")
|
||||
.arg(e.what())
|
||||
.toStdString();
|
||||
return asCacheEntry(std::move(dummy));
|
||||
}
|
||||
|
||||
// Add missing fields for the event.
|
||||
json body = json::parse(msg_str);
|
||||
body["event_id"] = e.event_id;
|
||||
body["sender"] = e.sender;
|
||||
body["origin_server_ts"] = e.origin_server_ts;
|
||||
body["unsigned"] = e.unsigned_data;
|
||||
|
||||
// relations are unencrypted in content...
|
||||
if (json old_ev = e; old_ev["content"].count("m.relates_to") != 0)
|
||||
body["content"]["m.relates_to"] = old_ev["content"]["m.relates_to"];
|
||||
|
||||
json event_array = json::array();
|
||||
event_array.push_back(body);
|
||||
|
||||
std::vector<mtx::events::collections::TimelineEvents> temp_events;
|
||||
mtx::responses::utils::parse_timeline_events(event_array, temp_events);
|
||||
|
||||
if (temp_events.size() == 1) {
|
||||
auto encInfo = mtx::accessors::file(temp_events[0]);
|
||||
|
||||
if (encInfo)
|
||||
emit newEncryptedImage(encInfo.value());
|
||||
|
||||
return asCacheEntry(std::move(temp_events[0]));
|
||||
}
|
||||
|
||||
auto encInfo = mtx::accessors::file(decryptionResult.event.value());
|
||||
if (encInfo)
|
||||
emit newEncryptedImage(encInfo.value());
|
||||
@ -515,7 +660,8 @@ EventStore::get(std::string_view id, std::string_view related_to, bool decrypt)
|
||||
mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
nhlog::net()->error(
|
||||
"Failed to retrieve event with id {}, which was "
|
||||
"Failed to retrieve event with id {}, which "
|
||||
"was "
|
||||
"requested to show the replyTo for event {}",
|
||||
relatedTo,
|
||||
id);
|
||||
|
@ -98,6 +98,9 @@ signals:
|
||||
void processPending();
|
||||
void messageSent(std::string txn_id, std::string event_id);
|
||||
void messageFailed(std::string txn_id);
|
||||
void startDMVerification(
|
||||
const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg);
|
||||
void updateFlowEventId(std::string event_id);
|
||||
|
||||
public slots:
|
||||
void addPending(mtx::events::collections::TimelineEvents event);
|
||||
@ -107,6 +110,7 @@ private:
|
||||
mtx::events::collections::TimelineEvents *decryptEvent(
|
||||
const IdIndex &idx,
|
||||
const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e);
|
||||
void handle_room_verification(mtx::events::collections::TimelineEvents event);
|
||||
|
||||
std::string room_id_;
|
||||
|
||||
|
@ -116,7 +116,46 @@ struct RoomEventType
|
||||
{
|
||||
return qml_mtx_events::EventType::VideoMessage;
|
||||
}
|
||||
|
||||
qml_mtx_events::EventType operator()(
|
||||
const mtx::events::Event<mtx::events::msg::KeyVerificationRequest> &)
|
||||
{
|
||||
return qml_mtx_events::EventType::KeyVerificationRequest;
|
||||
}
|
||||
qml_mtx_events::EventType operator()(
|
||||
const mtx::events::Event<mtx::events::msg::KeyVerificationStart> &)
|
||||
{
|
||||
return qml_mtx_events::EventType::KeyVerificationStart;
|
||||
}
|
||||
qml_mtx_events::EventType operator()(
|
||||
const mtx::events::Event<mtx::events::msg::KeyVerificationMac> &)
|
||||
{
|
||||
return qml_mtx_events::EventType::KeyVerificationMac;
|
||||
}
|
||||
qml_mtx_events::EventType operator()(
|
||||
const mtx::events::Event<mtx::events::msg::KeyVerificationAccept> &)
|
||||
{
|
||||
return qml_mtx_events::EventType::KeyVerificationAccept;
|
||||
}
|
||||
qml_mtx_events::EventType operator()(
|
||||
const mtx::events::Event<mtx::events::msg::KeyVerificationReady> &)
|
||||
{
|
||||
return qml_mtx_events::EventType::KeyVerificationReady;
|
||||
}
|
||||
qml_mtx_events::EventType operator()(
|
||||
const mtx::events::Event<mtx::events::msg::KeyVerificationCancel> &)
|
||||
{
|
||||
return qml_mtx_events::EventType::KeyVerificationCancel;
|
||||
}
|
||||
qml_mtx_events::EventType operator()(
|
||||
const mtx::events::Event<mtx::events::msg::KeyVerificationKey> &)
|
||||
{
|
||||
return qml_mtx_events::EventType::KeyVerificationKey;
|
||||
}
|
||||
qml_mtx_events::EventType operator()(
|
||||
const mtx::events::Event<mtx::events::msg::KeyVerificationDone> &)
|
||||
{
|
||||
return qml_mtx_events::EventType::KeyVerificationDone;
|
||||
}
|
||||
qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Redacted> &)
|
||||
{
|
||||
return qml_mtx_events::EventType::Redacted;
|
||||
@ -211,6 +250,15 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
|
||||
connect(&events, &EventStore::newEncryptedImage, this, &TimelineModel::newEncryptedImage);
|
||||
connect(
|
||||
&events, &EventStore::fetchedMore, this, [this]() { setPaginationInProgress(false); });
|
||||
connect(&events,
|
||||
&EventStore::startDMVerification,
|
||||
this,
|
||||
[this](mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> msg) {
|
||||
ChatPage::instance()->receivedRoomDeviceVerificationRequest(msg, this);
|
||||
});
|
||||
connect(&events, &EventStore::updateFlowEventId, this, [this](std::string event_id) {
|
||||
this->updateFlowEventId(event_id);
|
||||
});
|
||||
}
|
||||
|
||||
QHash<int, QByteArray>
|
||||
@ -743,9 +791,9 @@ TimelineModel::viewDecryptedRawMessage(QString id) const
|
||||
}
|
||||
|
||||
void
|
||||
TimelineModel::openUserProfile(QString userid) const
|
||||
TimelineModel::openUserProfile(QString userid)
|
||||
{
|
||||
MainWindow::instance()->openUserProfile(userid, room_id_);
|
||||
emit openProfile(new UserProfile(room_id_, userid, manager_, this));
|
||||
}
|
||||
|
||||
void
|
||||
@ -846,18 +894,18 @@ TimelineModel::markEventsAsRead(const std::vector<QString> &event_ids)
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void
|
||||
TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
|
||||
nlohmann::json content,
|
||||
mtx::events::EventType eventType)
|
||||
TimelineModel::sendEncryptedMessage(mtx::events::RoomEvent<T> msg, mtx::events::EventType eventType)
|
||||
{
|
||||
const auto room_id = room_id_.toStdString();
|
||||
|
||||
using namespace mtx::events;
|
||||
using namespace mtx::identifiers;
|
||||
|
||||
json doc = {
|
||||
{"type", mtx::events::to_string(eventType)}, {"content", content}, {"room_id", room_id}};
|
||||
json doc = {{"type", mtx::events::to_string(eventType)},
|
||||
{"content", json(msg.content)},
|
||||
{"room_id", room_id}};
|
||||
|
||||
try {
|
||||
// Check if we have already an outbound megolm session then we can use.
|
||||
@ -865,7 +913,7 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
|
||||
mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> event;
|
||||
event.content =
|
||||
olm::encrypt_group_message(room_id, http::client()->device_id(), doc);
|
||||
event.event_id = txn_id;
|
||||
event.event_id = msg.event_id;
|
||||
event.room_id = room_id;
|
||||
event.sender = http::client()->user_id().to_string();
|
||||
event.type = mtx::events::EventType::RoomEncrypted;
|
||||
@ -893,32 +941,43 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
|
||||
OutboundGroupSessionData session_data;
|
||||
session_data.session_id = session_id;
|
||||
session_data.session_key = session_key;
|
||||
session_data.message_index = 0; // TODO Update me
|
||||
session_data.message_index = 0;
|
||||
cache::saveOutboundMegolmSession(
|
||||
room_id, session_data, std::move(outbound_session));
|
||||
|
||||
{
|
||||
MegolmSessionIndex index;
|
||||
index.room_id = room_id;
|
||||
index.session_id = session_id;
|
||||
index.sender_key = olm::client()->identity_keys().curve25519;
|
||||
auto megolm_session =
|
||||
olm::client()->init_inbound_group_session(session_key);
|
||||
cache::saveInboundMegolmSession(index, std::move(megolm_session));
|
||||
}
|
||||
|
||||
const auto members = cache::roomMembers(room_id);
|
||||
nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id);
|
||||
|
||||
auto keeper = std::make_shared<StateKeeper>([room_id, doc, txn_id, this]() {
|
||||
try {
|
||||
mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> event;
|
||||
event.content = olm::encrypt_group_message(
|
||||
room_id, http::client()->device_id(), doc);
|
||||
event.event_id = txn_id;
|
||||
event.room_id = room_id;
|
||||
event.sender = http::client()->user_id().to_string();
|
||||
event.type = mtx::events::EventType::RoomEncrypted;
|
||||
event.origin_server_ts = QDateTime::currentMSecsSinceEpoch();
|
||||
auto keeper =
|
||||
std::make_shared<StateKeeper>([room_id, doc, txn_id = msg.event_id, this]() {
|
||||
try {
|
||||
mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> event;
|
||||
event.content = olm::encrypt_group_message(
|
||||
room_id, http::client()->device_id(), doc);
|
||||
event.event_id = txn_id;
|
||||
event.room_id = room_id;
|
||||
event.sender = http::client()->user_id().to_string();
|
||||
event.type = mtx::events::EventType::RoomEncrypted;
|
||||
event.origin_server_ts = QDateTime::currentMSecsSinceEpoch();
|
||||
|
||||
emit this->addPendingMessageToStore(event);
|
||||
} catch (const lmdb::error &e) {
|
||||
nhlog::db()->critical("failed to save megolm outbound session: {}",
|
||||
e.what());
|
||||
emit ChatPage::instance()->showNotification(
|
||||
tr("Failed to encrypt event, sending aborted!"));
|
||||
}
|
||||
});
|
||||
emit this->addPendingMessageToStore(event);
|
||||
} catch (const lmdb::error &e) {
|
||||
nhlog::db()->critical(
|
||||
"failed to save megolm outbound session: {}", e.what());
|
||||
emit ChatPage::instance()->showNotification(
|
||||
tr("Failed to encrypt event, sending aborted!"));
|
||||
}
|
||||
});
|
||||
|
||||
mtx::requests::QueryKeys req;
|
||||
for (const auto &member : members)
|
||||
@ -926,7 +985,7 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
|
||||
|
||||
http::client()->query_keys(
|
||||
req,
|
||||
[keeper = std::move(keeper), megolm_payload, txn_id, this](
|
||||
[keeper = std::move(keeper), megolm_payload, txn_id = msg.event_id, this](
|
||||
const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
nhlog::net()->warn("failed to query device keys: {} {}",
|
||||
@ -937,19 +996,23 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
|
||||
return;
|
||||
}
|
||||
|
||||
mtx::requests::ClaimKeys claim_keys;
|
||||
|
||||
// Mapping from user id to a device_id with valid identity keys to the
|
||||
// generated room_key event used for sharing the megolm session.
|
||||
std::map<std::string, std::map<std::string, std::string>> room_key_msgs;
|
||||
std::map<std::string, std::map<std::string, DevicePublicKeys>> deviceKeys;
|
||||
|
||||
for (const auto &user : res.device_keys) {
|
||||
// Mapping from a device_id with valid identity keys to the
|
||||
// generated room_key event used for sharing the megolm session.
|
||||
std::map<std::string, std::string> room_key_msgs;
|
||||
std::map<std::string, DevicePublicKeys> deviceKeys;
|
||||
|
||||
room_key_msgs.clear();
|
||||
deviceKeys.clear();
|
||||
|
||||
for (const auto &dev : user.second) {
|
||||
const auto user_id = ::UserId(dev.second.user_id);
|
||||
const auto device_id = DeviceId(dev.second.device_id);
|
||||
|
||||
if (user_id.get() ==
|
||||
http::client()->user_id().to_string() &&
|
||||
device_id.get() == http::client()->device_id())
|
||||
continue;
|
||||
|
||||
const auto device_keys = dev.second.keys;
|
||||
const auto curveKey = "curve25519:" + device_id.get();
|
||||
const auto edKey = "ed25519:" + device_id.get();
|
||||
@ -968,7 +1031,7 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
|
||||
|
||||
try {
|
||||
if (!mtx::crypto::verify_identity_signature(
|
||||
json(dev.second), device_id, user_id)) {
|
||||
dev.second, device_id, user_id)) {
|
||||
nhlog::crypto()->warn(
|
||||
"failed to verify identity keys: {}",
|
||||
json(dev.second).dump(2));
|
||||
@ -991,42 +1054,25 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
|
||||
user_id, pks.ed25519, megolm_payload)
|
||||
.dump();
|
||||
|
||||
room_key_msgs.emplace(device_id, room_key);
|
||||
deviceKeys.emplace(device_id, pks);
|
||||
room_key_msgs[user_id].emplace(device_id, room_key);
|
||||
deviceKeys[user_id].emplace(device_id, pks);
|
||||
claim_keys.one_time_keys[user.first][device_id] =
|
||||
mtx::crypto::SIGNED_CURVE25519;
|
||||
|
||||
nhlog::net()->info("{}", device_id.get());
|
||||
nhlog::net()->info(" curve25519 {}", pks.curve25519);
|
||||
nhlog::net()->info(" ed25519 {}", pks.ed25519);
|
||||
}
|
||||
|
||||
std::vector<std::string> valid_devices;
|
||||
valid_devices.reserve(room_key_msgs.size());
|
||||
for (auto const &d : room_key_msgs) {
|
||||
valid_devices.push_back(d.first);
|
||||
|
||||
nhlog::net()->info("{}", d.first);
|
||||
nhlog::net()->info(" curve25519 {}",
|
||||
deviceKeys.at(d.first).curve25519);
|
||||
nhlog::net()->info(" ed25519 {}",
|
||||
deviceKeys.at(d.first).ed25519);
|
||||
}
|
||||
|
||||
nhlog::net()->info(
|
||||
"sending claim request for user {} with {} devices",
|
||||
user.first,
|
||||
valid_devices.size());
|
||||
|
||||
http::client()->claim_keys(
|
||||
user.first,
|
||||
valid_devices,
|
||||
std::bind(&TimelineModel::handleClaimedKeys,
|
||||
this,
|
||||
keeper,
|
||||
room_key_msgs,
|
||||
deviceKeys,
|
||||
user.first,
|
||||
std::placeholders::_1,
|
||||
std::placeholders::_2));
|
||||
|
||||
// TODO: Wait before sending the next batch of requests.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
}
|
||||
|
||||
http::client()->claim_keys(claim_keys,
|
||||
std::bind(&TimelineModel::handleClaimedKeys,
|
||||
this,
|
||||
keeper,
|
||||
room_key_msgs,
|
||||
deviceKeys,
|
||||
std::placeholders::_1,
|
||||
std::placeholders::_2));
|
||||
});
|
||||
|
||||
// TODO: Let the user know about the errors.
|
||||
@ -1044,12 +1090,12 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
|
||||
}
|
||||
|
||||
void
|
||||
TimelineModel::handleClaimedKeys(std::shared_ptr<StateKeeper> keeper,
|
||||
const std::map<std::string, std::string> &room_keys,
|
||||
const std::map<std::string, DevicePublicKeys> &pks,
|
||||
const std::string &user_id,
|
||||
const mtx::responses::ClaimKeys &res,
|
||||
mtx::http::RequestErr err)
|
||||
TimelineModel::handleClaimedKeys(
|
||||
std::shared_ptr<StateKeeper> keeper,
|
||||
const std::map<std::string, std::map<std::string, std::string>> &room_keys,
|
||||
const std::map<std::string, std::map<std::string, DevicePublicKeys>> &pks,
|
||||
const mtx::responses::ClaimKeys &res,
|
||||
mtx::http::RequestErr err)
|
||||
{
|
||||
if (err) {
|
||||
nhlog::net()->warn("claim keys error: {} {} {}",
|
||||
@ -1059,65 +1105,59 @@ TimelineModel::handleClaimedKeys(std::shared_ptr<StateKeeper> keeper,
|
||||
return;
|
||||
}
|
||||
|
||||
nhlog::net()->debug("claimed keys for {}", user_id);
|
||||
|
||||
if (res.one_time_keys.size() == 0) {
|
||||
nhlog::net()->debug("no one-time keys found for user_id: {}", user_id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) {
|
||||
nhlog::net()->debug("no one-time keys found for user_id: {}", user_id);
|
||||
return;
|
||||
}
|
||||
|
||||
auto retrieved_devices = res.one_time_keys.at(user_id);
|
||||
|
||||
// Payload with all the to_device message to be sent.
|
||||
json body;
|
||||
body["messages"][user_id] = json::object();
|
||||
nlohmann::json body;
|
||||
|
||||
for (const auto &rd : retrieved_devices) {
|
||||
const auto device_id = rd.first;
|
||||
nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2));
|
||||
|
||||
// TODO: Verify signatures
|
||||
auto otk = rd.second.begin()->at("key");
|
||||
|
||||
if (pks.find(device_id) == pks.end()) {
|
||||
nhlog::net()->critical("couldn't find public key for device: {}",
|
||||
device_id);
|
||||
continue;
|
||||
for (const auto &[user_id, retrieved_devices] : res.one_time_keys) {
|
||||
nhlog::net()->debug("claimed keys for {}", user_id);
|
||||
if (retrieved_devices.size() == 0) {
|
||||
nhlog::net()->debug("no one-time keys found for user_id: {}", user_id);
|
||||
return;
|
||||
}
|
||||
|
||||
auto id_key = pks.at(device_id).curve25519;
|
||||
auto s = olm::client()->create_outbound_session(id_key, otk);
|
||||
for (const auto &rd : retrieved_devices) {
|
||||
const auto device_id = rd.first;
|
||||
|
||||
if (room_keys.find(device_id) == room_keys.end()) {
|
||||
nhlog::net()->critical("couldn't find m.room_key for device: {}",
|
||||
device_id);
|
||||
continue;
|
||||
nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2));
|
||||
|
||||
if (rd.second.empty() || !rd.second.begin()->contains("key")) {
|
||||
nhlog::net()->warn("Skipping device {} as it has no key.",
|
||||
device_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Verify signatures
|
||||
auto otk = rd.second.begin()->at("key");
|
||||
|
||||
auto id_key = pks.at(user_id).at(device_id).curve25519;
|
||||
auto s = olm::client()->create_outbound_session(id_key, otk);
|
||||
|
||||
auto device_msg = olm::client()->create_olm_encrypted_content(
|
||||
s.get(),
|
||||
room_keys.at(user_id).at(device_id),
|
||||
pks.at(user_id).at(device_id).curve25519);
|
||||
|
||||
try {
|
||||
cache::saveOlmSession(id_key, std::move(s));
|
||||
} catch (const lmdb::error &e) {
|
||||
nhlog::db()->critical("failed to save outbound olm session: {}",
|
||||
e.what());
|
||||
} catch (const mtx::crypto::olm_exception &e) {
|
||||
nhlog::crypto()->critical(
|
||||
"failed to pickle outbound olm session: {}", e.what());
|
||||
}
|
||||
|
||||
body["messages"][user_id][device_id] = device_msg;
|
||||
}
|
||||
|
||||
auto device_msg = olm::client()->create_olm_encrypted_content(
|
||||
s.get(), room_keys.at(device_id), pks.at(device_id).curve25519);
|
||||
|
||||
try {
|
||||
cache::saveOlmSession(id_key, std::move(s));
|
||||
} catch (const lmdb::error &e) {
|
||||
nhlog::db()->critical("failed to save outbound olm session: {}", e.what());
|
||||
} catch (const mtx::crypto::olm_exception &e) {
|
||||
nhlog::crypto()->critical("failed to pickle outbound olm session: {}",
|
||||
e.what());
|
||||
}
|
||||
|
||||
body["messages"][user_id][device_id] = device_msg;
|
||||
nhlog::net()->info("send_to_device: {}", user_id);
|
||||
}
|
||||
|
||||
nhlog::net()->info("send_to_device: {}", user_id);
|
||||
|
||||
http::client()->send_to_device(
|
||||
"m.room.encrypted", body, [keeper](mtx::http::RequestErr err) {
|
||||
mtx::events::to_string(mtx::events::EventType::RoomEncrypted),
|
||||
http::client()->generate_txn_id(),
|
||||
body,
|
||||
[keeper](mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
nhlog::net()->warn("failed to send "
|
||||
"send_to_device "
|
||||
@ -1143,8 +1183,7 @@ struct SendMessageVisitor
|
||||
if (encInfo)
|
||||
emit model_->newEncryptedImage(encInfo.value());
|
||||
|
||||
model_->sendEncryptedMessageEvent(
|
||||
msg.event_id, nlohmann::json(msg.content), Event);
|
||||
model_->sendEncryptedMessage(msg, Event);
|
||||
} else {
|
||||
msg.type = Event;
|
||||
emit model_->addPendingMessageToStore(msg);
|
||||
@ -1199,6 +1238,54 @@ struct SendMessageVisitor
|
||||
event);
|
||||
}
|
||||
|
||||
void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg)
|
||||
{
|
||||
sendRoomEvent<mtx::events::msg::KeyVerificationRequest,
|
||||
mtx::events::EventType::RoomMessage>(msg);
|
||||
}
|
||||
|
||||
void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationReady> &msg)
|
||||
{
|
||||
sendRoomEvent<mtx::events::msg::KeyVerificationReady,
|
||||
mtx::events::EventType::KeyVerificationReady>(msg);
|
||||
}
|
||||
|
||||
void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationStart> &msg)
|
||||
{
|
||||
sendRoomEvent<mtx::events::msg::KeyVerificationStart,
|
||||
mtx::events::EventType::KeyVerificationStart>(msg);
|
||||
}
|
||||
|
||||
void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationAccept> &msg)
|
||||
{
|
||||
sendRoomEvent<mtx::events::msg::KeyVerificationAccept,
|
||||
mtx::events::EventType::KeyVerificationAccept>(msg);
|
||||
}
|
||||
|
||||
void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationMac> &msg)
|
||||
{
|
||||
sendRoomEvent<mtx::events::msg::KeyVerificationMac,
|
||||
mtx::events::EventType::KeyVerificationMac>(msg);
|
||||
}
|
||||
|
||||
void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationKey> &msg)
|
||||
{
|
||||
sendRoomEvent<mtx::events::msg::KeyVerificationKey,
|
||||
mtx::events::EventType::KeyVerificationKey>(msg);
|
||||
}
|
||||
|
||||
void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationDone> &msg)
|
||||
{
|
||||
sendRoomEvent<mtx::events::msg::KeyVerificationDone,
|
||||
mtx::events::EventType::KeyVerificationDone>(msg);
|
||||
}
|
||||
|
||||
void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationCancel> &msg)
|
||||
{
|
||||
sendRoomEvent<mtx::events::msg::KeyVerificationCancel,
|
||||
mtx::events::EventType::KeyVerificationCancel>(msg);
|
||||
}
|
||||
|
||||
TimelineModel *model_;
|
||||
};
|
||||
|
||||
|
@ -10,6 +10,7 @@
|
||||
|
||||
#include "CacheCryptoStructs.h"
|
||||
#include "EventStore.h"
|
||||
#include "ui/UserProfile.h"
|
||||
|
||||
namespace mtx::http {
|
||||
using RequestErr = const std::optional<mtx::http::ClientError> &;
|
||||
@ -87,6 +88,14 @@ enum EventType
|
||||
VideoMessage,
|
||||
Redacted,
|
||||
UnknownMessage,
|
||||
KeyVerificationRequest,
|
||||
KeyVerificationStart,
|
||||
KeyVerificationMac,
|
||||
KeyVerificationAccept,
|
||||
KeyVerificationCancel,
|
||||
KeyVerificationKey,
|
||||
KeyVerificationDone,
|
||||
KeyVerificationReady
|
||||
};
|
||||
Q_ENUM_NS(EventType)
|
||||
|
||||
@ -199,7 +208,7 @@ public:
|
||||
|
||||
Q_INVOKABLE void viewRawMessage(QString id) const;
|
||||
Q_INVOKABLE void viewDecryptedRawMessage(QString id) const;
|
||||
Q_INVOKABLE void openUserProfile(QString userid) const;
|
||||
Q_INVOKABLE void openUserProfile(QString userid);
|
||||
Q_INVOKABLE void replyAction(QString id);
|
||||
Q_INVOKABLE void readReceiptsAction(QString id) const;
|
||||
Q_INVOKABLE void redactEvent(QString id);
|
||||
@ -275,23 +284,25 @@ signals:
|
||||
void paginationInProgressChanged(const bool);
|
||||
void newCallEvent(const mtx::events::collections::TimelineEvents &event);
|
||||
|
||||
void openProfile(UserProfile *profile);
|
||||
|
||||
void newMessageToSend(mtx::events::collections::TimelineEvents event);
|
||||
void addPendingMessageToStore(mtx::events::collections::TimelineEvents event);
|
||||
void updateFlowEventId(std::string event_id);
|
||||
|
||||
void roomNameChanged();
|
||||
void roomTopicChanged();
|
||||
void roomAvatarUrlChanged();
|
||||
|
||||
private:
|
||||
void sendEncryptedMessageEvent(const std::string &txn_id,
|
||||
nlohmann::json content,
|
||||
mtx::events::EventType);
|
||||
void handleClaimedKeys(std::shared_ptr<StateKeeper> keeper,
|
||||
const std::map<std::string, std::string> &room_key,
|
||||
const std::map<std::string, DevicePublicKeys> &pks,
|
||||
const std::string &user_id,
|
||||
const mtx::responses::ClaimKeys &res,
|
||||
mtx::http::RequestErr err);
|
||||
template<typename T>
|
||||
void sendEncryptedMessage(mtx::events::RoomEvent<T> msg, mtx::events::EventType eventType);
|
||||
void handleClaimedKeys(
|
||||
std::shared_ptr<StateKeeper> keeper,
|
||||
const std::map<std::string, std::map<std::string, std::string>> &room_keys,
|
||||
const std::map<std::string, std::map<std::string, DevicePublicKeys>> &pks,
|
||||
const mtx::responses::ClaimKeys &res,
|
||||
mtx::http::RequestErr err);
|
||||
void readEvent(const std::string &id);
|
||||
|
||||
void setPaginationInProgress(const bool paginationInProgress);
|
||||
|
@ -4,6 +4,7 @@
|
||||
#include <QMetaType>
|
||||
#include <QPalette>
|
||||
#include <QQmlContext>
|
||||
#include <QQmlEngine>
|
||||
#include <QString>
|
||||
|
||||
#include "BlurhashProvider.h"
|
||||
@ -19,7 +20,12 @@
|
||||
#include "emoji/EmojiModel.h"
|
||||
#include "emoji/Provider.h"
|
||||
|
||||
#include <iostream> //only for debugging
|
||||
|
||||
Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents)
|
||||
Q_DECLARE_METATYPE(std::vector<DeviceInfo>)
|
||||
|
||||
namespace msgs = mtx::events::msg;
|
||||
|
||||
void
|
||||
TimelineViewManager::updateEncryptedDescriptions()
|
||||
@ -65,9 +71,13 @@ TimelineViewManager::userColor(QString id, QColor background)
|
||||
QString
|
||||
TimelineViewManager::userPresence(QString id) const
|
||||
{
|
||||
return QString::fromStdString(
|
||||
mtx::presence::to_string(cache::presenceState(id.toStdString())));
|
||||
if (id.isEmpty())
|
||||
return "";
|
||||
else
|
||||
return QString::fromStdString(
|
||||
mtx::presence::to_string(cache::presenceState(id.toStdString())));
|
||||
}
|
||||
|
||||
QString
|
||||
TimelineViewManager::userStatus(QString id) const
|
||||
{
|
||||
@ -83,15 +93,52 @@ TimelineViewManager::TimelineViewManager(QSharedPointer<UserSettings> userSettin
|
||||
, callManager_(callManager)
|
||||
, settings(userSettings)
|
||||
{
|
||||
qRegisterMetaType<mtx::events::msg::KeyVerificationAccept>();
|
||||
qRegisterMetaType<mtx::events::msg::KeyVerificationCancel>();
|
||||
qRegisterMetaType<mtx::events::msg::KeyVerificationDone>();
|
||||
qRegisterMetaType<mtx::events::msg::KeyVerificationKey>();
|
||||
qRegisterMetaType<mtx::events::msg::KeyVerificationMac>();
|
||||
qRegisterMetaType<mtx::events::msg::KeyVerificationReady>();
|
||||
qRegisterMetaType<mtx::events::msg::KeyVerificationRequest>();
|
||||
qRegisterMetaType<mtx::events::msg::KeyVerificationStart>();
|
||||
|
||||
qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject,
|
||||
"im.nheko",
|
||||
1,
|
||||
0,
|
||||
"MtxEvent",
|
||||
"Can't instantiate enum!");
|
||||
qmlRegisterUncreatableMetaObject(verification::staticMetaObject,
|
||||
"im.nheko",
|
||||
1,
|
||||
0,
|
||||
"VerificationStatus",
|
||||
"Can't instantiate enum!");
|
||||
|
||||
qmlRegisterType<DelegateChoice>("im.nheko", 1, 0, "DelegateChoice");
|
||||
qmlRegisterType<DelegateChooser>("im.nheko", 1, 0, "DelegateChooser");
|
||||
qmlRegisterUncreatableType<DeviceVerificationFlow>(
|
||||
"im.nheko", 1, 0, "DeviceVerificationFlow", "Can't create verification flow from QML!");
|
||||
qmlRegisterUncreatableType<UserProfile>(
|
||||
"im.nheko",
|
||||
1,
|
||||
0,
|
||||
"UserProfileModel",
|
||||
"UserProfile needs to be instantiated on the C++ side");
|
||||
|
||||
static auto self = this;
|
||||
qmlRegisterSingletonType<TimelineViewManager>(
|
||||
"im.nheko", 1, 0, "TimelineManager", [](QQmlEngine *, QJSEngine *) -> QObject * {
|
||||
return self;
|
||||
});
|
||||
qmlRegisterSingletonType<UserSettings>(
|
||||
"im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * {
|
||||
return self->settings.data();
|
||||
});
|
||||
|
||||
qRegisterMetaType<mtx::events::collections::TimelineEvents>();
|
||||
qRegisterMetaType<std::vector<DeviceInfo>>();
|
||||
|
||||
qmlRegisterType<emoji::EmojiModel>("im.nheko.EmojiModel", 1, 0, "EmojiModel");
|
||||
qmlRegisterType<emoji::EmojiProxyModel>("im.nheko.EmojiModel", 1, 0, "EmojiProxyModel");
|
||||
qmlRegisterUncreatableType<QAbstractItemModel>(
|
||||
@ -123,8 +170,6 @@ TimelineViewManager::TimelineViewManager(QSharedPointer<UserSettings> userSettin
|
||||
});
|
||||
#endif
|
||||
container->setMinimumSize(200, 200);
|
||||
view->rootContext()->setContextProperty("timelineManager", this);
|
||||
view->rootContext()->setContextProperty("settings", settings.data());
|
||||
updateColorPalette();
|
||||
view->engine()->addImageProvider("MxcImage", imgProvider);
|
||||
view->engine()->addImageProvider("colorimage", colorImgProvider);
|
||||
@ -136,6 +181,57 @@ TimelineViewManager::TimelineViewManager(QSharedPointer<UserSettings> userSettin
|
||||
&ChatPage::decryptSidebarChanged,
|
||||
this,
|
||||
&TimelineViewManager::updateEncryptedDescriptions);
|
||||
connect(
|
||||
dynamic_cast<ChatPage *>(parent),
|
||||
&ChatPage::receivedRoomDeviceVerificationRequest,
|
||||
this,
|
||||
[this](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &message,
|
||||
TimelineModel *model) {
|
||||
auto event_id = QString::fromStdString(message.event_id);
|
||||
if (!this->dvList.contains(event_id)) {
|
||||
if (auto flow = DeviceVerificationFlow::NewInRoomVerification(
|
||||
this,
|
||||
model,
|
||||
message.content,
|
||||
QString::fromStdString(message.sender),
|
||||
event_id)) {
|
||||
dvList[event_id] = flow;
|
||||
emit newDeviceVerificationRequest(flow.data());
|
||||
}
|
||||
}
|
||||
});
|
||||
connect(dynamic_cast<ChatPage *>(parent),
|
||||
&ChatPage::receivedDeviceVerificationRequest,
|
||||
this,
|
||||
[this](const mtx::events::msg::KeyVerificationRequest &msg, std::string sender) {
|
||||
if (!msg.transaction_id)
|
||||
return;
|
||||
|
||||
auto txnid = QString::fromStdString(msg.transaction_id.value());
|
||||
if (!this->dvList.contains(txnid)) {
|
||||
if (auto flow = DeviceVerificationFlow::NewToDeviceVerification(
|
||||
this, msg, QString::fromStdString(sender), txnid)) {
|
||||
dvList[txnid] = flow;
|
||||
emit newDeviceVerificationRequest(flow.data());
|
||||
}
|
||||
}
|
||||
});
|
||||
connect(dynamic_cast<ChatPage *>(parent),
|
||||
&ChatPage::receivedDeviceVerificationStart,
|
||||
this,
|
||||
[this](const mtx::events::msg::KeyVerificationStart &msg, std::string sender) {
|
||||
if (!msg.transaction_id)
|
||||
return;
|
||||
|
||||
auto txnid = QString::fromStdString(msg.transaction_id.value());
|
||||
if (!this->dvList.contains(txnid)) {
|
||||
if (auto flow = DeviceVerificationFlow::NewToDeviceVerification(
|
||||
this, msg, QString::fromStdString(sender), txnid)) {
|
||||
dvList[txnid] = flow;
|
||||
emit newDeviceVerificationRequest(flow.data());
|
||||
}
|
||||
}
|
||||
});
|
||||
connect(parent, &ChatPage::loggedOut, this, [this]() {
|
||||
isInitialSync_ = true;
|
||||
emit initialSyncChanged(true);
|
||||
@ -284,6 +380,58 @@ TimelineViewManager::openRoomSettings() const
|
||||
MainWindow::instance()->openRoomSettings(timeline_->roomId());
|
||||
}
|
||||
|
||||
void
|
||||
TimelineViewManager::verifyUser(QString userid)
|
||||
{
|
||||
auto joined_rooms = cache::joinedRooms();
|
||||
auto room_infos = cache::getRoomInfo(joined_rooms);
|
||||
|
||||
for (std::string room_id : joined_rooms) {
|
||||
if ((room_infos[QString::fromStdString(room_id)].member_count == 2) &&
|
||||
cache::isRoomEncrypted(room_id)) {
|
||||
auto room_members = cache::roomMembers(room_id);
|
||||
if (std::find(room_members.begin(),
|
||||
room_members.end(),
|
||||
(userid).toStdString()) != room_members.end()) {
|
||||
auto model = models.value(QString::fromStdString(room_id));
|
||||
auto flow = DeviceVerificationFlow::InitiateUserVerification(
|
||||
this, model.data(), userid);
|
||||
connect(model.data(),
|
||||
&TimelineModel::updateFlowEventId,
|
||||
this,
|
||||
[this, flow](std::string eventId) {
|
||||
dvList[QString::fromStdString(eventId)] = flow;
|
||||
});
|
||||
emit newDeviceVerificationRequest(flow.data());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit ChatPage::instance()->showNotification(
|
||||
tr("No share room with this user found. Create an "
|
||||
"encrypted room with this user and try again."));
|
||||
}
|
||||
|
||||
void
|
||||
TimelineViewManager::removeVerificationFlow(DeviceVerificationFlow *flow)
|
||||
{
|
||||
for (auto it = dvList.keyValueBegin(); it != dvList.keyValueEnd(); ++it) {
|
||||
if ((*it).second == flow) {
|
||||
dvList.remove((*it).first);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
TimelineViewManager::verifyDevice(QString userid, QString deviceid)
|
||||
{
|
||||
auto flow = DeviceVerificationFlow::InitiateDeviceVerification(this, userid, deviceid);
|
||||
this->dvList[flow->transactionId()] = flow;
|
||||
emit newDeviceVerificationRequest(flow.data());
|
||||
}
|
||||
|
||||
void
|
||||
TimelineViewManager::updateReadReceipts(const QString &room_id,
|
||||
const std::vector<QString> &event_ids)
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
#include "Cache.h"
|
||||
#include "CallManager.h"
|
||||
#include "DeviceVerificationFlow.h"
|
||||
#include "Logging.h"
|
||||
#include "TimelineModel.h"
|
||||
#include "Utils.h"
|
||||
@ -71,6 +72,10 @@ public:
|
||||
Q_INVOKABLE void openMemberListDialog() const;
|
||||
Q_INVOKABLE void openLeaveRoomDialog() const;
|
||||
Q_INVOKABLE void openRoomSettings() const;
|
||||
Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow);
|
||||
|
||||
void verifyUser(QString userid);
|
||||
void verifyDevice(QString userid, QString deviceid);
|
||||
|
||||
signals:
|
||||
void clearRoomMessageCount(QString roomid);
|
||||
@ -79,6 +84,7 @@ signals:
|
||||
void initialSyncChanged(bool isInitialSync);
|
||||
void replyingEventChanged(QString replyingEvent);
|
||||
void replyClosed();
|
||||
void newDeviceVerificationRequest(DeviceVerificationFlow *flow);
|
||||
void inviteUsers(QStringList users);
|
||||
void showRoomList();
|
||||
void narrowViewChanged();
|
||||
@ -172,4 +178,14 @@ private:
|
||||
|
||||
QSharedPointer<UserSettings> settings;
|
||||
QHash<QString, QColor> userColors;
|
||||
|
||||
QHash<QString, QSharedPointer<DeviceVerificationFlow>> dvList;
|
||||
};
|
||||
Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationAccept)
|
||||
Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationCancel)
|
||||
Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationDone)
|
||||
Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationKey)
|
||||
Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationMac)
|
||||
Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationReady)
|
||||
Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationRequest)
|
||||
Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationStart)
|
||||
|
227
src/ui/UserProfile.cpp
Normal file
227
src/ui/UserProfile.cpp
Normal file
@ -0,0 +1,227 @@
|
||||
#include "UserProfile.h"
|
||||
#include "Cache_p.h"
|
||||
#include "ChatPage.h"
|
||||
#include "DeviceVerificationFlow.h"
|
||||
#include "Logging.h"
|
||||
#include "Utils.h"
|
||||
#include "mtx/responses/crypto.hpp"
|
||||
#include "timeline/TimelineModel.h"
|
||||
#include "timeline/TimelineViewManager.h"
|
||||
|
||||
UserProfile::UserProfile(QString roomid,
|
||||
QString userid,
|
||||
TimelineViewManager *manager_,
|
||||
TimelineModel *parent)
|
||||
: QObject(parent)
|
||||
, roomid_(roomid)
|
||||
, userid_(userid)
|
||||
, manager(manager_)
|
||||
, model(parent)
|
||||
{
|
||||
fetchDeviceList(this->userid_);
|
||||
|
||||
connect(cache::client(),
|
||||
&Cache::verificationStatusChanged,
|
||||
this,
|
||||
[this](const std::string &user_id) {
|
||||
if (user_id != this->userid_.toStdString())
|
||||
return;
|
||||
|
||||
auto status = cache::verificationStatus(user_id);
|
||||
if (!status)
|
||||
return;
|
||||
this->isUserVerified = status->user_verified;
|
||||
emit userStatusChanged();
|
||||
|
||||
for (auto &deviceInfo : deviceList_.deviceList_) {
|
||||
deviceInfo.verification_status =
|
||||
std::find(status->verified_devices.begin(),
|
||||
status->verified_devices.end(),
|
||||
deviceInfo.device_id.toStdString()) ==
|
||||
status->verified_devices.end()
|
||||
? verification::UNVERIFIED
|
||||
: verification::VERIFIED;
|
||||
}
|
||||
deviceList_.reset(deviceList_.deviceList_);
|
||||
});
|
||||
}
|
||||
|
||||
QHash<int, QByteArray>
|
||||
DeviceInfoModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{DeviceId, "deviceId"},
|
||||
{DeviceName, "deviceName"},
|
||||
{VerificationStatus, "verificationStatus"},
|
||||
};
|
||||
}
|
||||
|
||||
QVariant
|
||||
DeviceInfoModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() >= (int)deviceList_.size() || index.row() < 0)
|
||||
return {};
|
||||
|
||||
switch (role) {
|
||||
case DeviceId:
|
||||
return deviceList_[index.row()].device_id;
|
||||
case DeviceName:
|
||||
return deviceList_[index.row()].display_name;
|
||||
case VerificationStatus:
|
||||
return QVariant::fromValue(deviceList_[index.row()].verification_status);
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
DeviceInfoModel::reset(const std::vector<DeviceInfo> &deviceList)
|
||||
{
|
||||
beginResetModel();
|
||||
this->deviceList_ = std::move(deviceList);
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
DeviceInfoModel *
|
||||
UserProfile::deviceList()
|
||||
{
|
||||
return &this->deviceList_;
|
||||
}
|
||||
|
||||
QString
|
||||
UserProfile::userid()
|
||||
{
|
||||
return this->userid_;
|
||||
}
|
||||
|
||||
QString
|
||||
UserProfile::displayName()
|
||||
{
|
||||
return cache::displayName(roomid_, userid_);
|
||||
}
|
||||
|
||||
QString
|
||||
UserProfile::avatarUrl()
|
||||
{
|
||||
return cache::avatarUrl(roomid_, userid_);
|
||||
}
|
||||
|
||||
bool
|
||||
UserProfile::getUserStatus()
|
||||
{
|
||||
return isUserVerified;
|
||||
}
|
||||
|
||||
void
|
||||
UserProfile::fetchDeviceList(const QString &userID)
|
||||
{
|
||||
auto localUser = utils::localUser();
|
||||
|
||||
ChatPage::instance()->query_keys(
|
||||
userID.toStdString(),
|
||||
[other_user_id = userID.toStdString(), this](const UserKeyCache &other_user_keys,
|
||||
mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
nhlog::net()->warn("failed to query device keys: {},{}",
|
||||
err->matrix_error.errcode,
|
||||
static_cast<int>(err->status_code));
|
||||
return;
|
||||
}
|
||||
|
||||
// Finding if the User is Verified or not based on the Signatures
|
||||
ChatPage::instance()->query_keys(
|
||||
utils::localUser().toStdString(),
|
||||
[other_user_id, other_user_keys, this](const UserKeyCache &res,
|
||||
mtx::http::RequestErr err) {
|
||||
using namespace mtx;
|
||||
std::string local_user_id = utils::localUser().toStdString();
|
||||
|
||||
if (err) {
|
||||
nhlog::net()->warn("failed to query device keys: {},{}",
|
||||
err->matrix_error.errcode,
|
||||
static_cast<int>(err->status_code));
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.device_keys.empty()) {
|
||||
nhlog::net()->warn("no devices retrieved {}", local_user_id);
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<DeviceInfo> deviceInfo;
|
||||
auto devices = other_user_keys.device_keys;
|
||||
auto verificationStatus =
|
||||
cache::client()->verificationStatus(other_user_id);
|
||||
|
||||
isUserVerified = verificationStatus.user_verified;
|
||||
emit userStatusChanged();
|
||||
|
||||
for (const auto &d : devices) {
|
||||
auto device = d.second;
|
||||
verification::Status verified =
|
||||
verification::Status::UNVERIFIED;
|
||||
|
||||
if (std::find(verificationStatus.verified_devices.begin(),
|
||||
verificationStatus.verified_devices.end(),
|
||||
device.device_id) !=
|
||||
verificationStatus.verified_devices.end() &&
|
||||
mtx::crypto::verify_identity_signature(
|
||||
device,
|
||||
DeviceId(device.device_id),
|
||||
UserId(other_user_id)))
|
||||
verified = verification::Status::VERIFIED;
|
||||
|
||||
deviceInfo.push_back(
|
||||
{QString::fromStdString(d.first),
|
||||
QString::fromStdString(
|
||||
device.unsigned_info.device_display_name),
|
||||
verified});
|
||||
}
|
||||
|
||||
this->deviceList_.queueReset(std::move(deviceInfo));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
UserProfile::banUser()
|
||||
{
|
||||
ChatPage::instance()->banUser(this->userid_, "");
|
||||
}
|
||||
|
||||
// void ignoreUser(){
|
||||
|
||||
// }
|
||||
|
||||
void
|
||||
UserProfile::kickUser()
|
||||
{
|
||||
ChatPage::instance()->kickUser(this->userid_, "");
|
||||
}
|
||||
|
||||
void
|
||||
UserProfile::startChat()
|
||||
{
|
||||
mtx::requests::CreateRoom req;
|
||||
req.preset = mtx::requests::Preset::PrivateChat;
|
||||
req.visibility = mtx::requests::Visibility::Private;
|
||||
if (utils::localUser() != this->userid_)
|
||||
req.invite = {this->userid_.toStdString()};
|
||||
emit ChatPage::instance()->createRoom(req);
|
||||
}
|
||||
|
||||
void
|
||||
UserProfile::verify(QString device)
|
||||
{
|
||||
if (!device.isEmpty())
|
||||
manager->verifyDevice(userid_, device);
|
||||
else {
|
||||
manager->verifyUser(userid_);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
UserProfile::unverify(QString device)
|
||||
{
|
||||
cache::markDeviceUnverified(userid_.toStdString(), device.toStdString());
|
||||
}
|
123
src/ui/UserProfile.h
Normal file
123
src/ui/UserProfile.h
Normal file
@ -0,0 +1,123 @@
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
#include "MatrixClient.h"
|
||||
|
||||
namespace verification {
|
||||
Q_NAMESPACE
|
||||
|
||||
enum Status
|
||||
{
|
||||
VERIFIED,
|
||||
UNVERIFIED,
|
||||
BLOCKED
|
||||
};
|
||||
Q_ENUM_NS(Status)
|
||||
}
|
||||
|
||||
class DeviceVerificationFlow;
|
||||
class TimelineModel;
|
||||
class TimelineViewManager;
|
||||
|
||||
class DeviceInfo
|
||||
{
|
||||
public:
|
||||
DeviceInfo(const QString deviceID,
|
||||
const QString displayName,
|
||||
verification::Status verification_status_)
|
||||
: device_id(deviceID)
|
||||
, display_name(displayName)
|
||||
, verification_status(verification_status_)
|
||||
{}
|
||||
DeviceInfo()
|
||||
: verification_status(verification::UNVERIFIED)
|
||||
{}
|
||||
|
||||
QString device_id;
|
||||
QString display_name;
|
||||
|
||||
verification::Status verification_status;
|
||||
};
|
||||
|
||||
class DeviceInfoModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum Roles
|
||||
{
|
||||
DeviceId,
|
||||
DeviceName,
|
||||
VerificationStatus,
|
||||
};
|
||||
|
||||
explicit DeviceInfoModel(QObject *parent = nullptr)
|
||||
{
|
||||
(void)parent;
|
||||
connect(this, &DeviceInfoModel::queueReset, this, &DeviceInfoModel::reset);
|
||||
};
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override
|
||||
{
|
||||
(void)parent;
|
||||
return (int)deviceList_.size();
|
||||
}
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
signals:
|
||||
void queueReset(const std::vector<DeviceInfo> &deviceList);
|
||||
public slots:
|
||||
void reset(const std::vector<DeviceInfo> &deviceList);
|
||||
|
||||
private:
|
||||
std::vector<DeviceInfo> deviceList_;
|
||||
|
||||
friend class UserProfile;
|
||||
};
|
||||
|
||||
class UserProfile : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QString displayName READ displayName CONSTANT)
|
||||
Q_PROPERTY(QString userid READ userid CONSTANT)
|
||||
Q_PROPERTY(QString avatarUrl READ avatarUrl CONSTANT)
|
||||
Q_PROPERTY(DeviceInfoModel *deviceList READ deviceList CONSTANT)
|
||||
Q_PROPERTY(bool isUserVerified READ getUserStatus NOTIFY userStatusChanged)
|
||||
public:
|
||||
UserProfile(QString roomid,
|
||||
QString userid,
|
||||
TimelineViewManager *manager_,
|
||||
TimelineModel *parent = nullptr);
|
||||
|
||||
DeviceInfoModel *deviceList();
|
||||
|
||||
QString userid();
|
||||
QString displayName();
|
||||
QString avatarUrl();
|
||||
bool getUserStatus();
|
||||
|
||||
Q_INVOKABLE void verify(QString device = "");
|
||||
Q_INVOKABLE void unverify(QString device = "");
|
||||
Q_INVOKABLE void fetchDeviceList(const QString &userID);
|
||||
Q_INVOKABLE void banUser();
|
||||
// Q_INVOKABLE void ignoreUser();
|
||||
Q_INVOKABLE void kickUser();
|
||||
Q_INVOKABLE void startChat();
|
||||
|
||||
signals:
|
||||
void userStatusChanged();
|
||||
|
||||
private:
|
||||
QString roomid_, userid_;
|
||||
DeviceInfoModel deviceList_;
|
||||
bool isUserVerified = false;
|
||||
TimelineViewManager *manager;
|
||||
TimelineModel *model;
|
||||
|
||||
void callback_fn(const mtx::responses::QueryKeys &res,
|
||||
mtx::http::RequestErr err,
|
||||
std::string user_id);
|
||||
};
|
Loading…
Reference in New Issue
Block a user