Add self verification after login

This commit is contained in:
Nicolas Werner 2021-10-30 00:22:47 +02:00
parent 7a9c69cbd0
commit 5688b2647e
No known key found for this signature in database
GPG key ID: C8D75E610773F2D9
9 changed files with 285 additions and 72 deletions

View file

@ -93,6 +93,7 @@ Item {
columns: 2 columns: 2
rowSpacing: 0 rowSpacing: 0
columnSpacing: 0 columnSpacing: 0
z: 1
Label { Label {
Layout.margins: Nheko.paddingMedium Layout.margins: Nheko.paddingMedium
@ -220,15 +221,53 @@ Item {
MainWindowDialog { MainWindowDialog {
id: verifyMasterKey id: verifyMasterKey
onAccepted: SelfVerificationStatus.verifyMasterKey() standardButtons: Dialog.Cancel
GridLayout { GridLayout {
id: masterGrid id: masterGrid
width: verifyMasterKey.useableWidth width: verifyMasterKey.useableWidth
columns: 2 columns: 1
rowSpacing: 0 z: 1
columnSpacing: 0
Label {
Layout.margins: Nheko.paddingMedium
Layout.alignment: Qt.AlignHCenter
//Layout.columnSpan: 2
font.pointSize: fontMetrics.font.pointSize * 2
text: qsTr("Activate Encryption")
color: Nheko.colors.text
wrapMode: Text.Wrap
}
Label {
Layout.margins: Nheko.paddingMedium
Layout.alignment: Qt.AlignLeft
//Layout.columnSpan: 2
Layout.maximumWidth: grid.width - Nheko.paddingMedium * 2
text: qsTr("It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.\nIf you choose verify, you need to have the other device available. If you choose \"enter passphrase\", you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.")
color: Nheko.colors.text
wrapMode: Text.Wrap
}
FlatButton {
Layout.alignment: Qt.AlignHCenter
text: qsTr("verify")
onClicked: {
console.log("AAAAA");
SelfVerificationStatus.verifyMasterKey();
verifyMasterKey.close();
}
}
FlatButton {
visible: SelfVerificationStatus.hasSSSS
Layout.alignment: Qt.AlignHCenter
text: qsTr("enter passphrase")
onClicked: {
SelfVerificationStatus.verifyMasterKeyWithPassphrase()
verifyMasterKey.close();
}
}
} }
} }
@ -237,8 +276,8 @@ Item {
console.log("STATUS CHANGED: " + SelfVerificationStatus.status); console.log("STATUS CHANGED: " + SelfVerificationStatus.status);
if (SelfVerificationStatus.status == SelfVerificationStatus.NoMasterKey) if (SelfVerificationStatus.status == SelfVerificationStatus.NoMasterKey)
bootstrapCrosssigning.open(); bootstrapCrosssigning.open();
// else if (SelfVerificationStatus.status == SelfVerificationStatus.UnverifiedMasterKey) else if (SelfVerificationStatus.status == SelfVerificationStatus.UnverifiedMasterKey)
// verifyMasterKey.open(); verifyMasterKey.open();
} }

View file

@ -1131,11 +1131,71 @@ ChatPage::decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescriptio
return; return;
} }
auto deviceKeys = cache::client()->userKeys(http::client()->user_id().to_string());
mtx::requests::KeySignaturesUpload req;
for (const auto &[secretName, encryptedSecret] : secrets) { for (const auto &[secretName, encryptedSecret] : secrets) {
auto decrypted = mtx::crypto::decrypt(encryptedSecret, *decryptionKey, secretName); auto decrypted = mtx::crypto::decrypt(encryptedSecret, *decryptionKey, secretName);
if (!decrypted.empty()) if (!decrypted.empty()) {
cache::storeSecret(secretName, decrypted); cache::storeSecret(secretName, decrypted);
if (deviceKeys &&
secretName == mtx::secret_storage::secrets::cross_signing_self_signing) {
auto myKey = deviceKeys->device_keys.at(http::client()->device_id());
if (myKey.user_id == http::client()->user_id().to_string() &&
myKey.device_id == http::client()->device_id() &&
myKey.keys["ed25519:" + http::client()->device_id()] ==
olm::client()->identity_keys().ed25519 &&
myKey.keys["curve25519:" + http::client()->device_id()] ==
olm::client()->identity_keys().curve25519) {
json j = myKey;
j.erase("signatures");
j.erase("unsigned");
auto ssk = mtx::crypto::PkSigning::from_seed(decrypted);
myKey.signatures[http::client()->user_id().to_string()]
["ed25519:" + ssk.public_key()] = ssk.sign(j.dump());
req.signatures[http::client()->user_id().to_string()]
[http::client()->device_id()] = myKey;
}
} else if (deviceKeys &&
secretName == mtx::secret_storage::secrets::cross_signing_master) {
auto mk = mtx::crypto::PkSigning::from_seed(decrypted);
if (deviceKeys->master_keys.user_id == http::client()->user_id().to_string() &&
deviceKeys->master_keys.keys["ed25519:" + mk.public_key()] == mk.public_key()) {
json j = deviceKeys->master_keys;
j.erase("signatures");
j.erase("unsigned");
mtx::crypto::CrossSigningKeys master_key = j;
master_key.signatures[http::client()->user_id().to_string()]
["ed25519:" + http::client()->device_id()] =
olm::client()->sign_message(j.dump());
req.signatures[http::client()->user_id().to_string()][mk.public_key()] =
master_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: {},{}",
mtx::errors::to_string(err->matrix_error.errcode),
static_cast<int>(err->status_code));
}
for (const auto &[user_id, tmp] : res.errors)
for (const auto &[key_id, e] : tmp)
nhlog::net()->error("signature error for user '{}' and key "
"id {}: {}, {}",
user_id,
key_id,
mtx::errors::to_string(e.errcode),
e.error);
});
} }
void void

