Allow downloading keys from key backup

This commit is contained in:
Nicolas Werner 2021-08-17 03:23:51 +02:00
parent e5dea361c0
commit 56db0dbc7d
No known key found for this signature in database
GPG key ID: C8D75E610773F2D9
15 changed files with 342 additions and 34 deletions

View file

@ -381,7 +381,7 @@ if(USE_BUNDLED_MTXCLIENT)
FetchContent_Declare(
MatrixClient
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
GIT_TAG deb51ef1d6df870098069312f0a1999550e1eb85
GIT_TAG 513196f520733e2f70576168aff7aaf16e0df180
)
set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")

View file

@ -163,7 +163,7 @@ modules:
buildsystem: cmake-ninja
name: mtxclient
sources:
- commit: deb51ef1d6df870098069312f0a1999550e1eb85
- commit: 513196f520733e2f70576168aff7aaf16e0df180
type: git
url: https://github.com/Nheko-Reborn/mtxclient.git
- config-opts:

View file

@ -39,7 +39,7 @@ Image {
case Crypto.TOFU:
return qsTr("Encrypted by an unverified device, but you have trusted that user so far.");
default:
return qsTr("Encrypted by an unverified device");
return qsTr("Encrypted by an unverified device or the key is from an untrusted source like the key backup.");
}
}

View file

