Skip to content

Commit

Permalink
Implement local-initiated verification flow
Browse files Browse the repository at this point in the history
  • Loading branch information
KitsuneRal committed Mar 2, 2025
1 parent 5400426 commit d0063e1
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 34 deletions.
1 change: 1 addition & 0 deletions client/models/messageeventmodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,7 @@ QString MessageEventModel::visualiseEvent(const Quotient::RoomEvent& evt, bool a
[](const RoomTombstoneEvent& e) {
return tr("upgraded the room: %1").arg(e.serverMessage().toHtmlEscaped());
},
[](const EncryptedEvent&) { return tr("Undecryptable event"); },
[](const StateEvent& e) {
// A small hack for state events from TWIM bot
return e.stateKey() == "twim"
Expand Down
120 changes: 91 additions & 29 deletions client/profiledialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,8 @@ class ProfileDialog::DeviceTable : public QTableWidget {
return item;
}

void markupRow(int row, void (QFont::*fontFn)(bool), const QString& rowToolTip = {},
bool flagValue = true);

void markCurrentDevice(int row) {
markupRow(row, &QFont::setBold, tr("This is the current device"));
}
void markupRow(int row, void (QFont::*fontFn)(bool), bool flagValue = true);
void markCurrentDevice(int row) { markupRow(row, &QFont::setBold); }

void fillPendingData(const QString& currentDeviceId);
void refresh(const QVector<Quotient::Device>& devices, ProfileDialog* profileDialog);
Expand Down Expand Up @@ -157,9 +153,9 @@ void updateAvatarButton(Quotient::User* user, QPushButton* btn)
}
}

ProfileDialog::ProfileDialog(Quotient::AccountRegistry* accounts,
MainWindow* parent)
: Dialog(tr("User profiles"), parent)
ProfileDialog::ProfileDialog(Quotient::AccountRegistry* accounts, MainWindow* parent)
: Dialog(tr("User profiles"), QDialogButtonBox::Reset | QDialogButtonBox::Close, parent,
Dialog::StatusLine)
, m_settings("UI/ProfileDialog")
, m_avatar(new QPushButton)
, m_accountSelector(new AccountSelector(accounts))
Expand Down Expand Up @@ -202,7 +198,8 @@ ProfileDialog::ProfileDialog(Quotient::AccountRegistry* accounts,
m_deviceTable = new DeviceTable();
addWidget(m_deviceTable);

button(QDialogButtonBox::Ok)->setText(tr("Apply and close"));
// TODO: connect the title change to any changes in the dialog data
// button(QDialogButtonBox::Close)->setText(tr("Apply and close"));

if (m_settings.contains("normal_geometry"))
setGeometry(m_settings.value("normal_geometry").toRect());
Expand Down Expand Up @@ -236,27 +233,44 @@ void ProfileDialog::setVerifiedItem(int row, const QString& deviceId)
} else {
auto* verifyAction =
new QAction(QIcon::fromTheme(u"security-medium"_s), tr("Verify..."), this);
connect(verifyAction, &QAction::triggered, this, [this, deviceId] {
// auto verificationDialog = new VerificationDialog(account(), deviceId, this);
// TODO: connect accepted/rejected signals
// verificationDialog->show();
using KVSession = Quotient::KeyVerificationSession;
connect(verifyAction, &QAction::triggered, this, [this, deviceId, verifyAction] {
if (auto session = verifyAction->data().value<KVSession*>()) {
if (session->state() != KVSession::CANCELED)
session->cancelVerification(KVSession::USER);
} else
initiateVerification(deviceId, verifyAction);
});

auto* verifyButton = new QToolButton();
verifyButton->setToolButtonStyle(Qt::ToolButtonFollowStyle);
verifyButton->setAutoRaise(true);
verifyButton->setDefaultAction(verifyAction);
verifyButton->setEnabled(m_currentAccount->encryptionEnabled());
m_deviceTable->setCellWidget(clamp<int>(row, 0), DeviceTable::Verified, verifyButton);
}
}

void ProfileDialog::DeviceTable::markupRow(int row, void (QFont::*fontFn)(bool),
const QString& rowToolTip,
bool flagValue)
void ProfileDialog::refreshDevices()
{
m_devicesJob = m_currentAccount->callApi<Quotient::GetDevicesJob>();
connect(m_devicesJob, &BaseJob::success, m_deviceTable, [this] {
m_devices = m_devicesJob->devices();
m_deviceTable->refresh(m_devices, this);
if (m_settings.contains("device_table_state"))
m_deviceTable->horizontalHeader()->restoreState(
m_settings.value("device_table_state").toByteArray());
else
m_deviceTable->sortByColumn(DeviceTable::LastTimeSeen,
Qt::DescendingOrder);
});
}

void ProfileDialog::DeviceTable::markupRow(int row, void (QFont::*fontFn)(bool), bool flagValue)
{
Q_ASSERT(row < rowCount());
for (int c = 0; c < columnCount(); ++c)
if (auto* it = item(row, c)) {
it->setToolTip(rowToolTip);
auto font = it->font();
(font.*fontFn)(flagValue);
it->setFont(font);
Expand Down Expand Up @@ -336,17 +350,7 @@ void ProfileDialog::load()
if (!m_settings.contains("device_table_state"))
m_deviceTable->resizeColumnsToContents();

m_devicesJob = m_currentAccount->callApi<Quotient::GetDevicesJob>();
connect(m_devicesJob, &BaseJob::success, m_deviceTable, [this] {
m_devices = m_devicesJob->devices();
m_deviceTable->refresh(m_devices, this);
if (m_settings.contains("device_table_state"))
m_deviceTable->horizontalHeader()->restoreState(
m_settings.value("device_table_state").toByteArray());
else
m_deviceTable->sortByColumn(DeviceTable::LastTimeSeen,
Qt::DescendingOrder);
});
refreshDevices();
}

void ProfileDialog::apply()
Expand Down Expand Up @@ -396,3 +400,61 @@ void ProfileDialog::uploadAvatar()
}
});
}

