forked from mirror/nheko
Merge pull request #270 from Chethan2k1/device-verification
Device verification and Cross-Signingmaster
commit
517a126a44
@ -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,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,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,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()); |
||||
} |
||||
}; |
@ -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…
Reference in new issue