@ -42,6 +42,7 @@ static const std::string SECRET("secret");
static const std::string_view NEXT_BATCH_KEY("next_batch");
static const std::string_view OLM_ACCOUNT_KEY("olm_account");
static const std::string_view CACHE_FORMAT_VERSION_KEY("cache_format_version");
static const std::string_view CURRENT_ONLINE_BACKUP_VERSION("current_online_backup_version");
constexpr size_t MAX_RESTORED_MESSAGES = 30'000;
@ -723,6 +724,36 @@ Cache::restoreOlmAccount()
return std::string(pickled.data(), pickled.size());
}
void
Cache::saveBackupVersion(const OnlineBackupVersion &data)
{
auto txn = lmdb::txn::begin(env_);
syncStateDb_.put(txn, CURRENT_ONLINE_BACKUP_VERSION, nlohmann::json(data).dump());
txn.commit();
}
void
Cache::deleteBackupVersion()
{
auto txn = lmdb::txn::begin(env_);
syncStateDb_.del(txn, CURRENT_ONLINE_BACKUP_VERSION);
txn.commit();
}
std::optional<OnlineBackupVersion>
Cache::backupVersion()
{
try {
auto txn = ro_txn(env_);
std::string_view v;
syncStateDb_.get(txn, CURRENT_ONLINE_BACKUP_VERSION, v);
return nlohmann::json::parse(v).get<OnlineBackupVersion>();
} catch (...) {
return std::nullopt;
}
}
void
Cache::storeSecret(const std::string name, const std::string secret)
{
@ -4114,6 +4145,20 @@ from_json(const json &j, VerificationCache &info)
info.device_blocked = j.at("device_blocked").get<std::set<std::string>>();
}
void
to_json(json &j, const OnlineBackupVersion &info)
{
j["v"] = info.version;
j["a"] = info.algorithm;
}
void
from_json(const json &j, OnlineBackupVersion &info)
{
info.version = j.at("v").get<std::string>();
info.algorithm = j.at("a").get<std::string>();
}
std::optional<VerificationCache>
Cache::verificationCache(const std::string &user_id, lmdb::txn &txn)
{
@ -4461,6 +4506,7 @@ to_json(nlohmann::json &obj, const GroupSessionData &msg)
{
obj["message_index"] = msg.message_index;
obj["ts"] = msg.timestamp;
obj["trust"] = msg.trusted;
obj["sender_claimed_ed25519_key"] = msg.sender_claimed_ed25519_key;
obj["forwarding_curve25519_key_chain"] = msg.forwarding_curve25519_key_chain;
@ -4475,6 +4521,7 @@ from_json(const nlohmann::json &obj, GroupSessionData &msg)
{
msg.message_index = obj.at("message_index");
msg.timestamp = obj.value("ts", 0ULL);
msg.trusted = obj.value("trust", true);
msg.sender_claimed_ed25519_key = obj.value("sender_claimed_ed25519_key", "");
msg.forwarding_curve25519_key_chain =

View file

@ -47,6 +47,11 @@ struct GroupSessionData
uint64_t message_index = 0;
uint64_t timestamp = 0;
// If we got the session via key sharing or forwarding, we can usually trust it.
// If it came from asymmetric key backup, it is not trusted.
// TODO(Nico): What about forwards? They might come from key backup?
bool trusted = true;
std::string sender_claimed_ed25519_key;
std::vector<std::string> forwarding_curve25519_key_chain;
@ -83,6 +88,13 @@ from_json(const nlohmann::json &obj, DevicePublicKeys &msg);
//! Represents a unique megolm session identifier.
struct MegolmSessionIndex
{
MegolmSessionIndex() = default;
MegolmSessionIndex(std::string room_id_, const mtx::events::msg::Encrypted &e)
: room_id(std::move(room_id_))
, session_id(e.session_id)
, sender_key(e.sender_key)
{}
//! The room in which this session exists.
std::string room_id;
//! The session_id of the megolm session.
@ -167,3 +179,16 @@ void
to_json(nlohmann::json &j, const VerificationCache &info);
void
from_json(const nlohmann::json &j, VerificationCache &info);
struct OnlineBackupVersion
{
//! the version of the online backup currently enabled
std::string version;
//! the algorithm used by the backup
std::string algorithm;
};
void
to_json(nlohmann::json &j, const OnlineBackupVersion &info);
void
from_json(const nlohmann::json &j, OnlineBackupVersion &info);

View file

@ -287,6 +287,10 @@ public:
void saveOlmAccount(const std::string &pickled);
std::string restoreOlmAccount();
void saveBackupVersion(const OnlineBackupVersion &data);
void deleteBackupVersion();
std::optional<OnlineBackupVersion> backupVersion();
void storeSecret(const std::string name, const std::string secret);
void deleteSecret(const std::string name);
std::optional<std::string> secret(const std::string name);

View file

@ -385,6 +385,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
}
getProfileInfo();
getBackupVersion();
tryInitialSync();
}
@ -418,6 +419,7 @@ ChatPage::loadStateFromCache()
nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519);
getProfileInfo();
getBackupVersion();
emit contentLoaded();
@ -974,6 +976,104 @@ ChatPage::getProfileInfo()
});
}
void
ChatPage::getBackupVersion()
{
if (!UserSettings::instance()->useOnlineKeyBackup()) {
nhlog::crypto()->info("Online key backup disabled.");
return;
}
http::client()->backup_version(
[this](const mtx::responses::backup::BackupVersion &res, mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("Failed to retrieve backup version");
if (err->status_code == 404)
cache::client()->deleteBackupVersion();
return;
}
// switch to UI thread for secrets stuff
QTimer::singleShot(0, this, [this, res] {
auto auth_data = nlohmann::json::parse(res.auth_data);
if (res.algorithm == "m.megolm_backup.v1.curve25519-aes-sha2") {
auto key =
cache::secret(mtx::secret_storage::secrets::megolm_backup_v1);
if (!key) {
nhlog::crypto()->info("No key for online key backup.");
cache::client()->deleteBackupVersion();
return;
}
using namespace mtx::crypto;
auto pubkey = CURVE25519_public_key_from_private(
to_binary_buf(base642bin(*key)));
if (auth_data["public_key"].get<std::string>() != pubkey) {
nhlog::crypto()->info(
"Our backup key {} does not match the one "
"used in the online backup {}",
pubkey,
auth_data["public_key"]);
cache::client()->deleteBackupVersion();
return;
}
nhlog::crypto()->info("Using online key backup.");
OnlineBackupVersion data{};
data.algorithm = res.algorithm;
data.version = res.version;
cache::client()->saveBackupVersion(data);
// We really don't need to add our signature.
// if (!auth_data["signatures"]
// [http::client()->user_id().to_string()]
// .contains("ed25519:" +
// http::client()->device_id())) {
// // add our signature
// // This is not strictly necessary, but some Element
// // clients rely on it. We assume the master_key
// signature
// // already exists and add our device signature just to
// be
// // safe, even though just the master signature is
// enough,
// // if cross-signing is used.
// auto signatures = auth_data["signatures"];
// auth_data.erase("signatures");
// auth_data.erase("unsigned");
// signatures[http::client()->user_id().to_string()]
// ["ed25519:" + http::client()->device_id()] =
// olm::client()->sign_message(auth_data.dump());
// auth_data["signatures"] = signatures;
// auto copy = res;
// copy.auth_data = auth_data.dump();
// http::client()->update_backup_version(
// res.version, copy, [](mtx::http::RequestErr e) {
// if (e) {
// nhlog::crypto()->error(
// "Failed to update online backup "
// "signatures: {} - {}",
// mtx::errors::to_string(
// e->matrix_error.errcode),
// e->matrix_error.error);
// } else {
// nhlog::crypto()->info(
// "Updated key backup signatures");
// }
// });
//}
} else {
nhlog::crypto()->info("Unsupported key backup algorithm: {}",
res.algorithm);
cache::client()->deleteBackupVersion();
}
});
});
}
void
ChatPage::initiateLogout()
{

View file

@ -182,6 +182,7 @@ private:
void trySync();
void ensureOneTimeKeyCount(const std::map<std::string, uint16_t> &counts);
void getProfileInfo();
void getBackupVersion();
//! Check if the given room is currently open.
bool isRoomActive(const QString &room_id);

View file

@ -130,6 +130,8 @@ MainWindow::MainWindow(QWidget *parent)
trayIcon_->setVisible(userSettings_->tray());
// load cache on event loop
QTimer::singleShot(0, this, [this] {
if (hasActiveUser()) {
QString token = userSettings_->accessToken();
QString home_server = userSettings_->homeserver();
@ -150,6 +152,7 @@ MainWindow::MainWindow(QWidget *parent)
showChatPage();
}
});
if (loadJdenticonPlugin()) {
nhlog::ui()->info("loaded jdenticon.");

View file

@ -833,6 +833,8 @@ import_inbound_megolm_session(
data.forwarding_curve25519_key_chain =
roomKey.content.forwarding_curve25519_key_chain;
data.sender_claimed_ed25519_key = roomKey.content.sender_claimed_ed25519_key;
// may have come from online key backup, so we can't trust it...
data.trusted = false;
cache::saveInboundMegolmSession(index, std::move(megolm_session), data);
} catch (const lmdb::error &e) {
@ -856,6 +858,97 @@ mark_keys_as_published()
cache::saveOlmAccount(olm::client()->save(STORAGE_SECRET_KEY));
}
void
lookup_keybackup(const std::string room, const std::string session_id)
{
if (!UserSettings::instance()->useOnlineKeyBackup()) {
// Online key backup disabled
return;
}
auto backupVersion = cache::client()->backupVersion();
if (!backupVersion) {
// no trusted OKB
return;
}
using namespace mtx::crypto;
auto decryptedSecret = cache::secret(mtx::secret_storage::secrets::megolm_backup_v1);
if (!decryptedSecret) {
// no backup key available
return;
}
auto sessionDecryptionKey = to_binary_buf(base642bin(*decryptedSecret));
http::client()->room_keys(
backupVersion->version,
room,
session_id,
[room, session_id, sessionDecryptionKey](const mtx::responses::backup::SessionBackup &bk,
mtx::http::RequestErr err) {
if (err) {
if (err->status_code != 404)
nhlog::crypto()->error(
"Failed to dowload key {}:{}: {} - {}",
room,
session_id,
mtx::errors::to_string(err->matrix_error.errcode),
err->matrix_error.error);
return;
}
try {
auto session = decrypt_session(bk.session_data, sessionDecryptionKey);
if (session.algorithm != mtx::crypto::MEGOLM_ALGO)
// don't know this algorithm
return;
MegolmSessionIndex index;
index.room_id = room;
index.session_id = session_id;
index.sender_key = session.sender_key;
GroupSessionData data{};
data.forwarding_curve25519_key_chain =
session.forwarding_curve25519_key_chain;
data.sender_claimed_ed25519_key = session.sender_claimed_keys["ed25519"];
// online key backup can't be trusted, because anyone can upload to it.
data.trusted = false;
auto megolm_session =
olm::client()->import_inbound_group_session(session.session_key);
if (!cache::inboundMegolmSessionExists(index) ||
olm_inbound_group_session_first_known_index(megolm_session.get()) <
olm_inbound_group_session_first_known_index(
cache::getInboundMegolmSession(index).get())) {
cache::saveInboundMegolmSession(
index, std::move(megolm_session), data);
nhlog::crypto()->info("imported inbound megolm session "
"from key backup ({}, {})",
room,
session_id);
// call on UI thread
QTimer::singleShot(0, ChatPage::instance(), [index] {
ChatPage::instance()->receivedSessionKey(
index.room_id, index.session_id);
});
}
} catch (const lmdb::error &e) {
nhlog::crypto()->critical("failed to save inbound megolm session: {}",
e.what());
return;
} catch (const mtx::crypto::olm_exception &e) {
nhlog::crypto()->critical("failed to import inbound megolm session: {}",
e.what());
return;
}
});
}
void
send_key_request_for(mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> e,
const std::string &request_id,
@ -898,6 +991,8 @@ send_key_request_for(mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> e,
e.sender,
e.content.device_id);
});
// http::client()->room_keys
}
void
@ -1095,12 +1190,15 @@ decryptEvent(const MegolmSessionIndex &index,
}
crypto::Trust
calculate_trust(const std::string &user_id, const std::string &curve25519)
calculate_trust(const std::string &user_id, const MegolmSessionIndex &index)
{
auto status = cache::client()->verificationStatus(user_id);
auto megolmData = cache::client()->getMegolmSessionData(index);
crypto::Trust trustlevel = crypto::Trust::Unverified;
if (status.verified_device_keys.count(curve25519))
trustlevel = status.verified_device_keys.at(curve25519);
if (megolmData && megolmData->trusted &&
status.verified_device_keys.count(index.sender_key))
trustlevel = status.verified_device_keys.at(index.sender_key);
return trustlevel;
}