inline QString errorToMessage(Quotient::KeyVerificationSession::Error e)
{
switch (e) {
using enum Quotient::KeyVerificationSession::Error;
case TIMEOUT:
case REMOTE_TIMEOUT: return ProfileDialog::tr("Verification timed out");
case USER: return ProfileDialog::tr("Verification was cancelled");
case REMOTE_USER: return ProfileDialog::tr("Verification was cancelled on the other device");
case MISMATCHED_SAS:
case REMOTE_MISMATCHED_SAS:
return ProfileDialog::tr("Verification failed: emojis did not match");
default: return ProfileDialog::tr("Verification did not succeed");
}
}

Quotient::KeyVerificationSession* ProfileDialog::initiateVerification(const QString& deviceId,
QAction* verifyAction)
{
using namespace Quotient;
auto* session = account()->startKeyVerificationSession(account()->userId(), deviceId);
verifyAction->setData(QVariant::fromValue(session));
verifyAction->setText(tr("Cancel"));
setStatusMessage(tr("Please accept the verification request on the device you want to verify"));
connect(session, &KeyVerificationSession::finished, this, [this, session, verifyAction] {
if (session->state() == KeyVerificationSession::DONE)
refreshDevices();
else {
setStatusMessage(errorToMessage(session->error()));
verifyAction->setText(tr("Verify..."));
verifyAction->setData(QVariant::fromValue(nullptr));
}
});
// TODO: when the library supports other methods, ask to choose instead of opting
// for SAS straight away
QtFuture::connect(session, &KeyVerificationSession::stateChanged).then([this, session] {
using enum KeyVerificationSession::State;
if (auto s = session->state(); s == READY) {
setStatusMessage({});
session->sendStartSas();
} else if (s != WAITINGFORACCEPT && s != ACCEPTED && s != CANCELED && s != DONE) {
qCritical(MAIN) << "Unexpected state of key verification session:" << terse << s;
session->cancelVerification(KeyVerificationSession::UNEXPECTED_MESSAGE);
}
});
QtFuture::connect(session, &KeyVerificationSession::sasEmojisChanged)
.then([this, session] {
QUO_ALARM_X(session->sasEmojis().empty(),
"Empty SAS emoji sequence, the session seems to be broken");
auto dialog = new VerificationDialog(session, this);
dialog->setModal(true);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->show();
});
connect(this, &QDialog::finished, session,
[session] { session->cancelVerification(KeyVerificationSession::USER); });
return session;
}
4 changes: 4 additions & 0 deletions client/profiledialog.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ namespace Quotient {
class AccountRegistry;
class GetDevicesJob;
class Connection;
class KeyVerificationSession;
}

