#include "encryptionmanager.h" #include #include #include #include #include // QtOlm #include "csapi/keys.h" #include "connection.h" #include "e2ee.h" using namespace QMatrixClient; using namespace QtOlm; using std::move; class EncryptionManager::Private { public: explicit Private(const QByteArray& encryptionAccountPickle, float signedKeysProportion, float oneTimeKeyThreshold) : signedKeysProportion(move(signedKeysProportion)), oneTimeKeyThreshold(move(oneTimeKeyThreshold)) { Q_ASSERT((0 <= signedKeysProportion) && (signedKeysProportion <= 1)); Q_ASSERT((0 <= oneTimeKeyThreshold) && (oneTimeKeyThreshold <= 1)); if (encryptionAccountPickle.isEmpty()) { olmAccount.reset(new Account()); } else { olmAccount.reset(new Account(encryptionAccountPickle)); // TODO: passphrase even with qtkeychain? } /* * Note about targetKeysNumber: * * From: https://github.com/Zil0/matrix-python-sdk/ * File: matrix_client/crypto/olm_device.py * * Try to maintain half the number of one-time keys libolm can hold uploaded * on the HS. This is because some keys will be claimed by peers but not * used instantly, and we want them to stay in libolm, until the limit is reached * and it starts discarding keys, starting by the oldest. */ targetKeysNumber = olmAccount->maxOneTimeKeys(); // 2 // see note below targetOneTimeKeyCounts = { {SignedCurve25519Key, qRound(signedKeysProportion * targetKeysNumber)}, {Curve25519Key, qRound((1-signedKeysProportion) * targetKeysNumber)} }; } ~Private() = default; UploadKeysJob* uploadIdentityKeysJob = nullptr; UploadKeysJob* uploadOneTimeKeysJob = nullptr; QScopedPointer olmAccount; float signedKeysProportion; float oneTimeKeyThreshold; int targetKeysNumber; void updateKeysToUpload(); bool oneTimeKeyShouldUpload(); QHash oneTimeKeyCounts; void setOneTimeKeyCounts(const QHash oneTimeKeyCountsNewValue) { oneTimeKeyCounts = oneTimeKeyCountsNewValue; updateKeysToUpload(); } QHash oneTimeKeysToUploadCounts; QHash targetOneTimeKeyCounts; }; EncryptionManager::EncryptionManager(const QByteArray &encryptionAccountPickle, float signedKeysProportion, float oneTimeKeyThreshold, QObject* parent) : QObject(parent), d(std::make_unique(std::move(encryptionAccountPickle), std::move(signedKeysProportion), std::move(oneTimeKeyThreshold))) { } EncryptionManager::~EncryptionManager() = default; void EncryptionManager::uploadIdentityKeys(Connection* connection) { // https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-keys-upload DeviceKeys deviceKeys { /* * The ID of the user the device belongs to. Must match the user ID used when logging in. * The ID of the device these keys belong to. Must match the device ID used when logging in. * The encryption algorithms supported by this device. */ connection->userId(), connection->deviceId(), SupportedAlgorithms, /* * Public identity keys. The names of the properties should be in the format :. * The keys themselves should be encoded as specified by the key algorithm. */ { { Curve25519Key + QStringLiteral(":") + connection->deviceId(), d->olmAccount->curve25519IdentityKey() }, { Ed25519Key + QStringLiteral(":") + connection->deviceId(), d->olmAccount->ed25519IdentityKey() } }, /* signatures should be provided after the unsigned deviceKeys generation */ {} }; QJsonObject deviceKeysJsonObject = toJson(deviceKeys); /* additionally removing signatures key, * since we could not initialize deviceKeys * without an empty signatures value: */ deviceKeysJsonObject.remove(QStringLiteral("signatures")); /* * Signatures for the device key object. * A map from user ID, to a map from : to the signature. * The signature is calculated using the process called Signing JSON. */ deviceKeys.signatures = { { connection->userId(), { { Ed25519Key + QStringLiteral(":") + connection->deviceId(), d->olmAccount->sign(deviceKeysJsonObject) } } } }; connect(d->uploadIdentityKeysJob, &BaseJob::success, this, [this] { d->setOneTimeKeyCounts(d->uploadIdentityKeysJob->oneTimeKeyCounts()); qDebug() << QString("Uploaded identity keys."); }); d->uploadIdentityKeysJob = connection->callApi(deviceKeys); } void EncryptionManager::uploadOneTimeKeys(Connection* connection, bool forceUpdate) { if (forceUpdate || d->oneTimeKeyCounts.isEmpty()) { auto job = connection->callApi(); connect(job, &BaseJob::success, this, [job,this] { d->setOneTimeKeyCounts(job->oneTimeKeyCounts()); }); } int signedKeysToUploadCount = d->oneTimeKeysToUploadCounts.value(SignedCurve25519Key, 0); int unsignedKeysToUploadCount = d->oneTimeKeysToUploadCounts.value(Curve25519Key, 0); d->olmAccount->generateOneTimeKeys(signedKeysToUploadCount + unsignedKeysToUploadCount); QHash oneTimeKeys = {}; const auto& olmAccountCurve25519OneTimeKeys = d->olmAccount->curve25519OneTimeKeys(); int oneTimeKeysCounter = 0; for (auto it = olmAccountCurve25519OneTimeKeys.cbegin(); it != olmAccountCurve25519OneTimeKeys.cend(); ++it) { QString keyId = it.key(); QString keyType; QVariant key; if (oneTimeKeysCounter < signedKeysToUploadCount) { QJsonObject message { {QStringLiteral("key"), it.value().toString()} }; key = d->olmAccount->sign(message); keyType = SignedCurve25519Key; } else { key = it.value(); keyType = Curve25519Key; } ++oneTimeKeysCounter; oneTimeKeys.insert(QString("%1:%2").arg(keyType).arg(keyId), key); } d->uploadOneTimeKeysJob = connection->callApi(none, oneTimeKeys); d->olmAccount->markKeysAsPublished(); qDebug() << QString("Uploaded new one-time keys: %1 signed, %2 unsigned.") .arg(signedKeysToUploadCount).arg(unsignedKeysToUploadCount); } QByteArray EncryptionManager::olmAccountPickle() { return d->olmAccount->pickle(); // TODO: passphrase even with qtkeychain? } void EncryptionManager::Private::updateKeysToUpload() { for (auto it = targetOneTimeKeyCounts.cbegin(); it != targetOneTimeKeyCounts.cend(); ++it) { int numKeys = oneTimeKeyCounts.value(it.key(), 0); int numToCreate = qMax(it.value() - numKeys, 0); oneTimeKeysToUploadCounts.insert(it.key(), numToCreate); } } bool EncryptionManager::Private::oneTimeKeyShouldUpload() { if (oneTimeKeyCounts.empty()) return true; for (auto it = targetOneTimeKeyCounts.cbegin(); it != targetOneTimeKeyCounts.cend(); ++it) { if (oneTimeKeyCounts.value(it.key(), 0) < it.value() * oneTimeKeyThreshold) return true; } return false; }