View file

@ -70,6 +70,8 @@ create_inbound_megolm_session(const mtx::events::DeviceEvent<mtx::events::msg::R
void
import_inbound_megolm_session(
const mtx::events::DeviceEvent<mtx::events::msg::ForwardedRoomKey> &roomKey);
void
lookup_keybackup(const std::string room, const std::string session_id);
nlohmann::json
handle_pre_key_olm_message(const std::string &sender,
@ -87,7 +89,7 @@ decryptEvent(const MegolmSessionIndex &index,
const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &event,
bool dont_write_db = false);
crypto::Trust
calculate_trust(const std::string &user_id, const std::string &curve25519);
calculate_trust(const std::string &user_id, const MegolmSessionIndex &index);
void
mark_keys_as_published();

View file

@ -124,6 +124,7 @@ UserSettings::load(std::optional<QString> profile)
.toBool();
onlyShareKeysWithVerifiedUsers_ =
settings.value(prefix + "user/only_share_keys_with_verified_users", false).toBool();
useOnlineKeyBackup_ = settings.value(prefix + "user/online_key_backup", true).toBool();
disableCertificateValidation_ =
settings.value("disable_certificate_validation", false).toBool();
@ -425,6 +426,17 @@ UserSettings::setShareKeysWithTrustedUsers(bool shareKeys)
save();
}
void
UserSettings::setUseOnlineKeyBackup(bool useBackup)
{
if (useBackup == useOnlineKeyBackup_)
return;
useOnlineKeyBackup_ = useBackup;
emit useOnlineKeyBackupChanged(useBackup);
save();
}
void
UserSettings::setRingtone(QString ringtone)
{
@ -664,6 +676,7 @@ UserSettings::save()
shareKeysWithTrustedUsers_);
settings.setValue(prefix + "user/only_share_keys_with_verified_users",
onlyShareKeysWithVerifiedUsers_);
settings.setValue(prefix + "user/online_key_backup", useOnlineKeyBackup_);
settings.setValue("disable_certificate_validation", disableCertificateValidation_);
@ -725,6 +738,7 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
privacyScreen_ = new Toggle{this};
onlyShareKeysWithVerifiedUsers_ = new Toggle(this);
shareKeysWithTrustedUsers_ = new Toggle(this);
useOnlineKeyBackup_ = new Toggle(this);
groupViewToggle_ = new Toggle{this};
timelineButtonsToggle_ = new Toggle{this};
typingNotifications_ = new Toggle{this};
@ -756,6 +770,7 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
privacyScreen_->setChecked(settings_->privacyScreen());
onlyShareKeysWithVerifiedUsers_->setChecked(settings_->onlyShareKeysWithVerifiedUsers());
shareKeysWithTrustedUsers_->setChecked(settings_->shareKeysWithTrustedUsers());
useOnlineKeyBackup_->setChecked(settings_->useOnlineKeyBackup());
groupViewToggle_->setChecked(settings_->groupView());
timelineButtonsToggle_->setChecked(settings_->buttonsInTimeline());
typingNotifications_->setChecked(settings_->typingNotifications());
@ -1033,6 +1048,10 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
shareKeysWithTrustedUsers_,
tr("Automatically replies to key requests from other users, if they are verified, "
"even if that device shouldn't have access to those keys otherwise."));
boxWrap(tr("Online Key Backup"),
useOnlineKeyBackup_,
tr("Download message encryption keys from and upload to the encrypted online key "
"backup."));
formLayout_->addRow(new HorizontalLine{this});
formLayout_->addRow(sessionKeysLabel, sessionKeysLayout);
formLayout_->addRow(crossSigningKeysLabel, crossSigningKeysLayout);
@ -1208,6 +1227,10 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
settings_->setShareKeysWithTrustedUsers(enabled);
});
connect(useOnlineKeyBackup_, &Toggle::toggled, this, [this](bool enabled) {
settings_->setUseOnlineKeyBackup(enabled);
});
connect(avatarCircles_, &Toggle::toggled, this, [this](bool enabled) {
settings_->setAvatarCircles(enabled);
});
@ -1298,6 +1321,7 @@ UserSettingsPage::showEvent(QShowEvent *)
privacyScreen_->setState(settings_->privacyScreen());
onlyShareKeysWithVerifiedUsers_->setState(settings_->onlyShareKeysWithVerifiedUsers());
shareKeysWithTrustedUsers_->setState(settings_->shareKeysWithTrustedUsers());
useOnlineKeyBackup_->setState(settings_->useOnlineKeyBackup());
avatarCircles_->setState(settings_->avatarCircles());
typingNotifications_->setState(settings_->typingNotifications());
sortByImportance_->setState(settings_->sortByImportance());

