mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-22 19:08:58 +03:00
Add self verification after login
This commit is contained in:
parent
7a9c69cbd0
commit
5688b2647e
9 changed files with 285 additions and 72 deletions
|
@ -93,6 +93,7 @@ Item {
|
|||
columns: 2
|
||||
rowSpacing: 0
|
||||
columnSpacing: 0
|
||||
z: 1
|
||||
|
||||
Label {
|
||||
Layout.margins: Nheko.paddingMedium
|
||||
|
@ -220,15 +221,53 @@ Item {
|
|||
MainWindowDialog {
|
||||
id: verifyMasterKey
|
||||
|
||||
onAccepted: SelfVerificationStatus.verifyMasterKey()
|
||||
standardButtons: Dialog.Cancel
|
||||
|
||||
GridLayout {
|
||||
id: masterGrid
|
||||
|
||||
width: verifyMasterKey.useableWidth
|
||||
columns: 2
|
||||
rowSpacing: 0
|
||||
columnSpacing: 0
|
||||
columns: 1
|
||||
z: 1
|
||||
|
||||
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);
|
||||
if (SelfVerificationStatus.status == SelfVerificationStatus.NoMasterKey)
|
||||
bootstrapCrosssigning.open();
|
||||
// else if (SelfVerificationStatus.status == SelfVerificationStatus.UnverifiedMasterKey)
|
||||
// verifyMasterKey.open();
|
||||
else if (SelfVerificationStatus.status == SelfVerificationStatus.UnverifiedMasterKey)
|
||||
verifyMasterKey.open();
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1131,11 +1131,71 @@ ChatPage::decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescriptio
|
|||
return;
|
||||
}
|
||||
|
||||
auto deviceKeys = cache::client()->userKeys(http::client()->user_id().to_string());
|
||||
mtx::requests::KeySignaturesUpload req;
|
||||
|
||||
for (const auto &[secretName, encryptedSecret] : secrets) {
|
||||
auto decrypted = mtx::crypto::decrypt(encryptedSecret, *decryptionKey, secretName);
|
||||
if (!decrypted.empty())
|
||||
if (!decrypted.empty()) {
|
||||
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
|
||||
|
|
|
@ -32,12 +32,15 @@ DeviceVerificationFlow::DeviceVerificationFlow(QObject *,
|
|||
DeviceVerificationFlow::Type flow_type,
|
||||
TimelineModel *model,
|
||||
QString userID,
|
||||
QString deviceId_)
|
||||
std::vector<QString> deviceIds_)
|
||||
: sender(false)
|
||||
, type(flow_type)
|
||||
, deviceId(deviceId_)
|
||||
, deviceIds(std::move(deviceIds_))
|
||||
, model_(model)
|
||||
{
|
||||
if (deviceIds.size() == 1)
|
||||
deviceId = deviceIds.front();
|
||||
|
||||
timeout = new QTimer(this);
|
||||
timeout->setSingleShot(true);
|
||||
this->sas = olm::client()->sas_init();
|
||||
|
@ -346,7 +349,8 @@ DeviceVerificationFlow::DeviceVerificationFlow(QObject *,
|
|||
}
|
||||
});
|
||||
|
||||
connect(ChatPage::instance(),
|
||||
connect(
|
||||
ChatPage::instance(),
|
||||
&ChatPage::receivedDeviceVerificationReady,
|
||||
this,
|
||||
[this](const mtx::events::msg::KeyVerificationReady &msg) {
|
||||
|
@ -364,6 +368,34 @@ DeviceVerificationFlow::DeviceVerificationFlow(QObject *,
|
|||
if (msg.transaction_id.has_value()) {
|
||||
if (msg.transaction_id.value() != this->transaction_id)
|
||||
return;
|
||||
|
||||
if (this->deviceId.isEmpty() && this->deviceIds.size() > 1) {
|
||||
auto from = QString::fromStdString(msg.from_device);
|
||||
if (std::find(deviceIds.begin(), deviceIds.end(), from) != deviceIds.end()) {
|
||||
mtx::events::msg::KeyVerificationCancel req{};
|
||||
req.code = "m.user";
|
||||
req.reason = "accepted by other device";
|
||||
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;
|
||||
|
@ -782,7 +814,7 @@ DeviceVerificationFlow::NewInRoomVerification(QObject *parent_,
|
|||
Type::RoomMsg,
|
||||
timelineModel_,
|
||||
other_user_,
|
||||
QString::fromStdString(msg.from_device)));
|
||||
{QString::fromStdString(msg.from_device)}));
|
||||
|
||||
flow->setEventId(event_id_.toStdString());
|
||||
|
||||
|
@ -801,7 +833,7 @@ DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_,
|
|||
QString txn_id_)
|
||||
{
|
||||
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();
|
||||
|
||||
if (std::find(msg.methods.begin(),
|
||||
|
@ -819,7 +851,7 @@ DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_,
|
|||
QString txn_id_)
|
||||
{
|
||||
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->handleStartMessage(msg, "");
|
||||
|
@ -832,15 +864,19 @@ DeviceVerificationFlow::InitiateUserVerification(QObject *parent_,
|
|||
QString userid)
|
||||
{
|
||||
QSharedPointer<DeviceVerificationFlow> flow(
|
||||
new DeviceVerificationFlow(parent_, Type::RoomMsg, timelineModel_, userid, ""));
|
||||
new DeviceVerificationFlow(parent_, Type::RoomMsg, timelineModel_, userid, {}));
|
||||
flow->sender = true;
|
||||
return flow;
|
||||
}
|
||||
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(
|
||||
new DeviceVerificationFlow(parent_, Type::ToDevice, nullptr, userid, device));
|
||||
new DeviceVerificationFlow(parent_, Type::ToDevice, nullptr, userid, devices));
|
||||
|
||||
flow->sender = true;
|
||||
flow->transaction_id = http::client()->generate_txn_id();
|
||||
|
|
|
@ -120,9 +120,8 @@ public:
|
|||
QString txn_id_);
|
||||
static QSharedPointer<DeviceVerificationFlow>
|
||||
InitiateUserVerification(QObject *parent_, TimelineModel *timelineModel_, QString userid);
|
||||
static QSharedPointer<DeviceVerificationFlow> InitiateDeviceVerification(QObject *parent,
|
||||
QString userid,
|
||||
QString device);
|
||||
static QSharedPointer<DeviceVerificationFlow>
|
||||
InitiateDeviceVerification(QObject *parent, QString userid, std::vector<QString> devices);
|
||||
|
||||
// getters
|
||||
QString state();
|
||||
|
@ -161,7 +160,7 @@ private:
|
|||
DeviceVerificationFlow::Type flow_type,
|
||||
TimelineModel *model,
|
||||
QString userID,
|
||||
QString deviceId_);
|
||||
std::vector<QString> deviceIds_);
|
||||
void setState(State state)
|
||||
{
|
||||
if (state != state_) {
|
||||
|
@ -196,6 +195,7 @@ private:
|
|||
Type type;
|
||||
mtx::identifiers::User toClient;
|
||||
QString deviceId;
|
||||
std::vector<QString> deviceIds;
|
||||
|
||||
// public part of our master key, when trusted or empty
|
||||
std::string our_trusted_master_key;
|
||||
|
@ -223,10 +223,11 @@ private:
|
|||
if (this->type == DeviceVerificationFlow::Type::ToDevice) {
|
||||
mtx::requests::ToDeviceMessages<T> body;
|
||||
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>(
|
||||
this->transaction_id, body, [](mtx::http::RequestErr err) {
|
||||
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,
|
||||
|
|
|
@ -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_user_signing);
|
||||
request(mtx::secret_storage::secrets::megolm_backup_v1);
|
||||
|
@ -1573,16 +1574,23 @@ download_cross_signing_keys()
|
|||
if (!err)
|
||||
backup_key = secret;
|
||||
|
||||
http::client()->secret_storage_secret(
|
||||
secrets::cross_signing_master, [backup_key](Secret secret, mtx::http::RequestErr err) {
|
||||
std::optional<Secret> master_key;
|
||||
if (!err)
|
||||
master_key = secret;
|
||||
|
||||
http::client()->secret_storage_secret(
|
||||
secrets::cross_signing_self_signing,
|
||||
[backup_key](Secret secret, mtx::http::RequestErr err) {
|
||||
[backup_key, master_key](Secret secret, mtx::http::RequestErr err) {
|
||||
std::optional<Secret> self_signing_key;
|
||||
if (!err)
|
||||
self_signing_key = secret;
|
||||
|
||||
http::client()->secret_storage_secret(
|
||||
secrets::cross_signing_user_signing,
|
||||
[backup_key, self_signing_key](Secret secret, mtx::http::RequestErr err) {
|
||||
[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;
|
||||
|
@ -1591,12 +1599,20 @@ download_cross_signing_keys()
|
|||
secrets;
|
||||
|
||||
if (backup_key && !backup_key->encrypted.empty())
|
||||
secrets[backup_key->encrypted.begin()->first][secrets::megolm_backup_v1] =
|
||||
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] =
|
||||
|
@ -1607,6 +1623,7 @@ download_cross_signing_keys()
|
|||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace olm
|
||||
|
|
|
@ -5,10 +5,12 @@
|
|||
#include "SelfVerificationStatus.h"
|
||||
|
||||
#include "Cache_p.h"
|
||||
#include "ChatPage.h"
|
||||
#include "Logging.h"
|
||||
#include "MainWindow.h"
|
||||
#include "MatrixClient.h"
|
||||
#include "Olm.h"
|
||||
#include "timeline/TimelineViewManager.h"
|
||||
#include "ui/UIA.h"
|
||||
|
||||
#include <mtx/responses/common.hpp>
|
||||
|
@ -196,6 +198,35 @@ void
|
|||
SelfVerificationStatus::verifyMasterKey()
|
||||
{
|
||||
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
|
||||
|
@ -207,9 +238,15 @@ SelfVerificationStatus::verifyUnverifiedDevices()
|
|||
void
|
||||
SelfVerificationStatus::invalidate()
|
||||
{
|
||||
using namespace mtx::secret_storage;
|
||||
|
||||
nhlog::db()->info("Invalidating self verification status");
|
||||
this->hasSSSS_ = false;
|
||||
emit hasSSSSChanged();
|
||||
|
||||
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(),
|
||||
[](const UserKeyCache &, mtx::http::RequestErr) {});
|
||||
return;
|
||||
|
@ -223,6 +260,14 @@ SelfVerificationStatus::invalidate()
|
|||
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());
|
||||
|
||||
if (!verifStatus.user_verified) {
|
||||
|
|
|
@ -11,6 +11,7 @@ class SelfVerificationStatus : public QObject
|
|||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(Status status READ status NOTIFY statusChanged)
|
||||
Q_PROPERTY(bool hasSSSS READ hasSSSS NOTIFY hasSSSSChanged)
|
||||
|
||||
public:
|
||||
SelfVerificationStatus(QObject *o = nullptr);
|
||||
|
@ -25,12 +26,15 @@ public:
|
|||
|
||||
Q_INVOKABLE void setupCrosssigning(bool useSSSS, QString password, bool useOnlineKeyBackup);
|
||||
Q_INVOKABLE void verifyMasterKey();
|
||||
Q_INVOKABLE void verifyMasterKeyWithPassphrase();
|
||||
Q_INVOKABLE void verifyUnverifiedDevices();
|
||||
|
||||
Status status() const { return status_; }
|
||||
bool hasSSSS() const { return hasSSSS_; }
|
||||
|
||||
signals:
|
||||
void statusChanged();
|
||||
void hasSSSSChanged();
|
||||
void setupCompleted();
|
||||
void showRecoveryKey(QString key);
|
||||
void setupFailed(QString message);
|
||||
|
@ -40,4 +44,5 @@ public slots:
|
|||
|
||||
private:
|
||||
Status status_ = AllVerified;
|
||||
bool hasSSSS_ = true;
|
||||
};
|
||||
|
|
|
@ -120,7 +120,16 @@ VerificationManager::removeVerificationFlow(DeviceVerificationFlow *flow)
|
|||
void
|
||||
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;
|
||||
emit newDeviceVerificationRequest(flow.data());
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ public:
|
|||
Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow);
|
||||
void verifyUser(QString userid);
|
||||
void verifyDevice(QString userid, QString deviceid);
|
||||
void verifyOneOfDevices(QString userid, std::vector<QString> deviceids);
|
||||
|
||||
signals:
|
||||
void newDeviceVerificationRequest(DeviceVerificationFlow *flow);
|
||||
|
|
Loading…
Reference in a new issue