Support bootstrapping crosssigning

Showing the bootstrap state and showing there are unverified devices is
still missing.
This commit is contained in:
Nicolas Werner 2021-09-18 00:21:14 +02:00
parent c514acb7c5
commit ad1e6c8298
No known key found for this signature in database
GPG key ID: C8D75E610773F2D9
20 changed files with 834 additions and 68 deletions

View file

@ -201,6 +201,18 @@ Cache::Cache(const QString &userId, QObject *parent)
{
setup();
connect(this, &Cache::userKeysUpdate, this, &Cache::updateUserKeys, Qt::QueuedConnection);
connect(
this,
&Cache::verificationStatusChanged,
this,
[this](const std::string &u) {
if (u == localUserId_.toStdString()) {
auto status = verificationStatus(u);
if (status.unverified_device_count || !status.user_verified)
emit selfUnverified();
}
},
Qt::QueuedConnection);
}
void

View file

@ -310,6 +310,7 @@ signals:
void removeNotification(const QString &room_id, const QString &event_id);
void userKeysUpdate(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery);
void verificationStatusChanged(const std::string &userid);
void selfUnverified();
void secretChanged(const std::string name);
private:

View file

@ -0,0 +1,249 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "SelfVerificationStatus.h"
#include "Cache_p.h"
#include "Logging.h"
#include "MainWindow.h"
#include "MatrixClient.h"
#include "Olm.h"
#include "ui/UIA.h"
#include <mtx/responses/common.hpp>
SelfVerificationStatus::SelfVerificationStatus(QObject *o)
: QObject(o)
{
connect(MainWindow::instance(), &MainWindow::reload, this, [this] {
connect(cache::client(),
&Cache::selfUnverified,
this,
&SelfVerificationStatus::invalidate,
Qt::UniqueConnection);
invalidate();
});
}
void
SelfVerificationStatus::setupCrosssigning(bool useSSSS, QString password, bool useOnlineKeyBackup)
{
nhlog::db()->info("Clicked setup crossigning");
auto xsign_keys = olm::client()->create_crosssigning_keys();
if (!xsign_keys) {
nhlog::crypto()->critical("Failed to setup cross-signing keys!");
emit setupFailed(tr("Failed to create keys for cross-signing!"));
return;
}
cache::client()->storeSecret(mtx::secret_storage::secrets::cross_signing_master,
xsign_keys->private_master_key);
cache::client()->storeSecret(mtx::secret_storage::secrets::cross_signing_self_signing,
xsign_keys->private_self_signing_key);
cache::client()->storeSecret(mtx::secret_storage::secrets::cross_signing_user_signing,
xsign_keys->private_user_signing_key);
std::optional<mtx::crypto::OlmClient::OnlineKeyBackupSetup> okb;
if (useOnlineKeyBackup) {
okb = olm::client()->create_online_key_backup(xsign_keys->private_master_key);
if (!okb) {
nhlog::crypto()->critical("Failed to setup online key backup!");
emit setupFailed(tr("Failed to create keys for online key backup!"));
return;
}
cache::client()->storeSecret(
mtx::secret_storage::secrets::megolm_backup_v1,
mtx::crypto::bin2base64(mtx::crypto::to_string(okb->privateKey)));
http::client()->post_backup_version(
okb->backupVersion.algorithm,
okb->backupVersion.auth_data,
[](const mtx::responses::Version &v, mtx::http::RequestErr e) {
if (e) {
nhlog::net()->error("error setting up online key backup: {} {} {} {}",
e->parse_error,
e->status_code,
e->error_code,
e->matrix_error.error);
} else {
nhlog::crypto()->info("Set up online key backup: '{}'", v.version);
}
});
}
std::optional<mtx::crypto::OlmClient::SSSSSetup> ssss;
if (useSSSS) {
ssss = olm::client()->create_ssss_key(password.toStdString());
if (!ssss) {
nhlog::crypto()->critical("Failed to setup secure server side secret storage!");
emit setupFailed(tr("Failed to create keys secure server side secret storage!"));
return;
}
auto master = mtx::crypto::PkSigning::from_seed(xsign_keys->private_master_key);
nlohmann::json j = ssss->keyDescription;
j.erase("signatures");
ssss->keyDescription
.signatures[http::client()->user_id().to_string()]["ed25519:" + master.public_key()] =
master.sign(j.dump());
http::client()->upload_secret_storage_key(
ssss->keyDescription.name, ssss->keyDescription, [](mtx::http::RequestErr) {});
http::client()->set_secret_storage_default_key(ssss->keyDescription.name,
[](mtx::http::RequestErr) {});
auto uploadSecret = [ssss](const std::string &key_name, const std::string &secret) {
mtx::secret_storage::Secret s;
s.encrypted[ssss->keyDescription.name] =
mtx::crypto::encrypt(secret, ssss->privateKey, key_name);
http::client()->upload_secret_storage_secret(
key_name, s, [key_name](mtx::http::RequestErr) {
nhlog::crypto()->info("Uploaded secret: {}", key_name);
});
};
uploadSecret(mtx::secret_storage::secrets::cross_signing_master,
xsign_keys->private_master_key);
uploadSecret(mtx::secret_storage::secrets::cross_signing_self_signing,
xsign_keys->private_self_signing_key);
uploadSecret(mtx::secret_storage::secrets::cross_signing_user_signing,
xsign_keys->private_user_signing_key);
if (okb)
uploadSecret(mtx::secret_storage::secrets::megolm_backup_v1,
mtx::crypto::bin2base64(mtx::crypto::to_string(okb->privateKey)));
}
mtx::requests::DeviceSigningUpload device_sign{};
device_sign.master_key = xsign_keys->master_key;
device_sign.self_signing_key = xsign_keys->self_signing_key;
device_sign.user_signing_key = xsign_keys->user_signing_key;
http::client()->device_signing_upload(
device_sign,
UIA::instance()->genericHandler(tr("Encryption Setup")),
[this, ssss, xsign_keys](mtx::http::RequestErr e) {
if (e) {
nhlog::crypto()->critical("Failed to upload cross signing keys: {}",
e->matrix_error.error);
emit setupFailed(tr("Encryption setup failed: %1")
.arg(QString::fromStdString(e->matrix_error.error)));
return;
}
nhlog::crypto()->info("Crosssigning keys uploaded!");
auto deviceKeys = cache::client()->userKeys(http::client()->user_id().to_string());
if (deviceKeys) {
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(xsign_keys->private_self_signing_key);
myKey.signatures[http::client()->user_id().to_string()]
["ed25519:" + ssk.public_key()] = ssk.sign(j.dump());
mtx::requests::KeySignaturesUpload req;
req.signatures[http::client()->user_id().to_string()]
[http::client()->device_id()] = myKey;
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);
});
}
}
if (ssss) {
auto k = QString::fromStdString(mtx::crypto::key_to_recoverykey(ssss->privateKey));
QString r;
for (int i = 0; i < k.size(); i += 4)
r += k.mid(i, 4) + " ";
emit showRecoveryKey(r.trimmed());
} else {
emit setupCompleted();
}
});
}
void
SelfVerificationStatus::verifyMasterKey()
{
nhlog::db()->info("Clicked verify master key");
}
void
SelfVerificationStatus::verifyUnverifiedDevices()
{
nhlog::db()->info("Clicked verify unverified devices");
}
void
SelfVerificationStatus::invalidate()
{
nhlog::db()->info("Invalidating self verification status");
auto keys = cache::client()->userKeys(http::client()->user_id().to_string());
if (!keys) {
cache::client()->query_keys(http::client()->user_id().to_string(),
[](const UserKeyCache &, mtx::http::RequestErr) {});
return;
}
if (keys->master_keys.keys.empty()) {
if (status_ != SelfVerificationStatus::NoMasterKey) {
this->status_ = SelfVerificationStatus::NoMasterKey;
emit statusChanged();
}
return;
}
auto verifStatus = cache::client()->verificationStatus(http::client()->user_id().to_string());
if (!verifStatus.user_verified) {
if (status_ != SelfVerificationStatus::UnverifiedMasterKey) {
this->status_ = SelfVerificationStatus::UnverifiedMasterKey;
emit statusChanged();
}
return;
}
if (verifStatus.unverified_device_count > 0) {
if (status_ != SelfVerificationStatus::UnverifiedDevices) {
this->status_ = SelfVerificationStatus::UnverifiedDevices;
emit statusChanged();
}
return;
}
if (status_ != SelfVerificationStatus::AllVerified) {
this->status_ = SelfVerificationStatus::AllVerified;
emit statusChanged();
return;
}
}

