aboutsummaryrefslogtreecommitdiff
path: root/lib/encryptionmanager.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'lib/encryptionmanager.cpp')
-rw-r--r--lib/encryptionmanager.cpp369
1 files changed, 369 insertions, 0 deletions
diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp
new file mode 100644
index 00000000..4a1025b2
--- /dev/null
+++ b/lib/encryptionmanager.cpp
@@ -0,0 +1,369 @@
+#ifdef Quotient_E2EE_ENABLED
+#include "encryptionmanager.h"
+
+#include "connection.h"
+#include "e2ee.h"
+
+#include "csapi/keys.h"
+
+#include <QtCore/QHash>
+#include <QtCore/QStringBuilder>
+
+#include <account.h> // QtOlm
+#include <session.h> // QtOlm
+#include <message.h> // QtOlm
+#include <errors.h> // QtOlm
+#include <utils.h> // QtOlm
+#include <functional>
+#include <memory>
+
+using namespace Quotient;
+using namespace QtOlm;
+using std::move;
+
+class EncryptionManager::Private {
+public:
+ explicit Private(const QByteArray& encryptionAccountPickle,
+ float signedKeysProportion, float oneTimeKeyThreshold)
+ : q(nullptr)
+ , 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;
+ targetOneTimeKeyCounts = {
+ { SignedCurve25519Key,
+ qRound(signedKeysProportion * targetKeysNumber) },
+ { Curve25519Key,
+ qRound((1 - signedKeysProportion) * targetKeysNumber) }
+ };
+ updateKeysToUpload();
+ }
+ ~Private() = default;
+
+ EncryptionManager* q;
+
+ UploadKeysJob* uploadIdentityKeysJob = nullptr;
+ UploadKeysJob* uploadOneTimeKeysInitJob = nullptr;
+ UploadKeysJob* uploadOneTimeKeysJob = nullptr;
+ QueryKeysJob* queryKeysJob = nullptr;
+
+ QScopedPointer<Account> olmAccount;
+
+ float signedKeysProportion;
+ float oneTimeKeyThreshold;
+ int targetKeysNumber;
+
+ void updateKeysToUpload();
+ bool oneTimeKeyShouldUpload();
+
+ QHash<QString, int> oneTimeKeyCounts;
+ void setOneTimeKeyCounts(const QHash<QString, int> oneTimeKeyCountsNewValue)
+ {
+ oneTimeKeyCounts = oneTimeKeyCountsNewValue;
+ updateKeysToUpload();
+ }
+ QHash<QString, int> oneTimeKeysToUploadCounts;
+ QHash<QString, int> targetOneTimeKeyCounts;
+
+ // A map from senderKey to InboundSession
+ QMap<QString, InboundSession*> sessions; // TODO: cache
+ void updateDeviceKeys(
+ const QHash<QString,
+ QHash<QString, QueryKeysJob::DeviceInformation>>& deviceKeys)
+ {
+ for (auto userId : deviceKeys.keys()) {
+ for (auto deviceId : deviceKeys.value(userId).keys()) {
+ auto info = deviceKeys.value(userId).value(deviceId);
+ // TODO: ed25519Verify, etc
+ }
+ }
+ }
+ QString sessionDecrypt(Message* message, const QString& senderKey)
+ {
+ QString decrypted;
+ QList<InboundSession*> senderSessions = sessions.values(senderKey);
+ // Try to decrypt message body using one of the known sessions for that
+ // device
+ bool sessionsPassed = false;
+ for (auto senderSession : senderSessions) {
+ if (senderSession == senderSessions.last()) {
+ sessionsPassed = true;
+ }
+ try {
+ decrypted = senderSession->decrypt(message);
+ qCDebug(E2EE)
+ << "Success decrypting Olm event using existing session"
+ << senderSession->id();
+ break;
+ } catch (OlmError* e) {
+ if (message->messageType() == 0) {
+ PreKeyMessage preKeyMessage =
+ PreKeyMessage(message->cipherText());
+ if (senderSession->matches(&preKeyMessage, senderKey)) {
+ // We had a matching session for a pre-key message, but
+ // it didn't work. This means something is wrong, so we
+ // fail now.
+ qCDebug(E2EE)
+ << "Error decrypting pre-key message with existing "
+ "Olm session"
+ << senderSession->id() << "reason:" << e->what();
+ return QString();
+ }
+ }
+ // Simply keep trying otherwise
+ }
+ }
+ if (sessionsPassed || senderSessions.empty()) {
+ if (message->messageType() > 0) {
+ // Not a pre-key message, we should have had a matching session
+ if (!sessions.empty()) {
+ qCDebug(E2EE) << "Error decrypting with existing sessions";
+ return QString();
+ }
+ qCDebug(E2EE) << "No existing sessions";
+ return QString();
+ }
+ // We have a pre-key message without any matching session, in this
+ // case we should try to create one.
+ InboundSession* newSession;
+ qCDebug(E2EE) << "try to establish new InboundSession with" << senderKey;
+ PreKeyMessage preKeyMessage = PreKeyMessage(message->cipherText());
+ try {
+ newSession = new InboundSession(olmAccount.data(),
+ &preKeyMessage,
+ senderKey.toLatin1(), q);
+ } catch (OlmError* e) {
+ qCDebug(E2EE) << "Error decrypting pre-key message when trying "
+ "to establish a new session:"
+ << e->what();
+ return QString();
+ }
+ qCDebug(E2EE) << "Created new Olm session" << newSession->id();
+ try {
+ decrypted = newSession->decrypt(message);
+ } catch (OlmError* e) {
+ qCDebug(E2EE)
+ << "Error decrypting pre-key message with new session"
+ << e->what();
+ return QString();
+ }
+ olmAccount->removeOneTimeKeys(newSession);
+ sessions.insert(senderKey, newSession);
+ }
+ return decrypted;
+ }
+};
+
+EncryptionManager::EncryptionManager(const QByteArray& encryptionAccountPickle,
+ float signedKeysProportion,
+ float oneTimeKeyThreshold, QObject* parent)
+ : QObject(parent)
+ , d(std::make_unique<Private>(std::move(encryptionAccountPickle),
+ std::move(signedKeysProportion),
+ std::move(oneTimeKeyThreshold)))
+{
+ d->q = this;
+}
+
+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 <algorithm>:<device_id>. 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 <algorithm>:<device_id> 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) } } }
+ };
+
+ d->uploadIdentityKeysJob = connection->callApi<UploadKeysJob>(deviceKeys);
+ connect(d->uploadIdentityKeysJob, &BaseJob::success, this, [this] {
+ d->setOneTimeKeyCounts(d->uploadIdentityKeysJob->oneTimeKeyCounts());
+ });
+}
+
+void EncryptionManager::uploadOneTimeKeys(Connection* connection,
+ bool forceUpdate)
+{
+ if (forceUpdate || d->oneTimeKeyCounts.isEmpty()) {
+ d->uploadOneTimeKeysInitJob = connection->callApi<UploadKeysJob>();
+ connect(d->uploadOneTimeKeysInitJob, &BaseJob::success, this, [this] {
+ d->setOneTimeKeyCounts(d->uploadIdentityKeysJob->oneTimeKeyCounts());
+ });
+ }
+
+ int signedKeysToUploadCount =
+ d->oneTimeKeysToUploadCounts.value(SignedCurve25519Key, 0);
+ int unsignedKeysToUploadCount =
+ d->oneTimeKeysToUploadCounts.value(Curve25519Key, 0);
+
+ d->olmAccount->generateOneTimeKeys(signedKeysToUploadCount
+ + unsignedKeysToUploadCount);
+
+ QHash<QString, QVariant> 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() } };
+
+ QByteArray signedMessage = d->olmAccount->sign(message);
+ QJsonObject signatures {
+ { connection->userId(),
+ QJsonObject { { Ed25519Key + QStringLiteral(":")
+ + connection->deviceId(),
+ QString::fromUtf8(signedMessage) } } }
+ };
+ message.insert(QStringLiteral("signatures"), signatures);
+ key = message;
+ keyType = SignedCurve25519Key;
+ } else {
+ key = it.value();
+ keyType = Curve25519Key;
+ }
+ ++oneTimeKeysCounter;
+ oneTimeKeys.insert(QString("%1:%2").arg(keyType).arg(keyId), key);
+ }
+ d->uploadOneTimeKeysJob =
+ connection->callApi<UploadKeysJob>(none, oneTimeKeys);
+ connect(d->uploadOneTimeKeysJob, &BaseJob::success, this, [this] {
+ d->setOneTimeKeyCounts(d->uploadOneTimeKeysJob->oneTimeKeyCounts());
+ });
+ d->olmAccount->markKeysAsPublished();
+ qCDebug(E2EE) << QString("Uploaded new one-time keys: %1 signed, %2 unsigned.")
+ .arg(signedKeysToUploadCount)
+ .arg(unsignedKeysToUploadCount);
+}
+
+void EncryptionManager::updateOneTimeKeyCounts(
+ Connection* connection, const QHash<QString, int>& deviceOneTimeKeysCount)
+{
+ d->oneTimeKeyCounts = deviceOneTimeKeysCount;
+ if (d->oneTimeKeyShouldUpload()) {
+ qCDebug(E2EE) << "Uploading new one-time keys.";
+ uploadOneTimeKeys(connection);
+ }
+}
+
+void Quotient::EncryptionManager::updateDeviceKeys(
+ Connection* connection, const QHash<QString, QStringList>& deviceKeys)
+{
+ d->queryKeysJob = connection->callApi<QueryKeysJob>(deviceKeys);
+ connect(d->queryKeysJob, &BaseJob::success, this,
+ [this] { d->updateDeviceKeys(d->queryKeysJob->deviceKeys()); });
+}
+
+QString EncryptionManager::sessionDecryptMessage(
+ const QJsonObject& personalCipherObject, const QByteArray& senderKey)
+{
+ QString decrypted;
+ int type = personalCipherObject.value(TypeKeyL).toInt(-1);
+ QByteArray body = personalCipherObject.value(BodyKeyL).toString().toLatin1();
+ if (type == 0) {
+ PreKeyMessage preKeyMessage { body };
+ decrypted = d->sessionDecrypt(reinterpret_cast<Message*>(&preKeyMessage),
+ senderKey);
+ } else if (type == 1) {
+ Message message { body };
+ decrypted = d->sessionDecrypt(&message, senderKey);
+ }
+ return decrypted;
+}
+
+QByteArray EncryptionManager::olmAccountPickle()
+{
+ return d->olmAccount->pickle(); // TODO: passphrase even with qtkeychain?
+}
+
+QtOlm::Account* EncryptionManager::account() const
+{
+ return d->olmAccount.data();
+}
+
+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;
+}
+#endif // Quotient_E2EE_ENABLED