View file

@ -32,12 +32,15 @@ DeviceVerificationFlow::DeviceVerificationFlow(QObject *,
DeviceVerificationFlow::Type flow_type, DeviceVerificationFlow::Type flow_type,
TimelineModel *model, TimelineModel *model,
QString userID, QString userID,
QString deviceId_) std::vector<QString> deviceIds_)
: sender(false) : sender(false)
, type(flow_type) , type(flow_type)
, deviceId(deviceId_) , deviceIds(std::move(deviceIds_))
, model_(model) , model_(model)
{ {
if (deviceIds.size() == 1)
deviceId = deviceIds.front();
timeout = new QTimer(this); timeout = new QTimer(this);
timeout->setSingleShot(true); timeout->setSingleShot(true);
this->sas = olm::client()->sas_init(); this->sas = olm::client()->sas_init();
@ -346,33 +349,62 @@ DeviceVerificationFlow::DeviceVerificationFlow(QObject *,
} }
}); });
connect(ChatPage::instance(), connect(
&ChatPage::receivedDeviceVerificationReady, ChatPage::instance(),
this, &ChatPage::receivedDeviceVerificationReady,
[this](const mtx::events::msg::KeyVerificationReady &msg) { this,
nhlog::crypto()->info("verification: received ready"); [this](const mtx::events::msg::KeyVerificationReady &msg) {
if (!sender) { nhlog::crypto()->info("verification: received ready");
if (msg.from_device != http::client()->device_id()) { if (!sender) {
error_ = User; if (msg.from_device != http::client()->device_id()) {
emit errorChanged(); error_ = User;
setState(Failed); emit errorChanged();
} setState(Failed);
}
return; return;
} }
if (msg.transaction_id.has_value()) { if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id) if (msg.transaction_id.value() != this->transaction_id)
return; return;
} else if (msg.relations.references()) {
if (msg.relations.references() != this->relation.event_id) if (this->deviceId.isEmpty() && this->deviceIds.size() > 1) {
return; auto from = QString::fromStdString(msg.from_device);
else { if (std::find(deviceIds.begin(), deviceIds.end(), from) != deviceIds.end()) {
this->deviceId = QString::fromStdString(msg.from_device); mtx::events::msg::KeyVerificationCancel req{};
} req.code = "m.user";
} req.reason = "accepted by other device";
this->startVerificationRequest(); req.transaction_id = this->transaction_id;
}); mtx::requests::ToDeviceMessages<mtx::events::msg::KeyVerificationCancel> body;
for (const auto &d : this->deviceIds) {
if (d != from)
body[this->toClient][d.toStdString()] = req;
}
http::client()->send_to_device(
http::client()->generate_txn_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));
});
this->deviceId = from;
this->deviceIds = {from};
}
}
} else if (msg.relations.references()) {
if (msg.relations.references() != this->relation.event_id)
return;
else {
this->deviceId = QString::fromStdString(msg.from_device);
}
}
this->startVerificationRequest();
});
connect(ChatPage::instance(), connect(ChatPage::instance(),
&ChatPage::receivedDeviceVerificationDone, &ChatPage::receivedDeviceVerificationDone,
@ -782,7 +814,7 @@ DeviceVerificationFlow::NewInRoomVerification(QObject *parent_,
Type::RoomMsg, Type::RoomMsg,
timelineModel_, timelineModel_,
other_user_, other_user_,
QString::fromStdString(msg.from_device))); {QString::fromStdString(msg.from_device)}));
flow->setEventId(event_id_.toStdString()); flow->setEventId(event_id_.toStdString());
@ -801,7 +833,7 @@ DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_,
QString txn_id_) QString txn_id_)
{ {
QSharedPointer<DeviceVerificationFlow> flow(new DeviceVerificationFlow( QSharedPointer<DeviceVerificationFlow> flow(new DeviceVerificationFlow(
parent_, Type::ToDevice, nullptr, other_user_, QString::fromStdString(msg.from_device))); parent_, Type::ToDevice, nullptr, other_user_, {QString::fromStdString(msg.from_device)}));
flow->transaction_id = txn_id_.toStdString(); flow->transaction_id = txn_id_.toStdString();
if (std::find(msg.methods.begin(), if (std::find(msg.methods.begin(),
@ -819,7 +851,7 @@ DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_,
QString txn_id_) QString txn_id_)
{ {
QSharedPointer<DeviceVerificationFlow> flow(new DeviceVerificationFlow( QSharedPointer<DeviceVerificationFlow> flow(new DeviceVerificationFlow(
parent_, Type::ToDevice, nullptr, other_user_, QString::fromStdString(msg.from_device))); parent_, Type::ToDevice, nullptr, other_user_, {QString::fromStdString(msg.from_device)}));
flow->transaction_id = txn_id_.toStdString(); flow->transaction_id = txn_id_.toStdString();
flow->handleStartMessage(msg, ""); flow->handleStartMessage(msg, "");
@ -832,15 +864,19 @@ DeviceVerificationFlow::InitiateUserVerification(QObject *parent_,
QString userid) QString userid)
{ {
QSharedPointer<DeviceVerificationFlow> flow( QSharedPointer<DeviceVerificationFlow> flow(
new DeviceVerificationFlow(parent_, Type::RoomMsg, timelineModel_, userid, "")); new DeviceVerificationFlow(parent_, Type::RoomMsg, timelineModel_, userid, {}));
flow->sender = true; flow->sender = true;
return flow; return flow;
} }
QSharedPointer<DeviceVerificationFlow> QSharedPointer<DeviceVerificationFlow>
DeviceVerificationFlow::InitiateDeviceVerification(QObject *parent_, QString userid, QString device) DeviceVerificationFlow::InitiateDeviceVerification(QObject *parent_,
QString userid,
std::vector<QString> devices)
{ {
assert(!devices.empty());
QSharedPointer<DeviceVerificationFlow> flow( QSharedPointer<DeviceVerificationFlow> flow(
new DeviceVerificationFlow(parent_, Type::ToDevice, nullptr, userid, device)); new DeviceVerificationFlow(parent_, Type::ToDevice, nullptr, userid, devices));
flow->sender = true; flow->sender = true;
flow->transaction_id = http::client()->generate_txn_id(); flow->transaction_id = http::client()->generate_txn_id();

View file

@ -120,9 +120,8 @@ public:
QString txn_id_); QString txn_id_);
static QSharedPointer<DeviceVerificationFlow> static QSharedPointer<DeviceVerificationFlow>
InitiateUserVerification(QObject *parent_, TimelineModel *timelineModel_, QString userid); InitiateUserVerification(QObject *parent_, TimelineModel *timelineModel_, QString userid);
static QSharedPointer<DeviceVerificationFlow> InitiateDeviceVerification(QObject *parent, static QSharedPointer<DeviceVerificationFlow>
QString userid, InitiateDeviceVerification(QObject *parent, QString userid, std::vector<QString> devices);
QString device);
// getters // getters
QString state(); QString state();
@ -161,7 +160,7 @@ private:
DeviceVerificationFlow::Type flow_type, DeviceVerificationFlow::Type flow_type,
TimelineModel *model, TimelineModel *model,
QString userID, QString userID,
QString deviceId_); std::vector<QString> deviceIds_);
void setState(State state) void setState(State state)
{ {
if (state != state_) { if (state != state_) {
@ -196,6 +195,7 @@ private:
Type type; Type type;
mtx::identifiers::User toClient; mtx::identifiers::User toClient;
QString deviceId; QString deviceId;
std::vector<QString> deviceIds;
// public part of our master key, when trusted or empty // public part of our master key, when trusted or empty
std::string our_trusted_master_key; std::string our_trusted_master_key;
@ -222,11 +222,12 @@ private:
{ {
if (this->type == DeviceVerificationFlow::Type::ToDevice) { if (this->type == DeviceVerificationFlow::Type::ToDevice) {
mtx::requests::ToDeviceMessages<T> body; mtx::requests::ToDeviceMessages<T> body;
msg.transaction_id = this->transaction_id; msg.transaction_id = this->transaction_id;
body[this->toClient][deviceId.toStdString()] = msg; for (const auto &d : deviceIds)
body[this->toClient][d.toStdString()] = msg;
http::client()->send_to_device<T>( http::client()->send_to_device<T>(
this->transaction_id, body, [](mtx::http::RequestErr err) { http::client()->generate_txn_id(), body, [](mtx::http::RequestErr err) {
if (err) if (err)
nhlog::net()->warn("failed to send verification to_device message: {} {}", nhlog::net()->warn("failed to send verification to_device message: {} {}",
err->matrix_error.error, err->matrix_error.error,

View file

@ -1540,6 +1540,7 @@ request_cross_signing_keys()
}); });
}; };
request(mtx::secret_storage::secrets::cross_signing_master);
request(mtx::secret_storage::secrets::cross_signing_self_signing); request(mtx::secret_storage::secrets::cross_signing_self_signing);
request(mtx::secret_storage::secrets::cross_signing_user_signing); request(mtx::secret_storage::secrets::cross_signing_user_signing);
request(mtx::secret_storage::secrets::megolm_backup_v1); request(mtx::secret_storage::secrets::megolm_backup_v1);
@ -1574,36 +1575,52 @@ download_cross_signing_keys()
backup_key = secret; backup_key = secret;
http::client()->secret_storage_secret( http::client()->secret_storage_secret(
secrets::cross_signing_self_signing, secrets::cross_signing_master, [backup_key](Secret secret, mtx::http::RequestErr err) {
[backup_key](Secret secret, mtx::http::RequestErr err) { std::optional<Secret> master_key;
std::optional<Secret> self_signing_key;
if (!err) if (!err)
self_signing_key = secret; master_key = secret;
http::client()->secret_storage_secret( http::client()->secret_storage_secret(
secrets::cross_signing_user_signing, secrets::cross_signing_self_signing,
[backup_key, self_signing_key](Secret secret, mtx::http::RequestErr err) { [backup_key, master_key](Secret secret, mtx::http::RequestErr err) {
std::optional<Secret> user_signing_key; std::optional<Secret> self_signing_key;
if (!err) if (!err)
user_signing_key = secret; self_signing_key = secret;
std::map<std::string, std::map<std::string, AesHmacSha2EncryptedData>> http::client()->secret_storage_secret(
secrets; secrets::cross_signing_user_signing,
[backup_key, self_signing_key, master_key](Secret secret,
mtx::http::RequestErr err) {
std::optional<Secret> user_signing_key;
if (!err)
user_signing_key = secret;
if (backup_key && !backup_key->encrypted.empty()) std::map<std::string, std::map<std::string, AesHmacSha2EncryptedData>>
secrets[backup_key->encrypted.begin()->first][secrets::megolm_backup_v1] = secrets;
backup_key->encrypted.begin()->second;
if (self_signing_key && !self_signing_key->encrypted.empty())
secrets[self_signing_key->encrypted.begin()->first]
[secrets::cross_signing_self_signing] =
self_signing_key->encrypted.begin()->second;
if (user_signing_key && !user_signing_key->encrypted.empty())
secrets[user_signing_key->encrypted.begin()->first]
[secrets::cross_signing_user_signing] =
user_signing_key->encrypted.begin()->second;
for (const auto &[key, secrets] : secrets) if (backup_key && !backup_key->encrypted.empty())
unlock_secrets(key, secrets); secrets[backup_key->encrypted.begin()->first]
[secrets::megolm_backup_v1] =
backup_key->encrypted.begin()->second;
if (master_key && !master_key->encrypted.empty())
secrets[master_key->encrypted.begin()->first]
[secrets::cross_signing_master] =
master_key->encrypted.begin()->second;
if (self_signing_key && !self_signing_key->encrypted.empty())
secrets[self_signing_key->encrypted.begin()->first]
[secrets::cross_signing_self_signing] =
self_signing_key->encrypted.begin()->second;
if (user_signing_key && !user_signing_key->encrypted.empty())
secrets[user_signing_key->encrypted.begin()->first]
[secrets::cross_signing_user_signing] =
user_signing_key->encrypted.begin()->second;
for (const auto &[key, secrets] : secrets)
unlock_secrets(key, secrets);
});
}); });
}); });
}); });

View file

@ -5,10 +5,12 @@
#include "SelfVerificationStatus.h" #include "SelfVerificationStatus.h"
#include "Cache_p.h" #include "Cache_p.h"
#include "ChatPage.h"
#include "Logging.h" #include "Logging.h"
#include "MainWindow.h" #include "MainWindow.h"
#include "MatrixClient.h" #include "MatrixClient.h"
#include "Olm.h" #include "Olm.h"
#include "timeline/TimelineViewManager.h"
#include "ui/UIA.h" #include "ui/UIA.h"
#include <mtx/responses/common.hpp> #include <mtx/responses/common.hpp>
@ -196,6 +198,35 @@ void
SelfVerificationStatus::verifyMasterKey() SelfVerificationStatus::verifyMasterKey()
{ {
nhlog::db()->info("Clicked verify master key"); nhlog::db()->info("Clicked verify master key");
const auto this_user = http::client()->user_id().to_string();
auto keys = cache::client()->userKeys(this_user);
const auto &sigs = keys->master_keys.signatures[this_user];
std::vector<QString> devices;
for (const auto &[dev, sig] : sigs) {
(void)sig;
auto d = QString::fromStdString(dev);
if (d.startsWith("ed25519:")) {
d.remove("ed25519:");
if (keys->device_keys.count(d.toStdString()))
devices.push_back(std::move(d));
}
}
if (!devices.empty())
ChatPage::instance()->timelineManager()->verificationManager()->verifyOneOfDevices(
QString::fromStdString(this_user), std::move(devices));
}
void
SelfVerificationStatus::verifyMasterKeyWithPassphrase()
{
nhlog::db()->info("Clicked verify master key with passphrase");
olm::download_cross_signing_keys();
} }
void void
@ -207,9 +238,15 @@ SelfVerificationStatus::verifyUnverifiedDevices()
void void
SelfVerificationStatus::invalidate() SelfVerificationStatus::invalidate()
{ {
using namespace mtx::secret_storage;
nhlog::db()->info("Invalidating self verification status"); nhlog::db()->info("Invalidating self verification status");
this->hasSSSS_ = false;
emit hasSSSSChanged();
auto keys = cache::client()->userKeys(http::client()->user_id().to_string()); auto keys = cache::client()->userKeys(http::client()->user_id().to_string());
if (!keys) { if (!keys || keys->device_keys.find(http::client()->device_id()) == keys->device_keys.end()) {
cache::client()->markUserKeysOutOfDate({http::client()->user_id().to_string()});
cache::client()->query_keys(http::client()->user_id().to_string(), cache::client()->query_keys(http::client()->user_id().to_string(),
[](const UserKeyCache &, mtx::http::RequestErr) {}); [](const UserKeyCache &, mtx::http::RequestErr) {});
return; return;
@ -223,6 +260,14 @@ SelfVerificationStatus::invalidate()
return; return;
} }
http::client()->secret_storage_secret(secrets::cross_signing_self_signing,
[this](Secret secret, mtx::http::RequestErr err) {
if (!err && !secret.encrypted.empty()) {
this->hasSSSS_ = true;
emit hasSSSSChanged();
}
});
auto verifStatus = cache::client()->verificationStatus(http::client()->user_id().to_string()); auto verifStatus = cache::client()->verificationStatus(http::client()->user_id().to_string());
if (!verifStatus.user_verified) { if (!verifStatus.user_verified) {

View file

@ -11,6 +11,7 @@ class SelfVerificationStatus : public QObject
Q_OBJECT Q_OBJECT
Q_PROPERTY(Status status READ status NOTIFY statusChanged) Q_PROPERTY(Status status READ status NOTIFY statusChanged)
Q_PROPERTY(bool hasSSSS READ hasSSSS NOTIFY hasSSSSChanged)
public: public:
SelfVerificationStatus(QObject *o = nullptr); SelfVerificationStatus(QObject *o = nullptr);
@ -25,12 +26,15 @@ public:
Q_INVOKABLE void setupCrosssigning(bool useSSSS, QString password, bool useOnlineKeyBackup); Q_INVOKABLE void setupCrosssigning(bool useSSSS, QString password, bool useOnlineKeyBackup);
Q_INVOKABLE void verifyMasterKey(); Q_INVOKABLE void verifyMasterKey();
Q_INVOKABLE void verifyMasterKeyWithPassphrase();
Q_INVOKABLE void verifyUnverifiedDevices(); Q_INVOKABLE void verifyUnverifiedDevices();
Status status() const { return status_; } Status status() const { return status_; }
bool hasSSSS() const { return hasSSSS_; }
signals: signals:
void statusChanged(); void statusChanged();
void hasSSSSChanged();
void setupCompleted(); void setupCompleted();
void showRecoveryKey(QString key); void showRecoveryKey(QString key);
void setupFailed(QString message); void setupFailed(QString message);
@ -40,4 +44,5 @@ public slots:
private: private:
Status status_ = AllVerified; Status status_ = AllVerified;
bool hasSSSS_ = true;
}; };

View file

@ -120,7 +120,16 @@ VerificationManager::removeVerificationFlow(DeviceVerificationFlow *flow)
void void
VerificationManager::verifyDevice(QString userid, QString deviceid) VerificationManager::verifyDevice(QString userid, QString deviceid)
{ {
auto flow = DeviceVerificationFlow::InitiateDeviceVerification(this, userid, deviceid); auto flow = DeviceVerificationFlow::InitiateDeviceVerification(this, userid, {deviceid});
this->dvList[flow->transactionId()] = flow;
emit newDeviceVerificationRequest(flow.data());
}
void
VerificationManager::verifyOneOfDevices(QString userid, std::vector<QString> deviceids)
{
auto flow =
DeviceVerificationFlow::InitiateDeviceVerification(this, userid, std::move(deviceids));
this->dvList[flow->transactionId()] = flow; this->dvList[flow->transactionId()] = flow;
emit newDeviceVerificationRequest(flow.data()); emit newDeviceVerificationRequest(flow.data());
} }

View file

@ -27,6 +27,7 @@ public:
Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow); Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow);
void verifyUser(QString userid); void verifyUser(QString userid);
void verifyDevice(QString userid, QString deviceid); void verifyDevice(QString userid, QString deviceid);
void verifyOneOfDevices(QString userid, std::vector<QString> deviceids);
signals: signals:
void newDeviceVerificationRequest(DeviceVerificationFlow *flow); void newDeviceVerificationRequest(DeviceVerificationFlow *flow);