View file

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QObject>
class SelfVerificationStatus : public QObject
{
Q_OBJECT
Q_PROPERTY(Status status READ status NOTIFY statusChanged)
public:
SelfVerificationStatus(QObject *o = nullptr);
enum Status
{
AllVerified,
NoMasterKey,
UnverifiedMasterKey,
UnverifiedDevices,
};
Q_ENUM(Status)
Q_INVOKABLE void setupCrosssigning(bool useSSSS, QString password, bool useOnlineKeyBackup);
Q_INVOKABLE void verifyMasterKey();
Q_INVOKABLE void verifyUnverifiedDevices();
Status status() const { return status_; }
signals:
void statusChanged();
void setupCompleted();
void showRecoveryKey(QString key);
void setupFailed(QString message);
public slots:
void invalidate();
private:
Status status_ = AllVerified;
};

View file

@ -29,6 +29,7 @@
#include "ReadReceiptsModel.h"
#include "RoomDirectoryModel.h"
#include "RoomsModel.h"
#include "SelfVerificationStatus.h"
#include "SingleImagePackModel.h"
#include "UserSettingsPage.h"
#include "UsersModel.h"
@ -40,6 +41,7 @@
#include "ui/NhekoCursorShape.h"
#include "ui/NhekoDropArea.h"
#include "ui/NhekoGlobalObject.h"
#include "ui/UIA.h"
Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents)
Q_DECLARE_METATYPE(std::vector<DeviceInfo>)
@ -212,18 +214,9 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
"ReadReceiptsProxy needs to be instantiated on the C++ side");
static auto self = this;
qmlRegisterSingletonType<MainWindow>(
"im.nheko", 1, 0, "MainWindow", [](QQmlEngine *, QJSEngine *) -> QObject * {
auto ptr = MainWindow::instance();
QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
return ptr;
});
qmlRegisterSingletonType<TimelineViewManager>(
"im.nheko", 1, 0, "TimelineManager", [](QQmlEngine *, QJSEngine *) -> QObject * {
auto ptr = self;
QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
return ptr;
});
qmlRegisterSingletonInstance("im.nheko", 1, 0, "MainWindow", MainWindow::instance());
qmlRegisterSingletonInstance("im.nheko", 1, 0, "TimelineManager", self);
qmlRegisterSingletonInstance("im.nheko", 1, 0, "UIA", UIA::instance());
qmlRegisterSingletonType<RoomlistModel>(
"im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * {
auto ptr = new FilteredRoomlistModel(self->rooms_);
@ -238,24 +231,11 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
&FilteredRoomlistModel::updateHiddenTagsAndSpaces);
return ptr;
});
qmlRegisterSingletonType<RoomlistModel>(
"im.nheko", 1, 0, "Communities", [](QQmlEngine *, QJSEngine *) -> QObject * {
auto ptr = self->communities_;
QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
return ptr;
});
qmlRegisterSingletonType<UserSettings>(
"im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * {
auto ptr = ChatPage::instance()->userSettings().data();
QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
return ptr;
});
qmlRegisterSingletonType<CallManager>(
"im.nheko", 1, 0, "CallManager", [](QQmlEngine *, QJSEngine *) -> QObject * {
auto ptr = ChatPage::instance()->callManager();
QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
return ptr;
});
qmlRegisterSingletonInstance("im.nheko", 1, 0, "Communities", self->communities_);
qmlRegisterSingletonInstance(
"im.nheko", 1, 0, "Settings", ChatPage::instance()->userSettings().data());
qmlRegisterSingletonInstance(
"im.nheko", 1, 0, "CallManager", ChatPage::instance()->callManager());
qmlRegisterSingletonType<Clipboard>(
"im.nheko", 1, 0, "Clipboard", [](QQmlEngine *, QJSEngine *) -> QObject * {
return new Clipboard();
@ -264,6 +244,10 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
"im.nheko", 1, 0, "Nheko", [](QQmlEngine *, QJSEngine *) -> QObject * {
return new Nheko();
});
qmlRegisterSingletonType<SelfVerificationStatus>(
"im.nheko", 1, 0, "SelfVerificationStatus", [](QQmlEngine *, QJSEngine *) -> QObject * {
return new SelfVerificationStatus();
});
qRegisterMetaType<mtx::events::collections::TimelineEvents>();
qRegisterMetaType<std::vector<DeviceInfo>>();

136
src/ui/UIA.cpp Normal file
View file

@ -0,0 +1,136 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "UIA.h"
#include <algorithm>
#include <QInputDialog>
#include <QTimer>
#include "Logging.h"
#include "MainWindow.h"
#include "dialogs/FallbackAuth.h"
#include "dialogs/ReCaptcha.h"
UIA *
UIA::instance()
{
static UIA uia;
return &uia;
}
mtx::http::UIAHandler
UIA::genericHandler(QString context)
{
return mtx::http::UIAHandler([this, context](const mtx::http::UIAHandler &h,
const mtx::user_interactive::Unauthorized &u) {
QTimer::singleShot(0, this, [this, h, u, context]() {
this->currentHandler = h;
this->currentStatus = u;
this->title_ = context;
emit titleChanged();
std::vector<mtx::user_interactive::Flow> flows = u.flows;
nhlog::ui()->info("Completed stages: {}", u.completed.size());
if (!u.completed.empty()) {
// Get rid of all flows which don't start with the sequence of
// stages that have already been completed.
flows.erase(std::remove_if(flows.begin(),
flows.end(),
[completed_stages = u.completed](auto flow) {
if (completed_stages.size() > flow.stages.size())
return true;
for (size_t f = 0; f < completed_stages.size(); f++)
if (completed_stages[f] != flow.stages[f])
return true;
return false;
}),
flows.end());
}
if (flows.empty()) {
nhlog::ui()->error("No available registration flows!");
return;
}
auto current_stage = flows.front().stages.at(u.completed.size());
if (current_stage == mtx::user_interactive::auth_types::password) {
emit password();
} else if (current_stage == mtx::user_interactive::auth_types::recaptcha) {
auto captchaDialog =
new dialogs::ReCaptcha(QString::fromStdString(u.session), MainWindow::instance());
captchaDialog->setWindowTitle(context);
connect(
captchaDialog, &dialogs::ReCaptcha::confirmation, this, [captchaDialog, h, u]() {
captchaDialog->close();
captchaDialog->deleteLater();
h.next(mtx::user_interactive::Auth{u.session,
mtx::user_interactive::auth::Fallback{}});
});
// connect(
// captchaDialog, &dialogs::ReCaptcha::cancel, this, &RegisterPage::errorOccurred);
QTimer::singleShot(0, this, [captchaDialog]() { captchaDialog->show(); });
} else if (current_stage == mtx::user_interactive::auth_types::dummy) {
h.next(
mtx::user_interactive::Auth{u.session, mtx::user_interactive::auth::Dummy{}});
} else if (current_stage == mtx::user_interactive::auth_types::registration_token) {
bool ok;
QString token =
QInputDialog::getText(MainWindow::instance(),
context,
tr("Please enter a valid registration token."),
QLineEdit::Normal,
QString(),
&ok);
if (ok) {
h.next(mtx::user_interactive::Auth{
u.session,
mtx::user_interactive::auth::RegistrationToken{token.toStdString()}});
} else {
// emit errorOccurred();
}
} else {
// use fallback
auto dialog = new dialogs::FallbackAuth(QString::fromStdString(current_stage),
QString::fromStdString(u.session),
MainWindow::instance());
dialog->setWindowTitle(context);
connect(dialog, &dialogs::FallbackAuth::confirmation, this, [h, u, dialog]() {
dialog->close();
dialog->deleteLater();
h.next(mtx::user_interactive::Auth{u.session,
mtx::user_interactive::auth::Fallback{}});
});
// connect(dialog, &dialogs::FallbackAuth::cancel, this,
// &RegisterPage::errorOccurred);
dialog->show();
}
});
});
}
void
UIA::continuePassword(QString password)
{
mtx::user_interactive::auth::Password p{};
p.identifier_type = mtx::user_interactive::auth::Password::UserId;
p.password = password.toStdString();
p.identifier_user = http::client()->user_id().to_string();
if (currentHandler)
currentHandler->next(mtx::user_interactive::Auth{currentStatus.session, p});
}

40
src/ui/UIA.h Normal file
View file

@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QObject>
#include <MatrixClient.h>
class UIA : public QObject
{
Q_OBJECT
Q_PROPERTY(QString title READ title NOTIFY titleChanged)
public:
static UIA *instance();
UIA(QObject *parent = nullptr)
: QObject(parent)
{}
mtx::http::UIAHandler genericHandler(QString context);
QString title() const { return title_; }
public slots:
void continuePassword(QString password);
signals:
void password();
void titleChanged();
private:
std::optional<mtx::http::UIAHandler> currentHandler;
mtx::user_interactive::Unauthorized currentStatus;
QString title_;
};