View file

@ -93,6 +93,8 @@ class UserSettings : public QObject
setOnlyShareKeysWithVerifiedUsers NOTIFY onlyShareKeysWithVerifiedUsersChanged)
Q_PROPERTY(bool shareKeysWithTrustedUsers READ shareKeysWithTrustedUsers WRITE
setShareKeysWithTrustedUsers NOTIFY shareKeysWithTrustedUsersChanged)
Q_PROPERTY(bool useOnlineKeyBackup READ useOnlineKeyBackup WRITE setUseOnlineKeyBackup
NOTIFY useOnlineKeyBackupChanged)
Q_PROPERTY(QString profile READ profile WRITE setProfile NOTIFY profileChanged)
Q_PROPERTY(QString userId READ userId WRITE setUserId NOTIFY userIdChanged)
Q_PROPERTY(
@ -159,6 +161,7 @@ public:
void setUseStunServer(bool state);
void setOnlyShareKeysWithVerifiedUsers(bool state);
void setShareKeysWithTrustedUsers(bool state);
void setUseOnlineKeyBackup(bool state);
void setProfile(QString profile);
void setUserId(QString userId);
void setAccessToken(QString accessToken);
@ -215,6 +218,7 @@ public:
bool useStunServer() const { return useStunServer_; }
bool shareKeysWithTrustedUsers() const { return shareKeysWithTrustedUsers_; }
bool onlyShareKeysWithVerifiedUsers() const { return onlyShareKeysWithVerifiedUsers_; }
bool useOnlineKeyBackup() const { return useOnlineKeyBackup_; }
QString profile() const { return profile_; }
QString userId() const { return userId_; }
QString accessToken() const { return accessToken_; }
@ -261,6 +265,7 @@ signals:
void useStunServerChanged(bool state);
void onlyShareKeysWithVerifiedUsersChanged(bool state);
void shareKeysWithTrustedUsersChanged(bool state);
void useOnlineKeyBackupChanged(bool state);
void profileChanged(QString profile);
void userIdChanged(QString userId);
void accessTokenChanged(QString accessToken);
@ -293,6 +298,7 @@ private:
int privacyScreenTimeout_;
bool shareKeysWithTrustedUsers_;
bool onlyShareKeysWithVerifiedUsers_;
bool useOnlineKeyBackup_;
bool mobileMode_;
int timelineMaxWidth_;
int roomListWidth_;
@ -384,6 +390,7 @@ private:
QSpinBox *privacyScreenTimeout_;
Toggle *shareKeysWithTrustedUsers_;
Toggle *onlyShareKeysWithVerifiedUsers_;
Toggle *useOnlineKeyBackup_;
Toggle *mobileMode_;
QLabel *deviceFingerprintValue_;
QLabel *deviceIdValue_;

View file

@ -643,10 +643,7 @@ EventStore::decryptEvent(const IdIndex &idx,
if (auto cachedEvent = decryptedEvents_.object(idx))
return cachedEvent;
MegolmSessionIndex index;
index.room_id = room_id_;
index.session_id = e.content.session_id;
index.sender_key = e.content.sender_key;
MegolmSessionIndex index(room_id_, e.content);
auto asCacheEntry = [&idx](olm::DecryptionResult &&event) {
auto event_ptr = new olm::DecryptionResult(std::move(event));
@ -726,6 +723,7 @@ EventStore::requestSession(const mtx::events::EncryptedEvent<mtx::events::msg::E
qint64 delay = manual ? 60 : (60 * 10);
if (r.requested_at + delay < QDateTime::currentSecsSinceEpoch()) {
r.requested_at = QDateTime::currentSecsSinceEpoch();
olm::lookup_keybackup(room_id_, ev.content.session_id);
olm::send_key_request_for(copy, r.request_id);
}
} else {
@ -733,6 +731,7 @@ EventStore::requestSession(const mtx::events::EncryptedEvent<mtx::events::msg::E
request.request_id = "key_request." + http::client()->generate_txn_id();
request.requested_at = QDateTime::currentSecsSinceEpoch();
request.events.push_back(copy);
olm::lookup_keybackup(room_id_, ev.content.session_id);
olm::send_key_request_for(copy, request.request_id);
pending_key_requests[ev.content.session_id] = request;
}

View file

@ -641,8 +641,9 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
if (auto encrypted =
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
&*encrypted_event)) {
return olm::calculate_trust(encrypted->sender,
encrypted->content.sender_key);
return olm::calculate_trust(
encrypted->sender,
MegolmSessionIndex(room_id_.toStdString(), encrypted->content));
}
}
return crypto::Trust::Unverified;
@ -840,10 +841,7 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline)
for (auto e : timeline.events) {
if (auto encryptedEvent = std::get_if<EncryptedEvent<msg::Encrypted>>(&e)) {
MegolmSessionIndex index;
index.room_id = room_id_.toStdString();
index.session_id = encryptedEvent->content.session_id;
index.sender_key = encryptedEvent->content.sender_key;
MegolmSessionIndex index(room_id_.toStdString(), encryptedEvent->content);
auto result = olm::decryptEvent(index, *encryptedEvent);
if (result.event)