Merge pull request #270 from Chethan2k1/device-verification

Device verification and Cross-Signing
master
DeepBlueV7.X 4 years ago committed by GitHub
commit 517a126a44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 32
      .travis.yml
  2. 12
      CMakeLists.txt
  3. 2
      README.md
  4. 6
      io.github.NhekoReborn.Nheko.json
  5. 18
      resources/qml/ActiveCallBar.qml
  6. 12
      resources/qml/Avatar.qml
  7. 8
      resources/qml/MatrixText.qml
  8. 11
      resources/qml/Reactions.qml
  9. 13
      resources/qml/TimelineRow.qml
  10. 75
      resources/qml/TimelineView.qml
  11. 172
      resources/qml/UserProfile.qml
  12. 4
      resources/qml/delegates/FileMessage.qml
  13. 2
      resources/qml/delegates/ImageMessage.qml
  14. 68
      resources/qml/delegates/MessageDelegate.qml
  15. 4
      resources/qml/delegates/PlayableMediaMessage.qml
  16. 6
      resources/qml/delegates/Reply.qml
  17. 4
      resources/qml/delegates/TextMessage.qml
  18. 39
      resources/qml/device-verification/AwaitingVerificationConfirmation.qml
  19. 97
      resources/qml/device-verification/DeviceVerification.qml
  20. 60
      resources/qml/device-verification/DigitVerification.qml
  21. 26
      resources/qml/device-verification/EmojiElement.qml
  22. 140
      resources/qml/device-verification/EmojiVerification.qml
  23. 44
      resources/qml/device-verification/Failed.qml
  24. 44
      resources/qml/device-verification/NewVerificationRequest.qml
  25. 31
      resources/qml/device-verification/Success.qml
  26. 45
      resources/qml/device-verification/Waiting.qml
  27. 66
      resources/qml/device-verification/sas-emoji.json
  28. 4
      resources/qml/emoji/EmojiPicker.qml
  29. 8
      resources/res.qrc
  30. 446
      src/Cache.cpp
  31. 16
      src/Cache.h
  32. 50
      src/CacheCryptoStructs.h
  33. 34
      src/Cache_p.h
  34. 45
      src/ChatPage.cpp
  35. 24
      src/ChatPage.h
  36. 794
      src/DeviceVerificationFlow.cpp
  37. 235
      src/DeviceVerificationFlow.h
  38. 22
      src/EventAccessors.cpp
  39. 9
      src/MainWindow.cpp
  40. 2
      src/MainWindow.h
  41. 211
      src/Olm.cpp
  42. 10
      src/Olm.h
  43. 2
      src/TextInputWidget.cpp
  44. 1
      src/TextInputWidget.h
  45. 1
      src/WebRTCSession.cpp
  46. 1
      src/dialogs/AcceptCall.cpp
  47. 1
      src/dialogs/AcceptCall.h
  48. 1
      src/dialogs/PlaceCall.cpp
  49. 1
      src/dialogs/PlaceCall.h
  50. 1
      src/emoji/EmojiModel.h
  51. 1
      src/emoji/EmojiSearchModel.h
  52. 170
      src/timeline/EventStore.cpp
  53. 4
      src/timeline/EventStore.h
  54. 335
      src/timeline/TimelineModel.cpp
  55. 31
      src/timeline/TimelineModel.h
  56. 156
      src/timeline/TimelineViewManager.cpp
  57. 16
      src/timeline/TimelineViewManager.h
  58. 227
      src/ui/UserProfile.cpp
  59. 123
      src/ui/UserProfile.h

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

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

@ -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); }
}
]
}
}

@ -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();
}
}
}
}

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

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

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

@ -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();
}
}
}
}

@ -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();
}
}
}
}

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

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

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

@ -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();

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

@ -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();

@ -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()) {
nhlog::crypto()->debug("decrypted olm payload: {}", payload.dump(2));
create_inbound_megolm_session(msg.sender, msg.sender_key, payload);
return;
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;
}
}
// Not a PRE_KEY message
if (cipher.second.type != 0) {
// TODO: log that it should have matched something
return;
if (!payload.is_null()) {
std::string msg_type = payload["type"];
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,39 +344,54 @@ 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;
MegolmSessionIndex index;
index.room_id = roomKey.content.room_id;
index.session_id = roomKey.content.session_id;
index.sender_key = sender_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));
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());
return;
} catch (const mtx::crypto::olm_exception &e) {
nhlog::crypto()->critical("failed to create inbound megolm session: {}", e.what());
return;
}
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 = room_id;
index.session_id = session_id;
index.sender_key = sender_key;
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()->init_inbound_group_session(session_key);
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 create inbound megolm session: {}", e.what());
nhlog::crypto()->critical("failed to import inbound megolm session: {}", e.what());
return;
}
nhlog::crypto()->info("established inbound megolm session ({}, {})", room_id, sender);
// 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) {

@ -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();
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!"));
}
});
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!"));
}
});
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;
}
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;
mtx::requests::ClaimKeys claim_keys;
room_key_msgs.clear();
deviceKeys.clear();
// 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) {
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);
}
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);
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("{}", d.first);
nhlog::net()->info(" curve25519 {}",
deviceKeys.at(d.first).curve25519);
nhlog::net()->info(" ed25519 {}",
deviceKeys.at(d.first).ed25519);
nhlog::net()->info("{}", device_id.get());
nhlog::net()->info(" curve25519 {}", pks.curve25519);
nhlog::net()->info(" ed25519 {}", pks.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;
}
// Payload with all the to_device message to be sent.
nlohmann::json body;
auto retrieved_devices = res.one_time_keys.at(user_id);
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;
}
// Payload with all the to_device message to be sent.
json body;
body["messages"][user_id] = json::object();
for (const auto &rd : retrieved_devices) {
const auto device_id = rd.first;
for (const auto &rd : retrieved_devices) {
const auto device_id = rd.first;
nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2));
nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2));
// TODO: Verify signatures
auto otk = rd.second.begin()->at("key");
if (rd.second.empty() || !rd.second.begin()->contains("key")) {
nhlog::net()->warn("Skipping device {} as it has no key.",
device_id);
continue;
}
if (pks.find(device_id) == pks.end()) {
nhlog::net()->critical("couldn't find public key for device: {}",
device_id);
continue;
}
// TODO: Verify signatures
auto otk = rd.second.begin()->at("key");
auto id_key = pks.at(device_id).curve25519;
auto s = olm::client()->create_outbound_session(id_key, otk);
auto id_key = pks.at(user_id).at(device_id).curve25519;
auto s = olm::client()->create_outbound_session(id_key, otk);
if (room_keys.find(device_id) == room_keys.end()) {
nhlog::net()->critical("couldn't find m.room_key for device: {}",
device_id);
continue;
}
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);
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());
}
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;
}
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)

@ -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());
}

@ -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…
Cancel
Save