class ProfileDialog : public Dialog
Expand All @@ -42,6 +43,8 @@ private slots:
void load() override;
void apply() override;
void uploadAvatar();
Quotient::KeyVerificationSession* initiateVerification(const QString& deviceId,
QAction* verifyAction);

private:
Quotient::SettingsGroup m_settings;
Expand All @@ -59,4 +62,5 @@ private slots:
QVector<Quotient::Device> m_devices;

void setVerifiedItem(int row, const QString& deviceId);
void refreshDevices();
};
62 changes: 58 additions & 4 deletions client/verificationdialog.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,64 @@
#include "verificationdialog.h"

VerificationDialog::VerificationDialog(Quotient::Connection* account, const QString& deviceId,
QWidget* parent)
: Dialog(tr("Device verification"), QDialogButtonBox::NoButton, parent)
#include <Quotient/keyverificationsession.h>

#include <QtWidgets/QLabel>
#include <QtWidgets/QPushButton>

#include <ranges>

using namespace Qt::StringLiterals;

VerificationDialog::VerificationDialog(Session* session, QWidget* parent)
: Dialog(tr("Verifying device %1").arg(session->remoteDeviceId()),
QDialogButtonBox::Ok | QDialogButtonBox::Discard, parent)
, session(session)
{
//
// The same check as in Session::handleEvent() in the KeyVerificationKeyEvent branch
QUO_CHECK(session->state() == Session::WAITINGFORKEY || session->state() == Session::ACCEPTED);

addWidget(new QLabel(
tr("Confirm that the same emoji, in the same order are displayed on the other side")));
const auto emojis = session->sasEmojis();
constexpr auto rowsCount = 2;
const auto rowSize = (emojis.size() + 1) / rowsCount;
auto emojiGrid = addLayout<QHBoxLayout>();
for (auto [i, emojiData] : std::ranges::enumerate_view(emojis)) {
auto emojiLayout = new QVBoxLayout();
auto emoji = new QLabel(emojiData.emoji);
emoji->setFont({ u"emoji"_s, emoji->font().pointSize() * 4 });
for (auto* const l : { emoji, new QLabel(emojiData.description) }) {
emojiLayout->addWidget(l);
emojiLayout->setAlignment(l, Qt::AlignCenter);
}
emojiGrid->addLayout(emojiLayout);
if (i % rowSize == rowSize - 1)
emojiGrid = addLayout<QHBoxLayout>(); // Start new line
}
button(QDialogButtonBox::Ok)->setText(tr("They match"));
button(QDialogButtonBox::Discard)->setText(tr("They DON'T match"));

// Pin lifecycles of the dialog and the session, avoiding recursion (by the time
// QObject::destroyed is emitted, KeyVerificationSession signals are disconnected)
connect(session, &QObject::destroyed, this, &QDialog::reject);
// NB: this is only triggered when a dialog is closed using a window close button;
// QDialogButtonBox::Discard doesn't trigger QDialog::rejected as it has DestructiveRole
connect(this, &QDialog::rejected, session, [session] {
if (session->state() != Session::CANCELED)
session->cancelVerification(Session::USER);
});
}

VerificationDialog::~VerificationDialog() = default;

void VerificationDialog::buttonClicked(QAbstractButton* button)
{
if (button == this->button(QDialogButtonBox::Ok)) {
session->sendMac();
accept();
} else if (button == this->button(QDialogButtonBox::Discard)) {
session->cancelVerification(Session::MISMATCHED_SAS);
reject();
} else
QUO_ALARM_X(false, "Unknown button: " % button->text());
}
14 changes: 13 additions & 1 deletion client/verificationdialog.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,21 @@

#include "dialog.h"

namespace Quotient {
class KeyVerificationSession;
}

class VerificationDialog : public Dialog {
Q_OBJECT
public:
VerificationDialog(Quotient::Connection* account, const QString& deviceId, QWidget* parent);
using Session = Quotient::KeyVerificationSession;

VerificationDialog(Session* session, QWidget* parent);
~VerificationDialog() override;

private: // Overrides
void buttonClicked(QAbstractButton* button) override;

private: // Data
Session* session;
};

0 comments on commit d0063e1

Please sign in to comment.