diff options
Diffstat (limited to 'lib')
46 files changed, 3087 insertions, 673 deletions
diff --git a/lib/connection.cpp b/lib/connection.cpp index 67bc83f9..66df1e43 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -7,9 +7,6 @@ #include "connection.h" #include "connectiondata.h" -#ifdef Quotient_E2EE_ENABLED -# include "encryptionmanager.h" -#endif // Quotient_E2EE_ENABLED #include "room.h" #include "settings.h" #include "user.h" @@ -38,7 +35,16 @@ #include "jobs/syncjob.h" #ifdef Quotient_E2EE_ENABLED -# include "account.h" // QtOlm +# include "e2ee/qolmaccount.h" +# include "e2ee/qolmutils.h" +# include "database.h" +# include "e2ee/qolminboundsession.h" + +#if QT_VERSION_MAJOR >= 6 +# include <qt6keychain/keychain.h> +#else +# include <qt5keychain/keychain.h> +#endif #endif // Quotient_E2EE_ENABLED #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) @@ -55,6 +61,7 @@ #include <QtCore/QStringBuilder> #include <QtNetwork/QDnsLookup> + using namespace Quotient; // This is very much Qt-specific; STL iterators don't have key() and value() @@ -102,13 +109,30 @@ public: QMetaObject::Connection syncLoopConnection {}; int syncTimeout = -1; +#ifdef Quotient_E2EE_ENABLED + QSet<QString> trackedUsers; + QSet<QString> outdatedUsers; + QHash<QString, QHash<QString, DeviceKeys>> deviceKeys; + QueryKeysJob *currentQueryKeysJob = nullptr; + bool encryptionUpdateRequired = false; + PicklingMode picklingMode = Unencrypted {}; + Database *database = nullptr; + QHash<QString, int> oneTimeKeysCount; + + // A map from SenderKey to vector of InboundSession + UnorderedMap<QString, std::vector<QOlmSessionPtr>> olmSessions; + +#endif + GetCapabilitiesJob* capabilitiesJob = nullptr; GetCapabilitiesJob::Capabilities capabilities; QVector<GetLoginFlowsJob::LoginFlow> loginFlows; #ifdef Quotient_E2EE_ENABLED - QScopedPointer<EncryptionManager> encryptionManager; + std::unique_ptr<QOlmAccount> olmAccount; + bool isUploadingKeys = false; + bool firstSync = true; #endif // Quotient_E2EE_ENABLED QPointer<GetWellknownJob> resolverJob = nullptr; @@ -151,6 +175,7 @@ public: void consumeAccountData(Events&& accountDataEvents); void consumePresenceData(Events&& presenceData); void consumeToDeviceEvents(Events&& toDeviceEvents); + void consumeDevicesList(DevicesList&& devicesList); template <typename EventT> EventT* unpackAccountData() const @@ -181,29 +206,107 @@ public: return q->stateCacheDir().filePath("state.json"); } +#ifdef Quotient_E2EE_ENABLED + void loadSessions() { + olmSessions = q->database()->loadOlmSessions(q->picklingMode()); + } + void saveSession(QOlmSessionPtr& session, const QString &senderKey) { + auto pickleResult = session->pickle(q->picklingMode()); + if (std::holds_alternative<QOlmError>(pickleResult)) { + qCWarning(E2EE) << "Failed to pickle olm session. Error" << std::get<QOlmError>(pickleResult); + return; + } + q->database()->saveOlmSession(senderKey, session->sessionId(), std::get<QByteArray>(pickleResult)); + } + QString sessionDecryptPrekey(const QOlmMessage& message, const QString &senderKey, std::unique_ptr<QOlmAccount>& olmAccount) + { + Q_ASSERT(message.type() == QOlmMessage::PreKey); + for(auto& session : olmSessions[senderKey]) { + const auto matches = session->matchesInboundSessionFrom(senderKey, message); + if(std::holds_alternative<bool>(matches) && std::get<bool>(matches)) { + qCDebug(E2EE) << "Found inbound session"; + const auto result = session->decrypt(message); + if(std::holds_alternative<QString>(result)) { + return std::get<QString>(result); + } else { + qCDebug(E2EE) << "Failed to decrypt prekey message"; + return {}; + } + } + } + qCDebug(E2EE) << "Creating new inbound session"; + auto newSessionResult = olmAccount->createInboundSessionFrom(senderKey.toUtf8(), message); + if(std::holds_alternative<QOlmError>(newSessionResult)) { + qCWarning(E2EE) << "Failed to create inbound session for" << senderKey << std::get<QOlmError>(newSessionResult); + return {}; + } + auto newSession = std::move(std::get<QOlmSessionPtr>(newSessionResult)); + auto error = olmAccount->removeOneTimeKeys(newSession); + if (error) { + qWarning(E2EE) << "Failed to remove one time key for session" << newSession->sessionId(); + } + const auto result = newSession->decrypt(message); + saveSession(newSession, senderKey); + olmSessions[senderKey].push_back(std::move(newSession)); + if(std::holds_alternative<QString>(result)) { + return std::get<QString>(result); + } else { + qCDebug(E2EE) << "Failed to decrypt prekey message with new session"; + return {}; + } + } + QString sessionDecryptGeneral(const QOlmMessage& message, const QString &senderKey) + { + Q_ASSERT(message.type() == QOlmMessage::General); + for(auto& session : olmSessions[senderKey]) { + const auto result = session->decrypt(message); + if(std::holds_alternative<QString>(result)) { + return std::get<QString>(result); + } + } + qCWarning(E2EE) << "Failed to decrypt message"; + return {}; + } + + QString sessionDecryptMessage( + const QJsonObject& personalCipherObject, const QByteArray& senderKey, std::unique_ptr<QOlmAccount>& account) + { + QString decrypted; + int type = personalCipherObject.value(TypeKeyL).toInt(-1); + QByteArray body = personalCipherObject.value(BodyKeyL).toString().toLatin1(); + if (type == 0) { + QOlmMessage preKeyMessage(body, QOlmMessage::PreKey); + decrypted = sessionDecryptPrekey(preKeyMessage, senderKey, account); + } else if (type == 1) { + QOlmMessage message(body, QOlmMessage::General); + decrypted = sessionDecryptGeneral(message, senderKey); + } + return decrypted; + } +#endif + EventPtr sessionDecryptMessage(const EncryptedEvent& encryptedEvent) { #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; return {}; -#else // Quotient_E2EE_ENABLED +#else if (encryptedEvent.algorithm() != OlmV1Curve25519AesSha2AlgoKey) return {}; - const auto identityKey = - encryptionManager->account()->curve25519IdentityKey(); + const auto identityKey = olmAccount->identityKeys().curve25519; const auto personalCipherObject = encryptedEvent.ciphertext(identityKey); if (personalCipherObject.isEmpty()) { qCDebug(E2EE) << "Encrypted event is not for the current device"; return {}; } - const auto decrypted = encryptionManager->sessionDecryptMessage( - personalCipherObject, encryptedEvent.senderKey().toLatin1()); + const auto decrypted = sessionDecryptMessage( + personalCipherObject, encryptedEvent.senderKey().toLatin1(), olmAccount); if (decrypted.isEmpty()) { qCDebug(E2EE) << "Problem with new session from senderKey:" << encryptedEvent.senderKey() - << encryptionManager->account()->oneTimeKeys(); + << olmAccount->oneTimeKeys().keys; return {}; } @@ -220,22 +323,18 @@ public: // TODO: keys to constants const auto decryptedEventObject = decryptedEvent->fullJson(); - const auto recipient = - decryptedEventObject.value("recipient"_ls).toString(); + const auto recipient = decryptedEventObject.value("recipient"_ls).toString(); if (recipient != data->userId()) { qCDebug(E2EE) << "Found user" << recipient << "instead of us" << data->userId() << "in Olm plaintext"; return {}; } - const auto ourKey = - decryptedEventObject.value("recipient_keys"_ls).toObject() - .value(Ed25519Key).toString(); - if (ourKey - != QString::fromUtf8( - encryptionManager->account()->ed25519IdentityKey())) { + const auto ourKey = decryptedEventObject.value("recipient_keys"_ls).toObject() + .value(Ed25519Key).toString(); + if (ourKey != QString::fromUtf8(olmAccount->identityKeys().ed25519)) { qCDebug(E2EE) << "Found key" << ourKey << "instead of ours own ed25519 key" - << encryptionManager->account()->ed25519IdentityKey() + << olmAccount->identityKeys().ed25519 << "in Olm plaintext"; return {}; } @@ -243,12 +342,20 @@ public: return std::move(decryptedEvent); #endif // Quotient_E2EE_ENABLED } +#ifdef Quotient_E2EE_ENABLED + void loadOutdatedUserDevices(); + void saveDevicesList(); + void loadDevicesList(); +#endif }; Connection::Connection(const QUrl& server, QObject* parent) : QObject(parent) , d(makeImpl<Private>(std::make_unique<ConnectionData>(server))) { +#ifdef Quotient_E2EE_ENABLED + //connect(qApp, &QCoreApplication::aboutToQuit, this, &Connection::saveOlmAccount); +#endif d->q = this; // All d initialization should occur before this line } @@ -421,8 +528,7 @@ void Connection::Private::loginToServer(LoginArgTs&&... loginArgs) #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; #else // Quotient_E2EE_ENABLED - encryptionManager->uploadIdentityKeys(q); - encryptionManager->uploadOneTimeKeys(q); + database->clear(); #endif // Quotient_E2EE_ENABLED }); connect(loginJob, &BaseJob::failure, q, [this, loginJob] { @@ -443,11 +549,58 @@ void Connection::Private::completeSetup(const QString& mxId) qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; #else // Quotient_E2EE_ENABLED AccountSettings accountSettings(data->userId()); - encryptionManager.reset( - new EncryptionManager(accountSettings.encryptionAccountPickle())); - if (accountSettings.encryptionAccountPickle().isEmpty()) { - accountSettings.setEncryptionAccountPickle( - encryptionManager->olmAccountPickle()); + + QKeychain::ReadPasswordJob job(qAppName()); + job.setAutoDelete(false); + job.setKey(accountSettings.userId() + QStringLiteral("-Pickle")); + QEventLoop loop; + QKeychain::ReadPasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit); + job.start(); + loop.exec(); + + if (job.error() == QKeychain::Error::EntryNotFound) { + picklingMode = Encrypted { getRandom(128) }; + QKeychain::WritePasswordJob job(qAppName()); + job.setAutoDelete(false); + job.setKey(accountSettings.userId() + QStringLiteral("-Pickle")); + job.setBinaryData(std::get<Encrypted>(picklingMode).key); + QEventLoop loop; + QKeychain::WritePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit); + job.start(); + loop.exec(); + + if (job.error()) { + qCWarning(E2EE) << "Could not save pickling key to keychain: " << job.errorString(); + } + } else if(job.error() != QKeychain::Error::NoError) { + //TODO Error, do something + qCWarning(E2EE) << "Error loading pickling key from keychain:" << job.error(); + } else { + qCDebug(E2EE) << "Successfully loaded pickling key from keychain"; + picklingMode = Encrypted { job.binaryData() }; + } + + database = new Database(data->userId(), data->deviceId(), q); + + // init olmAccount + olmAccount = std::make_unique<QOlmAccount>(data->userId(), data->deviceId(), q); + connect(olmAccount.get(), &QOlmAccount::needsSave, q, &Connection::saveOlmAccount); + +#ifdef Quotient_E2EE_ENABLED + loadSessions(); +#endif + + if (database->accountPickle().isEmpty()) { + // create new account and save unpickle data + olmAccount->createNewAccount(); + auto job = q->callApi<UploadKeysJob>(olmAccount->deviceKeys()); + connect(job, &BaseJob::failure, q, [job]{ + qCWarning(E2EE) << "Failed to upload device keys:" << job->errorString(); + }); + } else { + // account already existing + auto pickle = database->accountPickle(); + olmAccount->unpickle(pickle, picklingMode); } #endif // Quotient_E2EE_ENABLED emit q->stateChanged(); @@ -602,24 +755,39 @@ QJsonObject toJson(const DirectChatsMap& directChats) void Connection::onSyncSuccess(SyncData&& data, bool fromCache) { +#ifdef Quotient_E2EE_ENABLED + d->oneTimeKeysCount = data.deviceOneTimeKeysCount(); + if (d->oneTimeKeysCount[SignedCurve25519Key] < 0.4 * d->olmAccount->maxNumberOfOneTimeKeys() + && !d->isUploadingKeys) { + d->isUploadingKeys = true; + d->olmAccount->generateOneTimeKeys( + d->olmAccount->maxNumberOfOneTimeKeys() / 2 - d->oneTimeKeysCount[SignedCurve25519Key]); + auto keys = d->olmAccount->oneTimeKeys(); + auto job = d->olmAccount->createUploadKeyRequest(keys); + run(job, ForegroundRequest); + connect(job, &BaseJob::success, this, + [this] { d->olmAccount->markKeysAsPublished(); }); + connect(job, &BaseJob::result, this, + [this] { d->isUploadingKeys = false; }); + } + if(d->firstSync) { + d->loadDevicesList(); + d->firstSync = false; + } + + d->consumeDevicesList(data.takeDevicesList()); +#endif // Quotient_E2EE_ENABLED d->data->setLastEvent(data.nextBatch()); d->consumeRoomData(data.takeRoomData(), fromCache); d->consumeAccountData(data.takeAccountData()); d->consumePresenceData(data.takePresenceData()); d->consumeToDeviceEvents(data.takeToDeviceEvents()); #ifdef Quotient_E2EE_ENABLED - // handling device_one_time_keys_count - if (!d->encryptionManager) - { - qCDebug(E2EE) << "Encryption manager is not there yet, updating " - "one-time key counts will be skipped"; - return; + if(d->encryptionUpdateRequired) { + d->loadOutdatedUserDevices(); + d->encryptionUpdateRequired = false; } - if (const auto deviceOneTimeKeysCount = data.deviceOneTimeKeysCount(); - !deviceOneTimeKeysCount.isEmpty()) - d->encryptionManager->updateOneTimeKeyCounts(this, - deviceOneTimeKeysCount); -#endif // Quotient_E2EE_ENABLED +#endif } void Connection::Private::consumeRoomData(SyncDataList&& roomDataList, @@ -747,34 +915,55 @@ void Connection::Private::consumePresenceData(Events&& presenceData) void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) { #ifdef Quotient_E2EE_ENABLED - // handling m.room_key to-device encrypted event - visitEach(toDeviceEvents, [this](const EncryptedEvent& ee) { - if (ee.algorithm() != OlmV1Curve25519AesSha2AlgoKey) { - qCDebug(E2EE) << "Encrypted event" << ee.id() << "algorithm" - << ee.algorithm() << "is not supported"; - return; - } + if (!toDeviceEvents.empty()) { + qCDebug(E2EE) << "Consuming" << toDeviceEvents.size() << "to-device events"; + visitEach(toDeviceEvents, [this](const EncryptedEvent& event) { + if (event.algorithm() != OlmV1Curve25519AesSha2AlgoKey) { + qCDebug(E2EE) << "Unsupported algorithm" << event.id() << "for event" << event.algorithm(); + return; + } + const auto decryptedEvent = sessionDecryptMessage(event); + if(!decryptedEvent) { + qCWarning(E2EE) << "Failed to decrypt event" << event.id(); + return; + } - // TODO: full maintaining of the device keys - // with device_lists sync extention and /keys/query - qCDebug(E2EE) << "Getting device keys for the m.room_key sender:" - << ee.senderId(); - // encryptionManager->updateDeviceKeys(); - - switchOnType(*sessionDecryptMessage(ee), - [this, senderKey = ee.senderKey()](const RoomKeyEvent& roomKeyEvent) { - if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) - detectedRoom->handleRoomKeyEvent(roomKeyEvent, senderKey); - else - qCDebug(E2EE) - << "Encrypted event room id" << roomKeyEvent.roomId() - << "is not found at the connection" << q->objectName(); - }, - [](const Event& evt) { - qCDebug(E2EE) << "Skipping encrypted to_device event, type" - << evt.matrixType(); - }); - }); + switchOnType(*decryptedEvent, + [this, senderKey = event.senderKey()](const RoomKeyEvent& roomKeyEvent) { + if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) { + detectedRoom->handleRoomKeyEvent(roomKeyEvent, senderKey); + } else { + qCDebug(E2EE) << "Encrypted event room id" << roomKeyEvent.roomId() + << "is not found at the connection" << q->objectName(); + } + }, + [](const Event& evt) { + qCDebug(E2EE) << "Skipping encrypted to_device event, type" + << evt.matrixType(); + }); + }); + } +#endif +} + +void Connection::Private::consumeDevicesList(DevicesList&& devicesList) +{ +#ifdef Quotient_E2EE_ENABLED + bool hasNewOutdatedUser = false; + for(const auto &changed : devicesList.changed) { + if(trackedUsers.contains(changed)) { + outdatedUsers += changed; + hasNewOutdatedUser = true; + } + } + for(const auto &left : devicesList.left) { + trackedUsers -= left; + outdatedUsers -= left; + deviceKeys.remove(left); + } + if(hasNewOutdatedUser) { + loadOutdatedUserDevices(); + } #endif } @@ -914,6 +1103,19 @@ DownloadFileJob* Connection::downloadFile(const QUrl& url, return job; } +#ifdef Quotient_E2EE_ENABLED +DownloadFileJob* Connection::downloadFile(const QUrl& url, + const EncryptedFile& file, + const QString& localFilename) +{ + auto mediaId = url.authority() + url.path(); + auto idParts = splitMediaId(mediaId); + auto* job = + callApi<DownloadFileJob>(idParts.front(), idParts.back(), file, localFilename); + return job; +} +#endif + CreateRoomJob* Connection::createRoom(RoomVisibility visibility, const QString& alias, const QString& name, const QString& topic, @@ -1227,9 +1429,9 @@ QByteArray Connection::accessToken() const bool Connection::isLoggedIn() const { return !accessToken().isEmpty(); } #ifdef Quotient_E2EE_ENABLED -QtOlm::Account* Connection::olmAccount() const +QOlmAccount *Connection::olmAccount() const { - return d->encryptionManager->account(); + return d->olmAccount.get(); } #endif // Quotient_E2EE_ENABLED @@ -1627,6 +1829,12 @@ void Connection::saveState() const QJsonObject { { QStringLiteral("events"), accountDataEvents } }); } +#ifdef Quotient_E2EE_ENABLED + { + QJsonObject keysJson = toJson(d->oneTimeKeysCount); + rootObj.insert(QStringLiteral("device_one_time_keys_count"), keysJson); + } +#endif #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) const auto data = @@ -1772,3 +1980,178 @@ QVector<Connection::SupportedRoomVersion> Connection::availableRoomVersions() co } return result; } + +#ifdef Quotient_E2EE_ENABLED +void Connection::Private::loadOutdatedUserDevices() +{ + QHash<QString, QStringList> users; + for(const auto &user : outdatedUsers) { + users[user] += QStringList(); + } + if(currentQueryKeysJob) { + currentQueryKeysJob->abandon(); + currentQueryKeysJob = nullptr; + } + auto queryKeysJob = q->callApi<QueryKeysJob>(users); + currentQueryKeysJob = queryKeysJob; + connect(queryKeysJob, &BaseJob::success, q, [this, queryKeysJob](){ + currentQueryKeysJob = nullptr; + const auto data = queryKeysJob->deviceKeys(); + for(const auto &[user, keys] : asKeyValueRange(data)) { + deviceKeys[user].clear(); + for(const auto &device : keys) { + if(device.userId != user) { + qCWarning(E2EE) + << "mxId mismatch during device key verification:" + << device.userId << user; + continue; + } + if (!std::all_of(device.algorithms.cbegin(), + device.algorithms.cend(), + isSupportedAlgorithm)) { + qCWarning(E2EE) << "Unsupported encryption algorithms found" + << device.algorithms; + continue; + } + if (!verifyIdentitySignature(device, device.deviceId, + device.userId)) { + qCWarning(E2EE) << "Failed to verify devicekeys signature. " + "Skipping this device"; + continue; + } + deviceKeys[user][device.deviceId] = device; + } + outdatedUsers -= user; + } + saveDevicesList(); + }); +} + +void Connection::Private::saveDevicesList() +{ + q->database()->transaction(); + auto query = q->database()->prepareQuery( + QStringLiteral("DELETE FROM tracked_users")); + q->database()->execute(query); + query.prepare(QStringLiteral( + "INSERT INTO tracked_users(matrixId) VALUES(:matrixId);")); + for (const auto& user : trackedUsers) { + query.bindValue(":matrixId", user); + q->database()->execute(query); + } + + query.prepare(QStringLiteral("DELETE FROM outdated_users")); + q->database()->execute(query); + query.prepare(QStringLiteral( + "INSERT INTO outdated_users(matrixId) VALUES(:matrixId);")); + for (const auto& user : outdatedUsers) { + query.bindValue(":matrixId", user); + q->database()->execute(query); + } + + query.prepare(QStringLiteral( + "INSERT INTO tracked_devices" + "(matrixId, deviceId, curveKeyId, curveKey, edKeyId, edKey) " + "VALUES(:matrixId, :deviceId, :curveKeyId, :curveKey, :edKeyId, :edKey);" + )); + for (const auto& user : deviceKeys.keys()) { + for (const auto& device : deviceKeys[user]) { + auto keys = device.keys.keys(); + auto curveKeyId = keys[0].startsWith(QLatin1String("curve")) ? keys[0] : keys[1]; + auto edKeyId = keys[0].startsWith(QLatin1String("ed")) ? keys[0] : keys[1]; + + query.bindValue(":matrixId", user); + query.bindValue(":deviceId", device.deviceId); + query.bindValue(":curveKeyId", curveKeyId); + query.bindValue(":curveKey", device.keys[curveKeyId]); + query.bindValue(":edKeyId", edKeyId); + query.bindValue(":edKey", device.keys[edKeyId]); + + q->database()->execute(query); + } + } + q->database()->commit(); +} + +void Connection::Private::loadDevicesList() +{ + auto query = q->database()->prepareQuery(QStringLiteral("SELECT * FROM tracked_users;")); + q->database()->execute(query); + while(query.next()) { + trackedUsers += query.value(0).toString(); + } + + query = q->database()->prepareQuery(QStringLiteral("SELECT * FROM outdated_users;")); + q->database()->execute(query); + while(query.next()) { + outdatedUsers += query.value(0).toString(); + } + + query = q->database()->prepareQuery(QStringLiteral("SELECT * FROM tracked_devices;")); + q->database()->execute(query); + while(query.next()) { + deviceKeys[query.value("matrixId").toString()][query.value("deviceId").toString()] = DeviceKeys { + query.value("matrixId").toString(), + query.value("deviceId").toString(), + { "m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"}, + {{query.value("curveKeyId").toString(), query.value("curveKey").toString()}, + {query.value("edKeyId").toString(), query.value("edKey").toString()}}, + {} // Signatures are not saved/loaded as they are not needed after initial validation + }; + } + +} + +void Connection::encryptionUpdate(Room *room) +{ + for(const auto &user : room->users()) { + if(!d->trackedUsers.contains(user->id())) { + d->trackedUsers += user->id(); + d->outdatedUsers += user->id(); + d->encryptionUpdateRequired = true; + } + } +} + +PicklingMode Connection::picklingMode() const +{ + return d->picklingMode; +} +#endif + +void Connection::saveOlmAccount() +{ + qCDebug(E2EE) << "Saving olm account"; +#ifdef Quotient_E2EE_ENABLED + auto pickle = d->olmAccount->pickle(d->picklingMode); + d->database->setAccountPickle(std::get<QByteArray>(pickle)); +#endif +} + +#ifdef Quotient_E2EE_ENABLED +QJsonObject Connection::decryptNotification(const QJsonObject ¬ification) +{ + auto room = this->room(notification["room_id"].toString()); + auto event = makeEvent<EncryptedEvent>(notification["event"].toObject()); + auto decrypted = room->decryptMessage(*event); + if(!decrypted) { + return QJsonObject(); + } + return decrypted->fullJson(); +} + +Database* Connection::database() +{ + return d->database; +} + +UnorderedMap<std::pair<QString, QString>, QOlmInboundGroupSessionPtr> Connection::loadRoomMegolmSessions(Room* room) +{ + return database()->loadMegolmSessions(room->id(), picklingMode()); +} + +void Connection::saveMegolmSession(Room* room, const QString& senderKey, QOlmInboundGroupSession* session) +{ + database()->saveMegolmSession(room->id(), senderKey, session->sessionId(), session->pickle(picklingMode())); +} +#endif diff --git a/lib/connection.h b/lib/connection.h index dc2eaad1..165d8d68 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -22,9 +22,9 @@ #include <functional> -namespace QtOlm { -class Account; -} +#ifdef Quotient_E2EE_ENABLED +#include "e2ee/e2ee.h" +#endif Q_DECLARE_METATYPE(Quotient::GetLoginFlowsJob::LoginFlow) @@ -48,6 +48,11 @@ class DownloadFileJob; class SendToDeviceJob; class SendMessageJob; class LeaveRoomJob; +class Database; +struct EncryptedFile; + +class QOlmAccount; +class QOlmInboundGroupSession; using LoginFlow = GetLoginFlowsJob::LoginFlow; @@ -310,7 +315,10 @@ public: QByteArray accessToken() const; bool isLoggedIn() const; #ifdef Quotient_E2EE_ENABLED - QtOlm::Account* olmAccount() const; + QOlmAccount* olmAccount() const; + Database* database(); + UnorderedMap<std::pair<QString, QString>, QOlmInboundGroupSessionPtr> loadRoomMegolmSessions(Room* room); + void saveMegolmSession(Room* room, const QString& senderKey, QOlmInboundGroupSession* session); #endif // Quotient_E2EE_ENABLED Q_INVOKABLE Quotient::SyncJob* syncJob() const; Q_INVOKABLE int millisToReconnect() const; @@ -489,6 +497,9 @@ public: setUserFactory(defaultUserFactory<T>); } + /// Saves the olm account data to disk. Usually doesn't need to be called manually. + void saveOlmAccount(); + public Q_SLOTS: /// \brief Set the homeserver base URL and retrieve its login flows /// @@ -576,6 +587,10 @@ public Q_SLOTS: DownloadFileJob* downloadFile(const QUrl& url, const QString& localFilename = {}); +#ifdef Quotient_E2EE_ENABLED + DownloadFileJob* downloadFile(const QUrl& url, const EncryptedFile& file, + const QString& localFilename = {}); +#endif /** * \brief Create a room (generic method) * This method allows to customize room entirely to your liking, @@ -661,6 +676,11 @@ public Q_SLOTS: /** \deprecated Do not use this directly, use Room::leaveRoom() instead */ virtual LeaveRoomJob* leaveRoom(Room* room); +#ifdef Quotient_E2EE_ENABLED + void encryptionUpdate(Room *room); + PicklingMode picklingMode() const; + QJsonObject decryptNotification(const QJsonObject ¬ification); +#endif Q_SIGNALS: /// \brief Initial server resolution has failed /// diff --git a/lib/database.cpp b/lib/database.cpp new file mode 100644 index 00000000..84c93046 --- /dev/null +++ b/lib/database.cpp @@ -0,0 +1,244 @@ +// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "database.h" + +#include <QtSql/QSqlDatabase> +#include <QtSql/QSqlQuery> +#include <QtSql/QSqlError> +#include <QtCore/QStandardPaths> +#include <QtCore/QDebug> +#include <QtCore/QDir> + +#include "e2ee/e2ee.h" +#include "e2ee/qolmsession.h" +#include "e2ee/qolminboundsession.h" + +using namespace Quotient; +Database::Database(const QString& matrixId, const QString& deviceId, QObject* parent) + : QObject(parent) + , m_matrixId(matrixId) +{ + m_matrixId.replace(':', '_'); + QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), QStringLiteral("Quotient_%1").arg(m_matrixId)); + QString databasePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/%1").arg(m_matrixId); + QDir(databasePath).mkpath(databasePath); + database().setDatabaseName(databasePath + QStringLiteral("/quotient_%1.db3").arg(deviceId)); + database().open(); + + switch(version()) { + case 0: migrateTo1(); + } +} + +int Database::version() +{ + auto query = execute(QStringLiteral("PRAGMA user_version;")); + if (query.next()) { + bool ok; + int value = query.value(0).toInt(&ok); + qCDebug(DATABASE) << "Database version" << value; + if (ok) + return value; + } else { + qCritical() << "Failed to check database version"; + } + return -1; +} + +QSqlQuery Database::execute(const QString &queryString) +{ + auto query = database().exec(queryString); + if (query.lastError().type() != QSqlError::NoError) { + qCritical() << "Failed to execute query"; + qCritical() << query.lastQuery(); + qCritical() << query.lastError(); + } + return query; +} + +QSqlQuery Database::execute(QSqlQuery &query) +{ + if (!query.exec()) { + qCritical() << "Failed to execute query"; + qCritical() << query.lastQuery(); + qCritical() << query.lastError(); + } + return query; +} + +void Database::transaction() +{ + database().transaction(); +} + +void Database::commit() +{ + database().commit(); +} + +void Database::migrateTo1() +{ + qCDebug(DATABASE) << "Migrating database to version 1"; + transaction(); + execute(QStringLiteral("CREATE TABLE accounts (pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE olm_sessions (senderKey TEXT, sessionId TEXT, pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE inbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE outbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE group_session_record_index (roomId TEXT, sessionId TEXT, i INTEGER, eventId TEXT, ts INTEGER);")); + execute(QStringLiteral("CREATE TABLE tracked_users (matrixId TEXT);")); + execute(QStringLiteral("CREATE TABLE outdated_users (matrixId TEXT);")); + execute(QStringLiteral("CREATE TABLE tracked_devices (matrixId TEXT, deviceId TEXT, curveKeyId TEXT, curveKey TEXT, edKeyId TEXT, edKey TEXT);")); + + execute(QStringLiteral("PRAGMA user_version = 1;")); + commit(); +} + +QByteArray Database::accountPickle() +{ + auto query = prepareQuery(QStringLiteral("SELECT pickle FROM accounts;")); + execute(query); + if (query.next()) { + return query.value(QStringLiteral("pickle")).toByteArray(); + } + return {}; +} + +void Database::setAccountPickle(const QByteArray &pickle) +{ + auto deleteQuery = prepareQuery(QStringLiteral("DELETE FROM accounts;")); + auto query = prepareQuery(QStringLiteral("INSERT INTO accounts(pickle) VALUES(:pickle);")); + query.bindValue(":pickle", pickle); + transaction(); + execute(deleteQuery); + execute(query); + commit(); +} + +void Database::clear() +{ + auto query = prepareQuery(QStringLiteral("DELETE FROM accounts;")); + auto sessionsQuery = prepareQuery(QStringLiteral("DELETE FROM olm_sessions;")); + auto megolmSessionsQuery = prepareQuery(QStringLiteral("DELETE FROM inbound_megolm_sessions;")); + auto groupSessionIndexRecordQuery = prepareQuery(QStringLiteral("DELETE FROM group_session_record_index;")); + + transaction(); + execute(query); + execute(sessionsQuery); + execute(megolmSessionsQuery); + execute(groupSessionIndexRecordQuery); + commit(); + +} + +void Database::saveOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray &pickle) +{ + auto query = prepareQuery(QStringLiteral("INSERT INTO olm_sessions(senderKey, sessionId, pickle) VALUES(:senderKey, :sessionId, :pickle);")); + query.bindValue(":senderKey", senderKey); + query.bindValue(":sessionId", sessionId); + query.bindValue(":pickle", pickle); + transaction(); + execute(query); + commit(); +} + +UnorderedMap<QString, std::vector<QOlmSessionPtr>> Database::loadOlmSessions(const PicklingMode& picklingMode) +{ + auto query = prepareQuery(QStringLiteral("SELECT * FROM olm_sessions;")); + transaction(); + execute(query); + commit(); + UnorderedMap<QString, std::vector<QOlmSessionPtr>> sessions; + while (query.next()) { + auto session = QOlmSession::unpickle(query.value("pickle").toByteArray(), picklingMode); + if (std::holds_alternative<QOlmError>(session)) { + qCWarning(E2EE) << "Failed to unpickle olm session"; + continue; + } + sessions[query.value("senderKey").toString()].push_back(std::move(std::get<QOlmSessionPtr>(session))); + } + return sessions; +} + +UnorderedMap<std::pair<QString, QString>, QOlmInboundGroupSessionPtr> Database::loadMegolmSessions(const QString& roomId, const PicklingMode& picklingMode) +{ + auto query = prepareQuery(QStringLiteral("SELECT * FROM inbound_megolm_sessions WHERE roomId=:roomId;")); + query.bindValue(":roomId", roomId); + transaction(); + execute(query); + commit(); + UnorderedMap<std::pair<QString, QString>, QOlmInboundGroupSessionPtr> sessions; + while (query.next()) { + auto session = QOlmInboundGroupSession::unpickle(query.value("pickle").toByteArray(), picklingMode); + if (std::holds_alternative<QOlmError>(session)) { + qCWarning(E2EE) << "Failed to unpickle megolm session"; + continue; + } + sessions[{query.value("senderKey").toString(), query.value("sessionId").toString()}] = std::move(std::get<QOlmInboundGroupSessionPtr>(session)); + } + return sessions; +} + +void Database::saveMegolmSession(const QString& roomId, const QString& senderKey, const QString& sessionId, const QByteArray& pickle) +{ + auto query = prepareQuery(QStringLiteral("INSERT INTO inbound_megolm_sessions(roomId, senderKey, sessionId, pickle) VALUES(:roomId, :senderKey, :sessionId, :pickle);")); + query.bindValue(":roomId", roomId); + query.bindValue(":senderKey", senderKey); + query.bindValue(":sessionId", sessionId); + query.bindValue(":pickle", pickle); + transaction(); + execute(query); + commit(); +} + +void Database::addGroupSessionIndexRecord(const QString& roomId, const QString& sessionId, uint32_t index, const QString& eventId, qint64 ts) +{ + auto query = prepareQuery("INSERT INTO group_session_record_index(roomId, sessionId, i, eventId, ts) VALUES(:roomId, :sessionId, :index, :eventId, :ts);"); + query.bindValue(":roomId", roomId); + query.bindValue(":sessionId", sessionId); + query.bindValue(":index", index); + query.bindValue(":eventId", eventId); + query.bindValue(":ts", ts); + transaction(); + execute(query); + commit(); +} + +std::pair<QString, qint64> Database::groupSessionIndexRecord(const QString& roomId, const QString& sessionId, qint64 index) +{ + auto query = prepareQuery(QStringLiteral("SELECT * FROM group_session_record_index WHERE roomId=:roomId AND sessionId=:sessionId AND i=:index;")); + query.bindValue(":roomId", roomId); + query.bindValue(":sessionId", sessionId); + query.bindValue(":index", index); + transaction(); + execute(query); + commit(); + if (!query.next()) { + return {}; + } + return {query.value("eventId").toString(), query.value("ts").toLongLong()}; +} + +QSqlDatabase Database::database() +{ + return QSqlDatabase::database(QStringLiteral("Quotient_%1").arg(m_matrixId)); +} + +QSqlQuery Database::prepareQuery(const QString& queryString) +{ + QSqlQuery query(database()); + query.prepare(queryString); + return query; +} + +void Database::clearRoomData(const QString& roomId) +{ + auto query = prepareQuery(QStringLiteral("DELETE FROM inbound_megolm_sessions WHERE roomId=:roomId;")); + auto query2 = prepareQuery(QStringLiteral("DELETE FROM outbound_megolm_sessions WHERE roomId=:roomId;")); + auto query3 = prepareQuery(QStringLiteral("DELETE FROM group_session_record_index WHERE roomId=:roomId;")); + transaction(); + execute(query); + execute(query2); + execute(query3); + commit(); +} diff --git a/lib/database.h b/lib/database.h new file mode 100644 index 00000000..d4d5fb56 --- /dev/null +++ b/lib/database.h @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include <QtCore/QObject> +#include <QtSql/QSqlQuery> +#include <QtCore/QVector> + +#include "e2ee/e2ee.h" + +namespace Quotient { +class QUOTIENT_API Database : public QObject +{ + Q_OBJECT +public: + Database(const QString& matrixId, const QString& deviceId, QObject* parent); + + int version(); + void transaction(); + void commit(); + QSqlQuery execute(const QString &queryString); + QSqlQuery execute(QSqlQuery &query); + QSqlDatabase database(); + QSqlQuery prepareQuery(const QString& quaryString); + + QByteArray accountPickle(); + void setAccountPickle(const QByteArray &pickle); + void clear(); + void saveOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray &pickle); + UnorderedMap<QString, std::vector<QOlmSessionPtr>> loadOlmSessions(const PicklingMode& picklingMode); + UnorderedMap<std::pair<QString, QString>, QOlmInboundGroupSessionPtr> loadMegolmSessions(const QString& roomId, const PicklingMode& picklingMode); + void saveMegolmSession(const QString& roomId, const QString& senderKey, const QString& sessionKey, const QByteArray& pickle); + void addGroupSessionIndexRecord(const QString& roomId, const QString& sessionId, uint32_t index, const QString& eventId, qint64 ts); + std::pair<QString, qint64> groupSessionIndexRecord(const QString& roomId, const QString& sessionId, qint64 index); + void clearRoomData(const QString& roomId); + +private: + void migrateTo1(); + QString m_matrixId; +}; +} diff --git a/lib/e2ee.h b/lib/e2ee.h deleted file mode 100644 index 4044aa02..00000000 --- a/lib/e2ee.h +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> -// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net> -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include "util.h" - -#include <QtCore/QStringList> - -namespace Quotient { -inline const auto CiphertextKeyL = "ciphertext"_ls; -inline const auto SenderKeyKeyL = "sender_key"_ls; -inline const auto DeviceIdKeyL = "device_id"_ls; -inline const auto SessionIdKeyL = "session_id"_ls; - -inline const auto AlgorithmKeyL = "algorithm"_ls; -inline const auto RotationPeriodMsKeyL = "rotation_period_ms"_ls; -inline const auto RotationPeriodMsgsKeyL = "rotation_period_msgs"_ls; - -inline const auto AlgorithmKey = QStringLiteral("algorithm"); -inline const auto RotationPeriodMsKey = QStringLiteral("rotation_period_ms"); -inline const auto RotationPeriodMsgsKey = - QStringLiteral("rotation_period_msgs"); - -inline const auto Ed25519Key = QStringLiteral("ed25519"); -inline const auto Curve25519Key = QStringLiteral("curve25519"); -inline const auto SignedCurve25519Key = QStringLiteral("signed_curve25519"); -inline const auto OlmV1Curve25519AesSha2AlgoKey = - QStringLiteral("m.olm.v1.curve25519-aes-sha2"); -inline const auto MegolmV1AesSha2AlgoKey = - QStringLiteral("m.megolm.v1.aes-sha2"); -inline const QStringList SupportedAlgorithms = { OlmV1Curve25519AesSha2AlgoKey, - MegolmV1AesSha2AlgoKey }; -} // namespace Quotient diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h new file mode 100644 index 00000000..268cb525 --- /dev/null +++ b/lib/e2ee/e2ee.h @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> +// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "converters.h" +#include "quotient_common.h" + +#include <QtCore/QMetaType> +#include <variant> + +namespace Quotient { + +constexpr auto CiphertextKeyL = "ciphertext"_ls; +constexpr auto SenderKeyKeyL = "sender_key"_ls; +constexpr auto DeviceIdKeyL = "device_id"_ls; +constexpr auto SessionIdKeyL = "session_id"_ls; + +constexpr auto AlgorithmKeyL = "algorithm"_ls; +constexpr auto RotationPeriodMsKeyL = "rotation_period_ms"_ls; +constexpr auto RotationPeriodMsgsKeyL = "rotation_period_msgs"_ls; + +constexpr auto AlgorithmKey = "algorithm"_ls; +constexpr auto RotationPeriodMsKey = "rotation_period_ms"_ls; +constexpr auto RotationPeriodMsgsKey = "rotation_period_msgs"_ls; + +constexpr auto Ed25519Key = "ed25519"_ls; +constexpr auto Curve25519Key = "curve25519"_ls; +constexpr auto SignedCurve25519Key = "signed_curve25519"_ls; + +constexpr auto OlmV1Curve25519AesSha2AlgoKey = "m.olm.v1.curve25519-aes-sha2"_ls; +constexpr auto MegolmV1AesSha2AlgoKey = "m.megolm.v1.aes-sha2"_ls; + +inline bool isSupportedAlgorithm(const QString& algorithm) +{ + static constexpr auto SupportedAlgorithms = + make_array(OlmV1Curve25519AesSha2AlgoKey, MegolmV1AesSha2AlgoKey); + return std::find(SupportedAlgorithms.cbegin(), SupportedAlgorithms.cend(), + algorithm) + != SupportedAlgorithms.cend(); +} + +struct Unencrypted {}; +struct Encrypted { + QByteArray key; +}; + +using PicklingMode = std::variant<Unencrypted, Encrypted>; + +class QOlmSession; +using QOlmSessionPtr = std::unique_ptr<QOlmSession>; + +class QOlmInboundGroupSession; +using QOlmInboundGroupSessionPtr = std::unique_ptr<QOlmInboundGroupSession>; + +struct IdentityKeys +{ + QByteArray curve25519; + QByteArray ed25519; +}; + +//! Struct representing the one-time keys. +struct QUOTIENT_API OneTimeKeys +{ + QHash<QString, QHash<QString, QString>> keys; + + //! Get the HashMap containing the curve25519 one-time keys. + QHash<QString, QString> curve25519() const; + + //! Get a reference to the hashmap corresponding to given key type. +// std::optional<QHash<QString, QString>> get(QString keyType) const; +}; + +//! Struct representing the signed one-time keys. +class SignedOneTimeKey +{ +public: + //! Required. The unpadded Base64-encoded 32-byte Curve25519 public key. + QString key; + + //! Required. Signatures of the key object. + //! The signature is calculated using the process described at Signing JSON. + QHash<QString, QHash<QString, QString>> signatures; +}; + + +template <> +struct JsonObjectConverter<SignedOneTimeKey> { + static void fillFrom(const QJsonObject& jo, SignedOneTimeKey& result) + { + fromJson(jo.value("key"_ls), result.key); + fromJson(jo.value("signatures"_ls), result.signatures); + } + + static void dumpTo(QJsonObject &jo, const SignedOneTimeKey &result) + { + addParam<>(jo, QStringLiteral("key"), result.key); + addParam<>(jo, QStringLiteral("signatures"), result.signatures); + } +}; + +template <typename T> +class asKeyValueRange +{ +public: + asKeyValueRange(T& data) + : m_data { data } + {} + + auto begin() { return m_data.keyValueBegin(); } + auto end() { return m_data.keyValueEnd(); } + +private: + T &m_data; +}; +template <typename T> +asKeyValueRange(T&) -> asKeyValueRange<T>; + +} // namespace Quotient + +Q_DECLARE_METATYPE(Quotient::SignedOneTimeKey) diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp new file mode 100644 index 00000000..476a60bd --- /dev/null +++ b/lib/e2ee/qolmaccount.cpp @@ -0,0 +1,301 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "qolmaccount.h" + +#include "connection.h" +#include "e2ee/qolmutility.h" +#include "e2ee/qolmutils.h" + +#include "csapi/keys.h" + +#include <QtCore/QRandomGenerator> + +using namespace Quotient; + +QHash<QString, QString> OneTimeKeys::curve25519() const +{ + return keys[Curve25519Key]; +} + +//std::optional<QHash<QString, QString>> OneTimeKeys::get(QString keyType) const +//{ +// if (!keys.contains(keyType)) { +// return std::nullopt; +// } +// return keys[keyType]; +//} + +// Convert olm error to enum +QOlmError lastError(OlmAccount *account) { + return fromString(olm_account_last_error(account)); +} + +QOlmAccount::QOlmAccount(const QString& userId, const QString& deviceId, + QObject* parent) + : QObject(parent) + , m_userId(userId) + , m_deviceId(deviceId) +{} + +QOlmAccount::~QOlmAccount() +{ + olm_clear_account(m_account); + delete[](reinterpret_cast<uint8_t *>(m_account)); +} + +void QOlmAccount::createNewAccount() +{ + m_account = olm_account(new uint8_t[olm_account_size()]); + size_t randomSize = olm_create_account_random_length(m_account); + QByteArray randomData = getRandom(randomSize); + const auto error = olm_create_account(m_account, randomData.data(), randomSize); + if (error == olm_error()) { + throw lastError(m_account); + } + emit needsSave(); +} + +void QOlmAccount::unpickle(QByteArray &pickled, const PicklingMode &mode) +{ + m_account = olm_account(new uint8_t[olm_account_size()]); + const QByteArray key = toKey(mode); + const auto error = olm_unpickle_account(m_account, key.data(), key.length(), pickled.data(), pickled.size()); + if (error == olm_error()) { + qCWarning(E2EE) << "Failed to unpickle olm account"; + //TODO: Do something that is not dying + // Probably log the user out since we have no way of getting to the keys + //throw lastError(m_account); + } +} + +std::variant<QByteArray, QOlmError> QOlmAccount::pickle(const PicklingMode &mode) +{ + const QByteArray key = toKey(mode); + const size_t pickleLength = olm_pickle_account_length(m_account); + QByteArray pickleBuffer(pickleLength, '0'); + const auto error = olm_pickle_account(m_account, key.data(), + key.length(), pickleBuffer.data(), pickleLength); + if (error == olm_error()) { + return lastError(m_account); + } + return pickleBuffer; +} + +IdentityKeys QOlmAccount::identityKeys() const +{ + const size_t keyLength = olm_account_identity_keys_length(m_account); + QByteArray keyBuffer(keyLength, '0'); + const auto error = olm_account_identity_keys(m_account, keyBuffer.data(), keyLength); + if (error == olm_error()) { + throw lastError(m_account); + } + const QJsonObject key = QJsonDocument::fromJson(keyBuffer).object(); + return IdentityKeys { + key.value(QStringLiteral("curve25519")).toString().toUtf8(), + key.value(QStringLiteral("ed25519")).toString().toUtf8() + }; +} + +QByteArray QOlmAccount::sign(const QByteArray &message) const +{ + QByteArray signatureBuffer(olm_account_signature_length(m_account), '0'); + + const auto error = olm_account_sign(m_account, message.data(), message.length(), + signatureBuffer.data(), signatureBuffer.length()); + + if (error == olm_error()) { + throw lastError(m_account); + } + return signatureBuffer; +} + +QByteArray QOlmAccount::sign(const QJsonObject &message) const +{ + return sign(QJsonDocument(message).toJson(QJsonDocument::Compact)); +} + +QByteArray QOlmAccount::signIdentityKeys() const +{ + const auto keys = identityKeys(); + QJsonObject body + { + {"algorithms", QJsonArray{"m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"}}, + {"user_id", m_userId}, + {"device_id", m_deviceId}, + {"keys", + QJsonObject{ + {QStringLiteral("curve25519:") + m_deviceId, QString::fromUtf8(keys.curve25519)}, + {QStringLiteral("ed25519:") + m_deviceId, QString::fromUtf8(keys.ed25519)} + } + } + }; + return sign(QJsonDocument(body).toJson(QJsonDocument::Compact)); + +} + +size_t QOlmAccount::maxNumberOfOneTimeKeys() const +{ + return olm_account_max_number_of_one_time_keys(m_account); +} + +size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) const +{ + const size_t randomLength = olm_account_generate_one_time_keys_random_length(m_account, numberOfKeys); + QByteArray randomBuffer = getRandom(randomLength); + const auto error = olm_account_generate_one_time_keys(m_account, numberOfKeys, randomBuffer.data(), randomLength); + + if (error == olm_error()) { + throw lastError(m_account); + } + emit needsSave(); + return error; +} + +OneTimeKeys QOlmAccount::oneTimeKeys() const +{ + const size_t oneTimeKeyLength = olm_account_one_time_keys_length(m_account); + QByteArray oneTimeKeysBuffer(oneTimeKeyLength, '0'); + + const auto error = olm_account_one_time_keys(m_account, oneTimeKeysBuffer.data(), oneTimeKeyLength); + if (error == olm_error()) { + throw lastError(m_account); + } + const auto json = QJsonDocument::fromJson(oneTimeKeysBuffer).object(); + OneTimeKeys oneTimeKeys; + fromJson(json, oneTimeKeys.keys); + return oneTimeKeys; +} + +QHash<QString, SignedOneTimeKey> QOlmAccount::signOneTimeKeys(const OneTimeKeys &keys) const +{ + QHash<QString, SignedOneTimeKey> signedOneTimeKeys; + for (const auto &keyid : keys.curve25519().keys()) { + const auto oneTimeKey = keys.curve25519()[keyid]; + QByteArray sign = signOneTimeKey(oneTimeKey); + signedOneTimeKeys["signed_curve25519:" + keyid] = signedOneTimeKey(oneTimeKey.toUtf8(), sign); + } + return signedOneTimeKeys; +} + +SignedOneTimeKey QOlmAccount::signedOneTimeKey(const QByteArray &key, const QString &signature) const +{ + SignedOneTimeKey sign{}; + sign.key = key; + sign.signatures = {{m_userId, {{"ed25519:" + m_deviceId, signature}}}}; + return sign; +} + +QByteArray QOlmAccount::signOneTimeKey(const QString &key) const +{ + QJsonDocument j(QJsonObject{{"key", key}}); + return sign(j.toJson(QJsonDocument::Compact)); +} + +std::optional<QOlmError> QOlmAccount::removeOneTimeKeys(const QOlmSessionPtr &session) const +{ + const auto error = olm_remove_one_time_keys(m_account, session->raw()); + + if (error == olm_error()) { + return lastError(m_account); + } + emit needsSave(); + return std::nullopt; +} + +OlmAccount* QOlmAccount::data() { return m_account; } + +DeviceKeys QOlmAccount::deviceKeys() const +{ + DeviceKeys deviceKeys; + deviceKeys.userId = m_userId; + deviceKeys.deviceId = m_deviceId; + deviceKeys.algorithms = QStringList {"m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"}; + + const auto idKeys = identityKeys(); + deviceKeys.keys["curve25519:" + m_deviceId] = idKeys.curve25519; + deviceKeys.keys["ed25519:" + m_deviceId] = idKeys.ed25519; + + const auto sign = signIdentityKeys(); + deviceKeys.signatures[m_userId]["ed25519:" + m_deviceId] = sign; + + return deviceKeys; +} + +UploadKeysJob *QOlmAccount::createUploadKeyRequest(const OneTimeKeys &oneTimeKeys) +{ + auto keys = deviceKeys(); + + if (oneTimeKeys.curve25519().isEmpty()) { + return new UploadKeysJob(keys); + } + + // Sign & append the one time keys. + auto temp = signOneTimeKeys(oneTimeKeys); + QHash<QString, QVariant> oneTimeKeysSigned; + for (const auto &[keyId, key] : asKeyValueRange(temp)) { + oneTimeKeysSigned[keyId] = QVariant::fromValue(toJson(key)); + } + + return new UploadKeysJob(keys, oneTimeKeysSigned); +} + +std::variant<QOlmSessionPtr, QOlmError> QOlmAccount::createInboundSession(const QOlmMessage &preKeyMessage) +{ + Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey); + return QOlmSession::createInboundSession(this, preKeyMessage); +} + +std::variant<QOlmSessionPtr, QOlmError> QOlmAccount::createInboundSessionFrom(const QByteArray &theirIdentityKey, const QOlmMessage &preKeyMessage) +{ + Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey); + return QOlmSession::createInboundSessionFrom(this, theirIdentityKey, preKeyMessage); +} + +std::variant<QOlmSessionPtr, QOlmError> QOlmAccount::createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey) +{ + return QOlmSession::createOutboundSession(this, theirIdentityKey, theirOneTimeKey); +} + +void QOlmAccount::markKeysAsPublished() +{ + olm_account_mark_keys_as_published(m_account); + emit needsSave(); +} + +bool Quotient::verifyIdentitySignature(const DeviceKeys& deviceKeys, + const QString& deviceId, + const QString& userId) +{ + const auto signKeyId = "ed25519:" + deviceId; + const auto signingKey = deviceKeys.keys[signKeyId]; + const auto signature = deviceKeys.signatures[userId][signKeyId]; + + return ed25519VerifySignature(signingKey, toJson(deviceKeys), signature); +} + +bool Quotient::ed25519VerifySignature(const QString& signingKey, + const QJsonObject& obj, + const QString& signature) +{ + if (signature.isEmpty()) + return false; + + QJsonObject obj1 = obj; + + obj1.remove("unsigned"); + obj1.remove("signatures"); + + auto canonicalJson = QJsonDocument(obj1).toJson(QJsonDocument::Compact); + + QByteArray signingKeyBuf = signingKey.toUtf8(); + QOlmUtility utility; + auto signatureBuf = signature.toUtf8(); + auto result = utility.ed25519Verify(signingKeyBuf, canonicalJson, signatureBuf); + if (std::holds_alternative<QOlmError>(result)) { + return false; + } + + return std::get<bool>(result); +} diff --git a/lib/e2ee/qolmaccount.h b/lib/e2ee/qolmaccount.h new file mode 100644 index 00000000..17f43f1a --- /dev/null +++ b/lib/e2ee/qolmaccount.h @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + + +#pragma once + +#include "csapi/keys.h" +#include "e2ee/e2ee.h" +#include "e2ee/qolmerrors.h" +#include "e2ee/qolmmessage.h" +#include "e2ee/qolmsession.h" +#include <QObject> + +struct OlmAccount; + +namespace Quotient { + +class QOlmSession; +class Connection; + +using QOlmSessionPtr = std::unique_ptr<QOlmSession>; + +//! An olm account manages all cryptographic keys used on a device. +//! \code{.cpp} +//! const auto olmAccount = new QOlmAccount(this); +//! \endcode +class QUOTIENT_API QOlmAccount : public QObject +{ + Q_OBJECT +public: + QOlmAccount(const QString &userId, const QString &deviceId, QObject *parent = nullptr); + ~QOlmAccount(); + + //! Creates a new instance of OlmAccount. During the instantiation + //! the Ed25519 fingerprint key pair and the Curve25519 identity key + //! pair are generated. For more information see <a + //! href="https://matrix.org/docs/guides/e2e_implementation.html#keys-used-in-end-to-end-encryption">here</a>. + //! This needs to be called before any other action or use unpickle() instead. + void createNewAccount(); + + //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmAccount`. + //! This needs to be called before any other action or use createNewAccount() instead. + void unpickle(QByteArray &pickled, const PicklingMode &mode); + + //! Serialises an OlmAccount to encrypted Base64. + std::variant<QByteArray, QOlmError> pickle(const PicklingMode &mode); + + //! Returns the account's public identity keys already formatted as JSON + IdentityKeys identityKeys() const; + + //! Returns the signature of the supplied message. + QByteArray sign(const QByteArray &message) const; + QByteArray sign(const QJsonObject& message) const; + + //! Sign identity keys. + QByteArray signIdentityKeys() const; + + //! Maximum number of one time keys that this OlmAccount can + //! currently hold. + size_t maxNumberOfOneTimeKeys() const; + + //! Generates the supplied number of one time keys. + size_t generateOneTimeKeys(size_t numberOfKeys) const; + + //! Gets the OlmAccount's one time keys formatted as JSON. + OneTimeKeys oneTimeKeys() const; + + //! Sign all one time keys. + QHash<QString, SignedOneTimeKey> signOneTimeKeys(const OneTimeKeys &keys) const; + + //! Sign one time key. + QByteArray signOneTimeKey(const QString &key) const; + + SignedOneTimeKey signedOneTimeKey(const QByteArray &key, const QString &signature) const; + + UploadKeysJob *createUploadKeyRequest(const OneTimeKeys &oneTimeKeys); + + DeviceKeys deviceKeys() const; + + //! Remove the one time key used to create the supplied session. + [[nodiscard]] std::optional<QOlmError> removeOneTimeKeys(const QOlmSessionPtr &session) const; + + //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. + //! + //! \param message An Olm pre-key message that was encrypted for this account. + std::variant<QOlmSessionPtr, QOlmError> createInboundSession(const QOlmMessage &preKeyMessage); + + //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. + //! + //! \param theirIdentityKey - The identity key of the Olm account that + //! encrypted this Olm message. + std::variant<QOlmSessionPtr, QOlmError> createInboundSessionFrom(const QByteArray &theirIdentityKey, const QOlmMessage &preKeyMessage); + + //! Creates an outbound session for sending messages to a specific + /// identity and one time key. + std::variant<QOlmSessionPtr, QOlmError> createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey); + + void markKeysAsPublished(); + + // HACK do not use directly + QOlmAccount(OlmAccount *account); + OlmAccount *data(); + +Q_SIGNALS: + void needsSave() const; + +private: + OlmAccount *m_account = nullptr; // owning + QString m_userId; + QString m_deviceId; +}; + +QUOTIENT_API bool verifyIdentitySignature(const DeviceKeys& deviceKeys, + const QString& deviceId, + const QString& userId); + +//! checks if the signature is signed by the signing_key +QUOTIENT_API bool ed25519VerifySignature(const QString& signingKey, + const QJsonObject& obj, + const QString& signature); + +} // namespace Quotient diff --git a/lib/e2ee/qolmerrors.cpp b/lib/e2ee/qolmerrors.cpp new file mode 100644 index 00000000..5a60b7e6 --- /dev/null +++ b/lib/e2ee/qolmerrors.cpp @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + + +#include "qolmerrors.h" +#include "util.h" +#include <QtCore/QLatin1String> + +Quotient::QOlmError Quotient::fromString(const char* error_raw) { + const QLatin1String error { error_raw }; + if (error_raw == "BAD_ACCOUNT_KEY"_ls) { + return QOlmError::BadAccountKey; + } else if (error_raw == "BAD_MESSAGE_KEY_ID"_ls) { + return QOlmError::BadMessageKeyId; + } else if (error_raw == "INVALID_BASE64"_ls) { + return QOlmError::InvalidBase64; + } else if (error_raw == "NOT_ENOUGH_RANDOM"_ls) { + return QOlmError::NotEnoughRandom; + } else if (error_raw == "OUTPUT_BUFFER_TOO_SMALL"_ls) { + return QOlmError::OutputBufferTooSmall; + } else { + return QOlmError::Unknown; + } +} diff --git a/lib/e2ee/qolmerrors.h b/lib/e2ee/qolmerrors.h new file mode 100644 index 00000000..20e61c12 --- /dev/null +++ b/lib/e2ee/qolmerrors.h @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "quotient_export.h" + +namespace Quotient { +//! All errors that could be caused by an operation regarding Olm +//! Errors are named exactly like the ones in libolm. +enum QOlmError +{ + BadAccountKey, + BadMessageFormat, + BadMessageKeyId, + BadMessageMac, + BadMessageVersion, + InvalidBase64, + NotEnoughRandom, + OutputBufferTooSmall, + UnknownMessageIndex, + Unknown, +}; + +QUOTIENT_API QOlmError fromString(const char* error_raw); + +} //namespace Quotient diff --git a/lib/e2ee/qolminboundsession.cpp b/lib/e2ee/qolminboundsession.cpp new file mode 100644 index 00000000..2e9cc716 --- /dev/null +++ b/lib/e2ee/qolminboundsession.cpp @@ -0,0 +1,151 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "e2ee/qolminboundsession.h" +#include <iostream> +#include <cstring> + +using namespace Quotient; + +QOlmError lastError(OlmInboundGroupSession *session) { + return fromString(olm_inbound_group_session_last_error(session)); +} + +QOlmInboundGroupSession::QOlmInboundGroupSession(OlmInboundGroupSession *session) + : m_groupSession(session) +{ +} + +QOlmInboundGroupSession::~QOlmInboundGroupSession() +{ + olm_clear_inbound_group_session(m_groupSession); + //delete[](reinterpret_cast<uint8_t *>(m_groupSession)); +} + +std::unique_ptr<QOlmInboundGroupSession> QOlmInboundGroupSession::create(const QByteArray &key) +{ + const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); + const auto error = olm_init_inbound_group_session(olmInboundGroupSession, + reinterpret_cast<const uint8_t *>(key.constData()), key.size()); + + if (error == olm_error()) { + throw lastError(olmInboundGroupSession); + } + + return std::make_unique<QOlmInboundGroupSession>(olmInboundGroupSession); +} + +std::unique_ptr<QOlmInboundGroupSession> QOlmInboundGroupSession::import(const QByteArray &key) +{ + const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); + QByteArray keyBuf = key; + + const auto error = olm_import_inbound_group_session(olmInboundGroupSession, + reinterpret_cast<const uint8_t *>(keyBuf.data()), keyBuf.size()); + if (error == olm_error()) { + throw lastError(olmInboundGroupSession); + } + + return std::make_unique<QOlmInboundGroupSession>(olmInboundGroupSession); +} + +QByteArray toKey(const PicklingMode &mode) +{ + if (std::holds_alternative<Unencrypted>(mode)) { + return ""; + } + return std::get<Encrypted>(mode).key; +} + +QByteArray QOlmInboundGroupSession::pickle(const PicklingMode &mode) const +{ + QByteArray pickledBuf(olm_pickle_inbound_group_session_length(m_groupSession), '0'); + const QByteArray key = toKey(mode); + const auto error = olm_pickle_inbound_group_session(m_groupSession, key.data(), key.length(), pickledBuf.data(), + pickledBuf.length()); + if (error == olm_error()) { + throw lastError(m_groupSession); + } + return pickledBuf; +} + +std::variant<std::unique_ptr<QOlmInboundGroupSession>, QOlmError> QOlmInboundGroupSession::unpickle(const QByteArray &pickled, const PicklingMode &mode) +{ + QByteArray pickledBuf = pickled; + const auto groupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); + QByteArray key = toKey(mode); + const auto error = olm_unpickle_inbound_group_session(groupSession, key.data(), key.length(), + pickledBuf.data(), pickledBuf.size()); + if (error == olm_error()) { + return lastError(groupSession); + } + key.clear(); + + return std::make_unique<QOlmInboundGroupSession>(groupSession); +} + +std::variant<std::pair<QString, uint32_t>, QOlmError> QOlmInboundGroupSession::decrypt(const QByteArray &message) +{ + // This is for capturing the output of olm_group_decrypt + uint32_t messageIndex = 0; + + // We need to clone the message because + // olm_decrypt_max_plaintext_length destroys the input buffer + QByteArray messageBuf(message.length(), '0'); + std::copy(message.begin(), message.end(), messageBuf.begin()); + + QByteArray plaintextBuf(olm_group_decrypt_max_plaintext_length(m_groupSession, + reinterpret_cast<uint8_t *>(messageBuf.data()), messageBuf.length()), '0'); + + messageBuf = QByteArray(message.length(), '0'); + std::copy(message.begin(), message.end(), messageBuf.begin()); + + const auto plaintextLen = olm_group_decrypt(m_groupSession, reinterpret_cast<uint8_t *>(messageBuf.data()), + messageBuf.length(), reinterpret_cast<uint8_t *>(plaintextBuf.data()), plaintextBuf.length(), &messageIndex); + + // Error code or plaintext length is returned + const auto decryptError = plaintextLen; + + if (decryptError == olm_error()) { + return lastError(m_groupSession); + } + + QByteArray output(plaintextLen, '0'); + std::memcpy(output.data(), plaintextBuf.data(), plaintextLen); + + return std::make_pair<QString, qint32>(QString(output), messageIndex); +} + +std::variant<QByteArray, QOlmError> QOlmInboundGroupSession::exportSession(uint32_t messageIndex) +{ + const auto keyLength = olm_export_inbound_group_session_length(m_groupSession); + QByteArray keyBuf(keyLength, '0'); + const auto error = olm_export_inbound_group_session(m_groupSession, reinterpret_cast<uint8_t *>(keyBuf.data()), keyLength, messageIndex); + + if (error == olm_error()) { + return lastError(m_groupSession); + } + return keyBuf; +} + +uint32_t QOlmInboundGroupSession::firstKnownIndex() const +{ + return olm_inbound_group_session_first_known_index(m_groupSession); +} + +QByteArray QOlmInboundGroupSession::sessionId() const +{ + QByteArray sessionIdBuf(olm_inbound_group_session_id_length(m_groupSession), '0'); + const auto error = olm_inbound_group_session_id(m_groupSession, reinterpret_cast<uint8_t *>(sessionIdBuf.data()), + sessionIdBuf.length()); + if (error == olm_error()) { + throw lastError(m_groupSession); + } + return sessionIdBuf; +} + +bool QOlmInboundGroupSession::isVerified() const +{ + return olm_inbound_group_session_is_verified(m_groupSession) != 0; +} diff --git a/lib/e2ee/qolminboundsession.h b/lib/e2ee/qolminboundsession.h new file mode 100644 index 00000000..437f753d --- /dev/null +++ b/lib/e2ee/qolminboundsession.h @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "e2ee/e2ee.h" +#include "e2ee/qolmerrors.h" +#include "olm/olm.h" + +#include <memory> +#include <variant> + +namespace Quotient { + +//! An in-bound group session is responsible for decrypting incoming +//! communication in a Megolm session. +class QUOTIENT_API QOlmInboundGroupSession +{ +public: + ~QOlmInboundGroupSession(); + //! Creates a new instance of `OlmInboundGroupSession`. + static std::unique_ptr<QOlmInboundGroupSession> create(const QByteArray& key); + //! Import an inbound group session, from a previous export. + static std::unique_ptr<QOlmInboundGroupSession> import(const QByteArray& key); + //! Serialises an `OlmInboundGroupSession` to encrypted Base64. + QByteArray pickle(const PicklingMode &mode) const; + //! Deserialises from encrypted Base64 that was previously obtained by pickling + //! an `OlmInboundGroupSession`. + static std::variant<std::unique_ptr<QOlmInboundGroupSession>, QOlmError> + unpickle(const QByteArray& picked, const PicklingMode& mode); + //! Decrypts ciphertext received for this group session. + std::variant<std::pair<QString, uint32_t>, QOlmError> decrypt( + const QByteArray& message); + //! Export the base64-encoded ratchet key for this session, at the given index, + //! in a format which can be used by import. + std::variant<QByteArray, QOlmError> exportSession(uint32_t messageIndex); + //! Get the first message index we know how to decrypt. + uint32_t firstKnownIndex() const; + //! Get a base64-encoded identifier for this session. + QByteArray sessionId() const; + bool isVerified() const; + + QOlmInboundGroupSession(OlmInboundGroupSession* session); +private: + OlmInboundGroupSession* m_groupSession; +}; + +using QOlmInboundGroupSessionPtr = std::unique_ptr<QOlmInboundGroupSession>; +} // namespace Quotient diff --git a/lib/e2ee/qolmmessage.cpp b/lib/e2ee/qolmmessage.cpp new file mode 100644 index 00000000..81b166b0 --- /dev/null +++ b/lib/e2ee/qolmmessage.cpp @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "qolmmessage.h" + +using namespace Quotient; + +QOlmMessage::QOlmMessage(QByteArray ciphertext, QOlmMessage::Type type) + : QByteArray(std::move(ciphertext)) + , m_messageType(type) +{ + Q_ASSERT_X(!isEmpty(), "olm message", "Ciphertext is empty"); +} + +QOlmMessage::QOlmMessage(const QOlmMessage &message) + : QByteArray(message) + , m_messageType(message.type()) +{ +} + +QOlmMessage::Type QOlmMessage::type() const +{ + return m_messageType; +} + +QByteArray QOlmMessage::toCiphertext() const +{ + return QByteArray(*this); +} + +QOlmMessage QOlmMessage::fromCiphertext(const QByteArray &ciphertext) +{ + return QOlmMessage(ciphertext, QOlmMessage::General); +} diff --git a/lib/e2ee/qolmmessage.h b/lib/e2ee/qolmmessage.h new file mode 100644 index 00000000..5d5db636 --- /dev/null +++ b/lib/e2ee/qolmmessage.h @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "quotient_export.h" + +#include <QObject> +#include <QByteArray> + +namespace Quotient { + +/*! \brief A wrapper around an olm encrypted message + * + * This class encapsulates a Matrix olm encrypted message, + * passed in either of 2 forms: a general message or a pre-key message. + * + * The class provides functions to get a type and the ciphertext. + */ +class QUOTIENT_API QOlmMessage : public QByteArray { + Q_GADGET +public: + enum Type { + General, + PreKey, + }; + Q_ENUM(Type) + + QOlmMessage() = default; + explicit QOlmMessage(QByteArray ciphertext, Type type = General); + explicit QOlmMessage(const QOlmMessage &message); + ~QOlmMessage() = default; + + static QOlmMessage fromCiphertext(const QByteArray &ciphertext); + + Q_INVOKABLE Type type() const; + Q_INVOKABLE QByteArray toCiphertext() const; + +private: + Type m_messageType = General; +}; + +} //namespace Quotient diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp new file mode 100644 index 00000000..da32417b --- /dev/null +++ b/lib/e2ee/qolmoutboundsession.cpp @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "e2ee/qolmoutboundsession.h" +#include "e2ee/qolmutils.h" + +using namespace Quotient; + +QOlmError lastError(OlmOutboundGroupSession *session) { + return fromString(olm_outbound_group_session_last_error(session)); +} + +QOlmOutboundGroupSession::QOlmOutboundGroupSession(OlmOutboundGroupSession *session) + : m_groupSession(session) +{ +} + +QOlmOutboundGroupSession::~QOlmOutboundGroupSession() +{ + olm_clear_outbound_group_session(m_groupSession); + delete[](reinterpret_cast<uint8_t *>(m_groupSession)); +} + +std::unique_ptr<QOlmOutboundGroupSession> QOlmOutboundGroupSession::create() +{ + auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); + const auto randomLength = olm_init_outbound_group_session_random_length(olmOutboundGroupSession); + QByteArray randomBuf = getRandom(randomLength); + + const auto error = olm_init_outbound_group_session(olmOutboundGroupSession, + reinterpret_cast<uint8_t *>(randomBuf.data()), randomBuf.length()); + + if (error == olm_error()) { + throw lastError(olmOutboundGroupSession); + } + + const auto keyMaxLength = olm_outbound_group_session_key_length(olmOutboundGroupSession); + QByteArray keyBuffer(keyMaxLength, '0'); + olm_outbound_group_session_key(olmOutboundGroupSession, reinterpret_cast<uint8_t *>(keyBuffer.data()), + keyMaxLength); + + randomBuf.clear(); + + return std::make_unique<QOlmOutboundGroupSession>(olmOutboundGroupSession); +} + +std::variant<QByteArray, QOlmError> QOlmOutboundGroupSession::pickle(const PicklingMode &mode) +{ + QByteArray pickledBuf(olm_pickle_outbound_group_session_length(m_groupSession), '0'); + QByteArray key = toKey(mode); + const auto error = olm_pickle_outbound_group_session(m_groupSession, key.data(), key.length(), + pickledBuf.data(), pickledBuf.length()); + + if (error == olm_error()) { + return lastError(m_groupSession); + } + + key.clear(); + + return pickledBuf; +} + +std::variant<std::unique_ptr<QOlmOutboundGroupSession>, QOlmError> QOlmOutboundGroupSession::unpickle(QByteArray &pickled, const PicklingMode &mode) +{ + QByteArray pickledBuf = pickled; + auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); + QByteArray key = toKey(mode); + const auto error = olm_unpickle_outbound_group_session(olmOutboundGroupSession, key.data(), key.length(), + pickled.data(), pickled.length()); + if (error == olm_error()) { + return lastError(olmOutboundGroupSession); + } + const auto idMaxLength = olm_outbound_group_session_id_length(olmOutboundGroupSession); + QByteArray idBuffer(idMaxLength, '0'); + olm_outbound_group_session_id(olmOutboundGroupSession, reinterpret_cast<uint8_t *>(idBuffer.data()), + idBuffer.length()); + + key.clear(); + return std::make_unique<QOlmOutboundGroupSession>(olmOutboundGroupSession); +} + +std::variant<QByteArray, QOlmError> QOlmOutboundGroupSession::encrypt(const QString &plaintext) +{ + QByteArray plaintextBuf = plaintext.toUtf8(); + const auto messageMaxLength = olm_group_encrypt_message_length(m_groupSession, plaintextBuf.length()); + QByteArray messageBuf(messageMaxLength, '0'); + const auto error = olm_group_encrypt(m_groupSession, reinterpret_cast<uint8_t *>(plaintextBuf.data()), + plaintextBuf.length(), reinterpret_cast<uint8_t *>(messageBuf.data()), messageBuf.length()); + + if (error == olm_error()) { + return lastError(m_groupSession); + } + + return messageBuf; +} + +uint32_t QOlmOutboundGroupSession::sessionMessageIndex() const +{ + return olm_outbound_group_session_message_index(m_groupSession); +} + +QByteArray QOlmOutboundGroupSession::sessionId() const +{ + const auto idMaxLength = olm_outbound_group_session_id_length(m_groupSession); + QByteArray idBuffer(idMaxLength, '0'); + const auto error = olm_outbound_group_session_id(m_groupSession, reinterpret_cast<uint8_t *>(idBuffer.data()), + idBuffer.length()); + if (error == olm_error()) { + throw lastError(m_groupSession); + } + return idBuffer; +} + +std::variant<QByteArray, QOlmError> QOlmOutboundGroupSession::sessionKey() const +{ + const auto keyMaxLength = olm_outbound_group_session_key_length(m_groupSession); + QByteArray keyBuffer(keyMaxLength, '0'); + const auto error = olm_outbound_group_session_key(m_groupSession, reinterpret_cast<uint8_t *>(keyBuffer.data()), + keyMaxLength); + if (error == olm_error()) { + return lastError(m_groupSession); + } + return keyBuffer; +} diff --git a/lib/e2ee/qolmoutboundsession.h b/lib/e2ee/qolmoutboundsession.h new file mode 100644 index 00000000..32ba2b3b --- /dev/null +++ b/lib/e2ee/qolmoutboundsession.h @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "olm/olm.h" +#include "e2ee/qolmerrors.h" +#include "e2ee/e2ee.h" +#include <memory> + +namespace Quotient { + +//! An out-bound group session is responsible for encrypting outgoing +//! communication in a Megolm session. +class QUOTIENT_API QOlmOutboundGroupSession +{ +public: + ~QOlmOutboundGroupSession(); + //! Creates a new instance of `QOlmOutboundGroupSession`. + //! Throw OlmError on errors + static std::unique_ptr<QOlmOutboundGroupSession> create(); + //! Serialises a `QOlmOutboundGroupSession` to encrypted Base64. + std::variant<QByteArray, QOlmError> pickle(const PicklingMode &mode); + //! Deserialises from encrypted Base64 that was previously obtained by + //! pickling a `QOlmOutboundGroupSession`. + static std::variant<std::unique_ptr<QOlmOutboundGroupSession>, QOlmError> + unpickle(QByteArray& pickled, const PicklingMode& mode); + //! Encrypts a plaintext message using the session. + std::variant<QByteArray, QOlmError> encrypt(const QString &plaintext); + + //! Get the current message index for this session. + //! + //! Each message is sent with an increasing index; this returns the + //! index for the next message. + uint32_t sessionMessageIndex() const; + + //! Get a base64-encoded identifier for this session. + QByteArray sessionId() const; + + //! Get the base64-encoded current ratchet key for this session. + //! + //! Each message is sent with a different ratchet key. This function returns the + //! ratchet key that will be used for the next message. + std::variant<QByteArray, QOlmError> sessionKey() const; + QOlmOutboundGroupSession(OlmOutboundGroupSession *groupSession); +private: + OlmOutboundGroupSession *m_groupSession; +}; + +using QOlmOutboundGroupSessionPtr = std::unique_ptr<QOlmOutboundGroupSession>; +} diff --git a/lib/e2ee/qolmsession.cpp b/lib/e2ee/qolmsession.cpp new file mode 100644 index 00000000..e575ff39 --- /dev/null +++ b/lib/e2ee/qolmsession.cpp @@ -0,0 +1,251 @@ +// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "qolmsession.h" +#include "e2ee/qolmutils.h" +#include "logging.h" +#include <cstring> +#include <QDebug> + +using namespace Quotient; + +QOlmError lastError(OlmSession* session) { + return fromString(olm_session_last_error(session)); +} + +Quotient::QOlmSession::~QOlmSession() +{ + olm_clear_session(m_session); + delete[](reinterpret_cast<uint8_t *>(m_session)); +} + +OlmSession* QOlmSession::create() +{ + return olm_session(new uint8_t[olm_session_size()]); +} + +std::variant<QOlmSessionPtr, QOlmError> QOlmSession::createInbound(QOlmAccount *account, const QOlmMessage &preKeyMessage, bool from, const QString &theirIdentityKey) +{ + if (preKeyMessage.type() != QOlmMessage::PreKey) { + qCCritical(E2EE) << "The message is not a pre-key in when creating inbound session" << BadMessageFormat; + } + + const auto olmSession = create(); + + QByteArray oneTimeKeyMessageBuf = preKeyMessage.toCiphertext(); + QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8(); + size_t error = 0; + if (from) { + error = olm_create_inbound_session_from(olmSession, account->data(), theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); + } else { + error = olm_create_inbound_session(olmSession, account->data(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); + } + + if (error == olm_error()) { + const auto lastErr = lastError(olmSession); + qCWarning(E2EE) << "Error when creating inbound session" << lastErr; + return lastErr; + } + + return std::make_unique<QOlmSession>(olmSession); +} + +std::variant<QOlmSessionPtr, QOlmError> QOlmSession::createInboundSession(QOlmAccount *account, const QOlmMessage &preKeyMessage) +{ + return createInbound(account, preKeyMessage); +} + +std::variant<QOlmSessionPtr, QOlmError> QOlmSession::createInboundSessionFrom(QOlmAccount *account, const QString &theirIdentityKey, const QOlmMessage &preKeyMessage) +{ + return createInbound(account, preKeyMessage, true, theirIdentityKey); +} + +std::variant<QOlmSessionPtr, QOlmError> QOlmSession::createOutboundSession(QOlmAccount *account, const QString &theirIdentityKey, const QString &theirOneTimeKey) +{ + auto *olmOutboundSession = create(); + const auto randomLen = olm_create_outbound_session_random_length(olmOutboundSession); + QByteArray randomBuf = getRandom(randomLen); + + QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8(); + QByteArray theirOneTimeKeyBuf = theirOneTimeKey.toUtf8(); + const auto error = olm_create_outbound_session(olmOutboundSession, + account->data(), + reinterpret_cast<uint8_t *>(theirIdentityKeyBuf.data()), theirIdentityKeyBuf.length(), + reinterpret_cast<uint8_t *>(theirOneTimeKeyBuf.data()), theirOneTimeKeyBuf.length(), + reinterpret_cast<uint8_t *>(randomBuf.data()), randomBuf.length()); + + if (error == olm_error()) { + const auto lastErr = lastError(olmOutboundSession); + if (lastErr == QOlmError::NotEnoughRandom) { + throw lastErr; + } + return lastErr; + } + + randomBuf.clear(); + return std::make_unique<QOlmSession>(olmOutboundSession); +} + +std::variant<QByteArray, QOlmError> QOlmSession::pickle(const PicklingMode &mode) +{ + QByteArray pickledBuf(olm_pickle_session_length(m_session), '0'); + QByteArray key = toKey(mode); + const auto error = olm_pickle_session(m_session, key.data(), key.length(), + pickledBuf.data(), pickledBuf.length()); + + if (error == olm_error()) { + return lastError(m_session); + } + + key.clear(); + + return pickledBuf; +} + +std::variant<QOlmSessionPtr, QOlmError> QOlmSession::unpickle(const QByteArray &pickled, const PicklingMode &mode) +{ + QByteArray pickledBuf = pickled; + auto *olmSession = create(); + QByteArray key = toKey(mode); + const auto error = olm_unpickle_session(olmSession, key.data(), key.length(), + pickledBuf.data(), pickledBuf.length()); + if (error == olm_error()) { + return lastError(olmSession); + } + + key.clear(); + return std::make_unique<QOlmSession>(olmSession); +} + +QOlmMessage QOlmSession::encrypt(const QString &plaintext) +{ + QByteArray plaintextBuf = plaintext.toUtf8(); + const auto messageMaxLen = olm_encrypt_message_length(m_session, plaintextBuf.length()); + QByteArray messageBuf(messageMaxLen, '0'); + const auto messageType = encryptMessageType(); + const auto randomLen = olm_encrypt_random_length(m_session); + QByteArray randomBuf = getRandom(randomLen); + const auto error = olm_encrypt(m_session, + reinterpret_cast<uint8_t *>(plaintextBuf.data()), plaintextBuf.length(), + reinterpret_cast<uint8_t *>(randomBuf.data()), randomBuf.length(), + reinterpret_cast<uint8_t *>(messageBuf.data()), messageBuf.length()); + + if (error == olm_error()) { + throw lastError(m_session); + } + + return QOlmMessage(messageBuf, messageType); +} + +std::variant<QString, QOlmError> QOlmSession::decrypt(const QOlmMessage &message) const +{ + const auto messageType = message.type(); + const auto ciphertext = message.toCiphertext(); + const auto messageTypeValue = messageType == QOlmMessage::Type::General + ? OLM_MESSAGE_TYPE_MESSAGE : OLM_MESSAGE_TYPE_PRE_KEY; + + // We need to clone the message because + // olm_decrypt_max_plaintext_length destroys the input buffer + QByteArray messageBuf(ciphertext.length(), '0'); + std::copy(message.begin(), message.end(), messageBuf.begin()); + + const auto plaintextMaxLen = olm_decrypt_max_plaintext_length(m_session, messageTypeValue, + reinterpret_cast<uint8_t *>(messageBuf.data()), messageBuf.length()); + + if (plaintextMaxLen == olm_error()) { + return lastError(m_session); + } + + QByteArray plaintextBuf(plaintextMaxLen, '0'); + QByteArray messageBuf2(ciphertext.length(), '0'); + std::copy(message.begin(), message.end(), messageBuf2.begin()); + + const auto plaintextResultLen = olm_decrypt(m_session, messageTypeValue, + reinterpret_cast<uint8_t *>(messageBuf2.data()), messageBuf2.length(), + reinterpret_cast<uint8_t *>(plaintextBuf.data()), plaintextMaxLen); + + if (plaintextResultLen == olm_error()) { + const auto lastErr = lastError(m_session); + if (lastErr == QOlmError::OutputBufferTooSmall) { + throw lastErr; + } + return lastErr; + } + QByteArray output(plaintextResultLen, '0'); + std::memcpy(output.data(), plaintextBuf.data(), plaintextResultLen); + plaintextBuf.clear(); + return output; +} + +QOlmMessage::Type QOlmSession::encryptMessageType() +{ + const auto messageTypeResult = olm_encrypt_message_type(m_session); + if (messageTypeResult == olm_error()) { + throw lastError(m_session); + } + if (messageTypeResult == OLM_MESSAGE_TYPE_PRE_KEY) { + return QOlmMessage::PreKey; + } + return QOlmMessage::General; +} + +QByteArray QOlmSession::sessionId() const +{ + const auto idMaxLength = olm_session_id_length(m_session); + QByteArray idBuffer(idMaxLength, '0'); + const auto error = olm_session_id(m_session, reinterpret_cast<uint8_t *>(idBuffer.data()), + idBuffer.length()); + if (error == olm_error()) { + throw lastError(m_session); + } + return idBuffer; +} + +bool QOlmSession::hasReceivedMessage() const +{ + return olm_session_has_received_message(m_session); +} + +std::variant<bool, QOlmError> QOlmSession::matchesInboundSession(const QOlmMessage &preKeyMessage) const +{ + Q_ASSERT(preKeyMessage.type() == QOlmMessage::Type::PreKey); + QByteArray oneTimeKeyBuf(preKeyMessage.data()); + const auto matchesResult = olm_matches_inbound_session(m_session, oneTimeKeyBuf.data(), oneTimeKeyBuf.length()); + + if (matchesResult == olm_error()) { + return lastError(m_session); + } + switch (matchesResult) { + case 0: + return false; + case 1: + return true; + default: + return QOlmError::Unknown; + } +} +std::variant<bool, QOlmError> QOlmSession::matchesInboundSessionFrom(const QString &theirIdentityKey, const QOlmMessage &preKeyMessage) const +{ + const auto theirIdentityKeyBuf = theirIdentityKey.toUtf8(); + auto oneTimeKeyMessageBuf = preKeyMessage.toCiphertext(); + const auto error = olm_matches_inbound_session_from(m_session, theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), + oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); + + if (error == olm_error()) { + return lastError(m_session); + } + switch (error) { + case 0: + return false; + case 1: + return true; + default: + return QOlmError::Unknown; + } +} + +QOlmSession::QOlmSession(OlmSession *session) + : m_session(session) +{ +} diff --git a/lib/e2ee/qolmsession.h b/lib/e2ee/qolmsession.h new file mode 100644 index 00000000..f20c9837 --- /dev/null +++ b/lib/e2ee/qolmsession.h @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include <QDebug> +#include <olm/olm.h> // FIXME: OlmSession +#include "e2ee/e2ee.h" +#include "e2ee/qolmmessage.h" +#include "e2ee/qolmerrors.h" +#include "e2ee/qolmaccount.h" + +namespace Quotient { + +class QOlmAccount; +class QOlmSession; + +//! Either an outbound or inbound session for secure communication. +class QUOTIENT_API QOlmSession +{ +public: + ~QOlmSession(); + //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. + static std::variant<std::unique_ptr<QOlmSession>, QOlmError> + createInboundSession(QOlmAccount* account, const QOlmMessage& preKeyMessage); + + static std::variant<std::unique_ptr<QOlmSession>, QOlmError> + createInboundSessionFrom(QOlmAccount* account, + const QString& theirIdentityKey, + const QOlmMessage& preKeyMessage); + + static std::variant<std::unique_ptr<QOlmSession>, QOlmError> + createOutboundSession(QOlmAccount* account, const QString& theirIdentityKey, + const QString& theirOneTimeKey); + + //! Serialises an `QOlmSession` to encrypted Base64. + std::variant<QByteArray, QOlmError> pickle(const PicklingMode &mode); + + //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmSession`. + static std::variant<std::unique_ptr<QOlmSession>, QOlmError> unpickle( + const QByteArray& pickled, const PicklingMode& mode); + + //! Encrypts a plaintext message using the session. + QOlmMessage encrypt(const QString &plaintext); + + //! Decrypts a message using this session. Decoding is lossy, meaing if + //! the decrypted plaintext contains invalid UTF-8 symbols, they will + //! be returned as `U+FFFD` (�). + std::variant<QString, QOlmError> decrypt(const QOlmMessage &message) const; + + //! Get a base64-encoded identifier for this session. + QByteArray sessionId() const; + + //! The type of the next message that will be returned from encryption. + QOlmMessage::Type encryptMessageType(); + + //! Checker for any received messages for this session. + bool hasReceivedMessage() const; + + //! Checks if the 'prekey' message is for this in-bound session. + std::variant<bool, QOlmError> matchesInboundSession( + const QOlmMessage& preKeyMessage) const; + + //! Checks if the 'prekey' message is for this in-bound session. + std::variant<bool, QOlmError> matchesInboundSessionFrom( + const QString& theirIdentityKey, const QOlmMessage& preKeyMessage) const; + + friend bool operator<(const QOlmSession& lhs, const QOlmSession& rhs) + { + return lhs.sessionId() < rhs.sessionId(); + } + + friend bool operator<(const std::unique_ptr<QOlmSession>& lhs, + const std::unique_ptr<QOlmSession>& rhs) + { + return *lhs < *rhs; + } + + OlmSession* raw() const { return m_session; } + + QOlmSession(OlmSession* session); +private: + //! Helper function for creating new sessions and handling errors. + static OlmSession* create(); + static std::variant<std::unique_ptr<QOlmSession>, QOlmError> createInbound( + QOlmAccount* account, const QOlmMessage& preKeyMessage, + bool from = false, const QString& theirIdentityKey = ""); + OlmSession* m_session; +}; +} //namespace Quotient diff --git a/lib/e2ee/qolmutility.cpp b/lib/e2ee/qolmutility.cpp new file mode 100644 index 00000000..303f6d75 --- /dev/null +++ b/lib/e2ee/qolmutility.cpp @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "e2ee/qolmutility.h" +#include "olm/olm.h" +#include <QDebug> + +using namespace Quotient; + +// Convert olm error to enum +QOlmError lastError(OlmUtility *utility) { + return fromString(olm_utility_last_error(utility)); +} + +QOlmUtility::QOlmUtility() +{ + auto utility = new uint8_t[olm_utility_size()]; + m_utility = olm_utility(utility); +} + +QOlmUtility::~QOlmUtility() +{ + olm_clear_utility(m_utility); + delete[](reinterpret_cast<uint8_t *>(m_utility)); +} + +QString QOlmUtility::sha256Bytes(const QByteArray &inputBuf) const +{ + const auto outputLen = olm_sha256_length(m_utility); + QByteArray outputBuf(outputLen, '0'); + olm_sha256(m_utility, inputBuf.data(), inputBuf.length(), + outputBuf.data(), outputBuf.length()); + + return QString::fromUtf8(outputBuf); +} + +QString QOlmUtility::sha256Utf8Msg(const QString &message) const +{ + return sha256Bytes(message.toUtf8()); +} + +std::variant<bool, QOlmError> QOlmUtility::ed25519Verify(const QByteArray &key, + const QByteArray &message, const QByteArray &signature) +{ + QByteArray signatureBuf(signature.length(), '0'); + std::copy(signature.begin(), signature.end(), signatureBuf.begin()); + + const auto ret = olm_ed25519_verify(m_utility, key.data(), key.size(), + message.data(), message.size(), (void *)signatureBuf.data(), signatureBuf.size()); + + const auto error = ret; + if (error == olm_error()) { + return lastError(m_utility); + } + + if (ret != 0) { + return false; + } + return true; +} diff --git a/lib/e2ee/qolmutility.h b/lib/e2ee/qolmutility.h new file mode 100644 index 00000000..a12af49a --- /dev/null +++ b/lib/e2ee/qolmutility.h @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include <variant> +#include "e2ee/qolmerrors.h" + +struct OlmUtility; + +namespace Quotient { + +class QOlmSession; + +//! Allows you to make use of crytographic hashing via SHA-2 and +//! verifying ed25519 signatures. +class QUOTIENT_API QOlmUtility +{ +public: + QOlmUtility(); + ~QOlmUtility(); + + //! Returns a sha256 of the supplied byte slice. + QString sha256Bytes(const QByteArray &inputBuf) const; + + //! Convenience function that converts the UTF-8 message + //! to bytes and then calls `sha256Bytes()`, returning its output. + QString sha256Utf8Msg(const QString &message) const; + + //! Verify a ed25519 signature. + //! \param key QByteArray The public part of the ed25519 key that signed the message. + //! \param message QByteArray The message that was signed. + //! \param signature QByteArray The signature of the message. + std::variant<bool, QOlmError> ed25519Verify(const QByteArray &key, + const QByteArray &message, const QByteArray &signature); + +private: + OlmUtility *m_utility; + +}; +} diff --git a/lib/e2ee/qolmutils.cpp b/lib/e2ee/qolmutils.cpp new file mode 100644 index 00000000..6f7937e8 --- /dev/null +++ b/lib/e2ee/qolmutils.cpp @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "e2ee/qolmutils.h" +#include <QtCore/QRandomGenerator> + +using namespace Quotient; + +QByteArray Quotient::toKey(const Quotient::PicklingMode &mode) +{ + if (std::holds_alternative<Quotient::Unencrypted>(mode)) { + return {}; + } + return std::get<Quotient::Encrypted>(mode).key; +} + +QByteArray Quotient::getRandom(size_t bufferSize) +{ + QByteArray buffer(bufferSize, '0'); + QRandomGenerator::system()->generate(buffer.begin(), buffer.end()); + return buffer; +} diff --git a/lib/e2ee/qolmutils.h b/lib/e2ee/qolmutils.h new file mode 100644 index 00000000..f218e628 --- /dev/null +++ b/lib/e2ee/qolmutils.h @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include <QByteArray> + +#include "e2ee/e2ee.h" + +namespace Quotient { +// Convert PicklingMode to key +QUOTIENT_API QByteArray toKey(const PicklingMode &mode); +QUOTIENT_API QByteArray getRandom(size_t bufferSize); +} diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp deleted file mode 100644 index 37f3b7c3..00000000 --- a/lib/encryptionmanager.cpp +++ /dev/null @@ -1,373 +0,0 @@ -// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> -// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net> -// SPDX-License-Identifier: LGPL-2.1-or-later - -#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->uploadOneTimeKeysInitJob->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 diff --git a/lib/encryptionmanager.h b/lib/encryptionmanager.h deleted file mode 100644 index 714f95fd..00000000 --- a/lib/encryptionmanager.h +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> -// SPDX-License-Identifier: LGPL-2.1-or-later - -#ifdef Quotient_E2EE_ENABLED -#pragma once - -#include <QtCore/QObject> - -#include <functional> -#include <memory> - -namespace QtOlm { -class Account; -} - -namespace Quotient { -class Connection; - -class EncryptionManager : public QObject { - Q_OBJECT - -public: - // TODO: store constats separately? - // TODO: 0.5 oneTimeKeyThreshold instead of 0.1? - explicit EncryptionManager( - const QByteArray& encryptionAccountPickle = QByteArray(), - float signedKeysProportion = 1, float oneTimeKeyThreshold = float(0.1), - QObject* parent = nullptr); - ~EncryptionManager(); - - void uploadIdentityKeys(Connection* connection); - void uploadOneTimeKeys(Connection* connection, bool forceUpdate = false); - void - updateOneTimeKeyCounts(Connection* connection, - const QHash<QString, int>& deviceOneTimeKeysCount); - void updateDeviceKeys(Connection* connection, - const QHash<QString, QStringList>& deviceKeys); - QString sessionDecryptMessage(const QJsonObject& personalCipherObject, - const QByteArray& senderKey); - QByteArray olmAccountPickle(); - - QtOlm::Account* account() const; - -private: - class Private; - std::unique_ptr<Private> d; -}; - -} // namespace Quotient -#endif // Quotient_E2EE_ENABLED diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp index 0290f973..9d07a35f 100644 --- a/lib/events/encryptedevent.cpp +++ b/lib/events/encryptedevent.cpp @@ -2,6 +2,8 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "encryptedevent.h" +#include "roommessageevent.h" +#include "events/eventloader.h" using namespace Quotient; @@ -30,3 +32,32 @@ EncryptedEvent::EncryptedEvent(const QJsonObject& obj) { qCDebug(E2EE) << "Encrypted event from" << senderId(); } + +QString EncryptedEvent::algorithm() const +{ + const auto algo = contentPart<QString>(AlgorithmKeyL); + if (!isSupportedAlgorithm(algo)) + qWarning(MAIN) << "The EncryptedEvent's algorithm" << algo + << "is not supported"; + + return algo; +} + +RoomEventPtr EncryptedEvent::createDecrypted(const QString &decrypted) const +{ + auto eventObject = QJsonDocument::fromJson(decrypted.toUtf8()).object(); + eventObject["event_id"] = id(); + eventObject["sender"] = senderId(); + eventObject["origin_server_ts"] = originTimestamp().toMSecsSinceEpoch(); + if (const auto relatesToJson = contentPart("m.relates_to"_ls); !relatesToJson.isUndefined()) { + auto content = eventObject["content"].toObject(); + content["m.relates_to"] = relatesToJson.toObject(); + eventObject["content"] = content; + } + if (const auto redactsJson = unsignedPart("redacts"_ls); !redactsJson.isUndefined()) { + auto unsign = eventObject["unsigned"].toObject(); + unsign["redacts"] = redactsJson.toString(); + eventObject["unsigned"] = unsign; + } + return loadEvent<RoomEvent>(eventObject); +} diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h index 81343a29..72efffd4 100644 --- a/lib/events/encryptedevent.h +++ b/lib/events/encryptedevent.h @@ -3,7 +3,7 @@ #pragma once -#include "e2ee.h" +#include "e2ee/e2ee.h" #include "roomevent.h" namespace Quotient { @@ -39,28 +39,23 @@ public: const QString& deviceId, const QString& sessionId); explicit EncryptedEvent(const QJsonObject& obj); - QString algorithm() const - { - QString algo = contentPart<QString>(AlgorithmKeyL); - if (!SupportedAlgorithms.contains(algo)) { - qWarning(MAIN) << "The EncryptedEvent's algorithm" << algo - << "is not supported"; - } - return algo; - } + QString algorithm() const; QByteArray ciphertext() const { return contentPart<QString>(CiphertextKeyL).toLatin1(); } QJsonObject ciphertext(const QString& identityKey) const { - return contentPart<QJsonObject>(CiphertextKeyL).value(identityKey).toObject(); + return contentPart<QJsonObject>(CiphertextKeyL) + .value(identityKey) + .toObject(); } QString senderKey() const { return contentPart<QString>(SenderKeyKeyL); } /* device_id and session_id are required with Megolm */ QString deviceId() const { return contentPart<QString>(DeviceIdKeyL); } QString sessionId() const { return contentPart<QString>(SessionIdKeyL); } + RoomEventPtr createDecrypted(const QString &decrypted) const; }; REGISTER_EVENT_TYPE(EncryptedEvent) diff --git a/lib/events/encryptedfile.cpp b/lib/events/encryptedfile.cpp new file mode 100644 index 00000000..d4a517bd --- /dev/null +++ b/lib/events/encryptedfile.cpp @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "encryptedfile.h" +#include "logging.h" + +#ifdef Quotient_E2EE_ENABLED +#include <openssl/evp.h> +#include <QtCore/QCryptographicHash> +#endif + +using namespace Quotient; + +QByteArray EncryptedFile::decryptFile(const QByteArray& ciphertext) const +{ +#ifdef Quotient_E2EE_ENABLED + auto _key = key.k; + const auto keyBytes = QByteArray::fromBase64( + _key.replace(u'_', u'/').replace(u'-', u'+').toLatin1()); + const auto sha256 = QByteArray::fromBase64(hashes["sha256"].toLatin1()); + if (sha256 + != QCryptographicHash::hash(ciphertext, QCryptographicHash::Sha256)) { + qCWarning(E2EE) << "Hash verification failed for file"; + return {}; + } + { + int length; + auto* ctx = EVP_CIPHER_CTX_new(); + QByteArray plaintext(ciphertext.size() + EVP_CIPHER_CTX_block_size(ctx) + - 1, + '\0'); + EVP_DecryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, + reinterpret_cast<const unsigned char*>( + keyBytes.data()), + reinterpret_cast<const unsigned char*>( + QByteArray::fromBase64(iv.toLatin1()).data())); + EVP_DecryptUpdate( + ctx, reinterpret_cast<unsigned char*>(plaintext.data()), &length, + reinterpret_cast<const unsigned char*>(ciphertext.data()), + ciphertext.size()); + EVP_DecryptFinal_ex(ctx, + reinterpret_cast<unsigned char*>(plaintext.data()) + + length, + &length); + EVP_CIPHER_CTX_free(ctx); + return plaintext; + } +#else + qWarning(MAIN) << "This build of libQuotient doesn't support E2EE, " + "cannot decrypt the file"; + return ciphertext; +#endif +} + +void JsonObjectConverter<EncryptedFile>::dumpTo(QJsonObject& jo, + const EncryptedFile& pod) +{ + addParam<>(jo, QStringLiteral("url"), pod.url); + addParam<>(jo, QStringLiteral("key"), pod.key); + addParam<>(jo, QStringLiteral("iv"), pod.iv); + addParam<>(jo, QStringLiteral("hashes"), pod.hashes); + addParam<>(jo, QStringLiteral("v"), pod.v); +} + +void JsonObjectConverter<EncryptedFile>::fillFrom(const QJsonObject& jo, + EncryptedFile& pod) +{ + fromJson(jo.value("url"_ls), pod.url); + fromJson(jo.value("key"_ls), pod.key); + fromJson(jo.value("iv"_ls), pod.iv); + fromJson(jo.value("hashes"_ls), pod.hashes); + fromJson(jo.value("v"_ls), pod.v); +} + +void JsonObjectConverter<JWK>::dumpTo(QJsonObject &jo, const JWK &pod) +{ + addParam<>(jo, QStringLiteral("kty"), pod.kty); + addParam<>(jo, QStringLiteral("key_ops"), pod.keyOps); + addParam<>(jo, QStringLiteral("alg"), pod.alg); + addParam<>(jo, QStringLiteral("k"), pod.k); + addParam<>(jo, QStringLiteral("ext"), pod.ext); +} + +void JsonObjectConverter<JWK>::fillFrom(const QJsonObject &jo, JWK &pod) +{ + fromJson(jo.value("kty"_ls), pod.kty); + fromJson(jo.value("key_ops"_ls), pod.keyOps); + fromJson(jo.value("alg"_ls), pod.alg); + fromJson(jo.value("k"_ls), pod.k); + fromJson(jo.value("ext"_ls), pod.ext); +} diff --git a/lib/events/encryptedfile.h b/lib/events/encryptedfile.h index 24ac9de1..0558563f 100644 --- a/lib/events/encryptedfile.h +++ b/lib/events/encryptedfile.h @@ -29,7 +29,7 @@ public: bool ext; }; -struct EncryptedFile +struct QUOTIENT_API EncryptedFile { Q_GADGET Q_PROPERTY(QUrl url MEMBER url CONSTANT) @@ -44,45 +44,19 @@ public: QString iv; QHash<QString, QString> hashes; QString v; + + QByteArray decryptFile(const QByteArray &ciphertext) const; }; template <> -struct JsonObjectConverter<EncryptedFile> { - static void dumpTo(QJsonObject& jo, const EncryptedFile& pod) - { - addParam<>(jo, QStringLiteral("url"), pod.url); - addParam<>(jo, QStringLiteral("key"), pod.key); - addParam<>(jo, QStringLiteral("iv"), pod.iv); - addParam<>(jo, QStringLiteral("hashes"), pod.hashes); - addParam<>(jo, QStringLiteral("v"), pod.v); - } - static void fillFrom(const QJsonObject& jo, EncryptedFile& pod) - { - fromJson(jo.value("url"_ls), pod.url); - fromJson(jo.value("key"_ls), pod.key); - fromJson(jo.value("iv"_ls), pod.iv); - fromJson(jo.value("hashes"_ls), pod.hashes); - fromJson(jo.value("v"_ls), pod.v); - } +struct QUOTIENT_API JsonObjectConverter<EncryptedFile> { + static void dumpTo(QJsonObject& jo, const EncryptedFile& pod); + static void fillFrom(const QJsonObject& jo, EncryptedFile& pod); }; template <> -struct JsonObjectConverter<JWK> { - static void dumpTo(QJsonObject& jo, const JWK& pod) - { - addParam<>(jo, QStringLiteral("kty"), pod.kty); - addParam<>(jo, QStringLiteral("key_ops"), pod.keyOps); - addParam<>(jo, QStringLiteral("alg"), pod.alg); - addParam<>(jo, QStringLiteral("k"), pod.k); - addParam<>(jo, QStringLiteral("ext"), pod.ext); - } - static void fillFrom(const QJsonObject& jo, JWK& pod) - { - fromJson(jo.value("kty"_ls), pod.kty); - fromJson(jo.value("key_ops"_ls), pod.keyOps); - fromJson(jo.value("alg"_ls), pod.alg); - fromJson(jo.value("k"_ls), pod.k); - fromJson(jo.value("ext"_ls), pod.ext); - } +struct QUOTIENT_API JsonObjectConverter<JWK> { + static void dumpTo(QJsonObject& jo, const JWK& pod); + static void fillFrom(const QJsonObject& jo, JWK& pod); }; } // namespace Quotient diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp index aa05a96e..6272c668 100644 --- a/lib/events/encryptionevent.cpp +++ b/lib/events/encryptionevent.cpp @@ -4,7 +4,7 @@ #include "encryptionevent.h" -#include "e2ee.h" +#include "e2ee/e2ee.h" #include <array> diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp index 4ce130a6..9d7edf20 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -74,6 +74,7 @@ void FileInfo::fillInfoJson(QJsonObject* infoJson) const infoJson->insert(QStringLiteral("size"), payloadSize); if (mimeType.isValid()) infoJson->insert(QStringLiteral("mimetype"), mimeType.name()); + //TODO add encryptedfile } ImageInfo::ImageInfo(const QFileInfo& fi, QSize imageSize) diff --git a/lib/events/keyverificationevent.cpp b/lib/events/keyverificationevent.cpp new file mode 100644 index 00000000..4803955d --- /dev/null +++ b/lib/events/keyverificationevent.cpp @@ -0,0 +1,164 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "keyverificationevent.h" + +using namespace Quotient; + +KeyVerificationRequestEvent::KeyVerificationRequestEvent(const QJsonObject &obj) + : Event(typeId(), obj) +{} + +QString KeyVerificationRequestEvent::fromDevice() const +{ + return contentPart<QString>("from_device"_ls); +} + +QString KeyVerificationRequestEvent::transactionId() const +{ + return contentPart<QString>("transaction_id"_ls); +} + +QStringList KeyVerificationRequestEvent::methods() const +{ + return contentPart<QStringList>("methods"_ls); +} + +uint64_t KeyVerificationRequestEvent::timestamp() const +{ + return contentPart<double>("timestamp"_ls); +} + +KeyVerificationStartEvent::KeyVerificationStartEvent(const QJsonObject &obj) + : Event(typeId(), obj) +{} + +QString KeyVerificationStartEvent::fromDevice() const +{ + return contentPart<QString>("from_device"_ls); +} + +QString KeyVerificationStartEvent::transactionId() const +{ + return contentPart<QString>("transaction_id"_ls); +} + +QString KeyVerificationStartEvent::method() const +{ + return contentPart<QString>("method"_ls); +} + +Omittable<QString> KeyVerificationStartEvent::nextMethod() const +{ + return contentPart<Omittable<QString>>("method_ls"); +} + +QStringList KeyVerificationStartEvent::keyAgreementProtocols() const +{ + Q_ASSERT(method() == QStringLiteral("m.sas.v1")); + return contentPart<QStringList>("key_agreement_protocols"_ls); +} + +QStringList KeyVerificationStartEvent::hashes() const +{ + Q_ASSERT(method() == QStringLiteral("m.sas.v1")); + return contentPart<QStringList>("hashes"_ls); + +} + +QStringList KeyVerificationStartEvent::messageAuthenticationCodes() const +{ + Q_ASSERT(method() == QStringLiteral("m.sas.v1")); + return contentPart<QStringList>("message_authentication_codes"_ls); +} + +QString KeyVerificationStartEvent::shortAuthenticationString() const +{ + return contentPart<QString>("short_authentification_string"_ls); +} + +KeyVerificationAcceptEvent::KeyVerificationAcceptEvent(const QJsonObject &obj) + : Event(typeId(), obj) +{} + +QString KeyVerificationAcceptEvent::transactionId() const +{ + return contentPart<QString>("transaction_id"_ls); +} + +QString KeyVerificationAcceptEvent::method() const +{ + return contentPart<QString>("method"_ls); +} + +QString KeyVerificationAcceptEvent::keyAgreementProtocol() const +{ + return contentPart<QString>("key_agreement_protocol"_ls); +} + +QString KeyVerificationAcceptEvent::hashData() const +{ + return contentPart<QString>("hash"_ls); +} + +QStringList KeyVerificationAcceptEvent::shortAuthenticationString() const +{ + return contentPart<QStringList>("short_authentification_string"_ls); +} + +QString KeyVerificationAcceptEvent::commitement() const +{ + return contentPart<QString>("commitment"_ls); +} + +KeyVerificationCancelEvent::KeyVerificationCancelEvent(const QJsonObject &obj) + : Event(typeId(), obj) +{} + +QString KeyVerificationCancelEvent::transactionId() const +{ + return contentPart<QString>("transaction_id"_ls); +} + +QString KeyVerificationCancelEvent::reason() const +{ + return contentPart<QString>("reason"_ls); +} + +QString KeyVerificationCancelEvent::code() const +{ + return contentPart<QString>("code"_ls); +} + +KeyVerificationKeyEvent::KeyVerificationKeyEvent(const QJsonObject &obj) + : Event(typeId(), obj) +{} + +QString KeyVerificationKeyEvent::transactionId() const +{ + return contentPart<QString>("transaction_id"_ls); +} + +QString KeyVerificationKeyEvent::key() const +{ + return contentPart<QString>("key"_ls); +} + +KeyVerificationMacEvent::KeyVerificationMacEvent(const QJsonObject &obj) + : Event(typeId(), obj) +{} + +QString KeyVerificationMacEvent::transactionId() const +{ + return contentPart<QString>("transaction_id"_ls); +} + +QString KeyVerificationMacEvent::keys() const +{ + return contentPart<QString>("keys"_ls); +} + +QHash<QString, QString> KeyVerificationMacEvent::mac() const +{ + return contentPart<QHash<QString, QString>>("mac"_ls); +} diff --git a/lib/events/keyverificationevent.h b/lib/events/keyverificationevent.h new file mode 100644 index 00000000..497e56a2 --- /dev/null +++ b/lib/events/keyverificationevent.h @@ -0,0 +1,161 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "event.h" + +namespace Quotient { + +/// Requests a key verification with another user's devices. +/// Typically sent as a to-device event. +class QUOTIENT_API KeyVerificationRequestEvent : public Event { +public: + DEFINE_EVENT_TYPEID("m.key.verification.request", KeyVerificationRequestEvent) + + explicit KeyVerificationRequestEvent(const QJsonObject& obj); + + /// The device ID which is initiating the request. + QString fromDevice() const; + + /// An opaque identifier for the verification request. Must + /// be unique with respect to the devices involved. + QString transactionId() const; + + /// The verification methods supported by the sender. + QStringList methods() const; + + /// The POSIX timestamp in milliseconds for when the request was + /// made. If the request is in the future by more than 5 minutes or + /// more than 10 minutes in the past, the message should be ignored + /// by the receiver. + uint64_t timestamp() const; +}; +REGISTER_EVENT_TYPE(KeyVerificationRequestEvent) + +/// Begins a key verification process. +class QUOTIENT_API KeyVerificationStartEvent : public Event { +public: + DEFINE_EVENT_TYPEID("m.key.verification.start", KeyVerificationStartEvent) + + explicit KeyVerificationStartEvent(const QJsonObject &obj); + + /// The device ID which is initiating the process. + QString fromDevice() const; + + /// An opaque identifier for the verification request. Must + /// be unique with respect to the devices involved. + QString transactionId() const; + + /// The verification method to use. + QString method() const; + + /// Optional method to use to verify the other user's key with. + Omittable<QString> nextMethod() const; + + // SAS.V1 methods + + /// The key agreement protocols the sending device understands. + /// \note Only exist if method is m.sas.v1 + QStringList keyAgreementProtocols() const; + + /// The hash methods the sending device understands. + /// \note Only exist if method is m.sas.v1 + QStringList hashes() const; + + /// The message authentication codes that the sending device understands. + /// \note Only exist if method is m.sas.v1 + QStringList messageAuthenticationCodes() const; + + /// The SAS methods the sending device (and the sending device's + /// user) understands. + /// \note Only exist if method is m.sas.v1 + QString shortAuthenticationString() const; +}; +REGISTER_EVENT_TYPE(KeyVerificationStartEvent) + +/// Accepts a previously sent m.key.verification.start message. +/// Typically sent as a to-device event. +class QUOTIENT_API KeyVerificationAcceptEvent : public Event { +public: + DEFINE_EVENT_TYPEID("m.key.verification.accept", KeyVerificationAcceptEvent) + + explicit KeyVerificationAcceptEvent(const QJsonObject& obj); + + /// An opaque identifier for the verification process. + QString transactionId() const; + + /// The verification method to use. Must be 'm.sas.v1'. + QString method() const; + + /// The key agreement protocol the device is choosing to use, out of + /// the options in the m.key.verification.start message. + QString keyAgreementProtocol() const; + + /// The hash method the device is choosing to use, out of the + /// options in the m.key.verification.start message. + QString hashData() const; + + /// The message authentication code the device is choosing to use, out + /// of the options in the m.key.verification.start message. + QString messageAuthenticationCode() const; + + /// The SAS methods both devices involved in the verification process understand. + QStringList shortAuthenticationString() const; + + /// The hash (encoded as unpadded base64) of the concatenation of the + /// device's ephemeral public key (encoded as unpadded base64) and the + /// canonical JSON representation of the m.key.verification.start message. + QString commitement() const; +}; +REGISTER_EVENT_TYPE(KeyVerificationAcceptEvent) + +class QUOTIENT_API KeyVerificationCancelEvent : public Event { +public: + DEFINE_EVENT_TYPEID("m.key.verification.cancel", KeyVerificationCancelEvent) + + explicit KeyVerificationCancelEvent(const QJsonObject &obj); + + /// An opaque identifier for the verification process. + QString transactionId() const; + + /// A human readable description of the code. The client should only + /// rely on this string if it does not understand the code. + QString reason() const; + + /// The error code for why the process/request was cancelled by the user. + QString code() const; +}; +REGISTER_EVENT_TYPE(KeyVerificationCancelEvent) + +/// Sends the ephemeral public key for a device to the partner device. +/// Typically sent as a to-device event. +class KeyVerificationKeyEvent : public Event { +public: + DEFINE_EVENT_TYPEID("m.key.verification.key", KeyVerificationKeyEvent) + + explicit KeyVerificationKeyEvent(const QJsonObject &obj); + + /// An opaque identifier for the verification process. + QString transactionId() const; + + /// The device's ephemeral public key, encoded as unpadded base64. + QString key() const; +}; +REGISTER_EVENT_TYPE(KeyVerificationKeyEvent) + +/// Sends the MAC of a device's key to the partner device. +class QUOTIENT_API KeyVerificationMacEvent : public Event { +public: + DEFINE_EVENT_TYPEID("m.key.verification.mac", KeyVerificationMacEvent) + + explicit KeyVerificationMacEvent(const QJsonObject &obj); + + /// An opaque identifier for the verification process. + QString transactionId() const; + + /// The device's ephemeral public key, encoded as unpadded base64. + QString keys() const; + + QHash<QString, QString> mac() const; +}; +REGISTER_EVENT_TYPE(KeyVerificationMacEvent) +} // namespace Quotient diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index 3502e3f7..2f482871 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -122,3 +122,18 @@ CallEventBase::CallEventBase(Event::Type type, const QJsonObject& json) if (callId().isEmpty()) qCWarning(EVENTS) << id() << "is a call event with an empty call id"; } + +#ifdef Quotient_E2EE_ENABLED +void RoomEvent::setOriginalEvent(event_ptr_tt<RoomEvent>&& originalEvent) +{ + _originalEvent = std::move(originalEvent); +} + +const QJsonObject RoomEvent::encryptedJson() const +{ + if(!_originalEvent) { + return {}; + } + return _originalEvent->fullJson(); +} +#endif diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index dcee1170..c4b0131a 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -60,11 +60,21 @@ public: //! callback for that in RoomEvent. void addId(const QString& newId); +#ifdef Quotient_E2EE_ENABLED + void setOriginalEvent(event_ptr_tt<RoomEvent>&& originalEvent); + const RoomEvent* originalEvent() { return _originalEvent.get(); } + const QJsonObject encryptedJson() const; +#endif + protected: void dumpTo(QDebug dbg) const override; private: event_ptr_tt<RedactionEvent> _redactedBecause; + +#ifdef Quotient_E2EE_ENABLED + event_ptr_tt<RoomEvent> _originalEvent; +#endif }; using RoomEventPtr = event_ptr_tt<RoomEvent>; using RoomEvents = EventsArray<RoomEvent>; diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index 4a507ebd..d00fc5f4 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -7,8 +7,12 @@ #include <QtCore/QTemporaryFile> #include <QtNetwork/QNetworkReply> -using namespace Quotient; +#ifdef Quotient_E2EE_ENABLED +# include <QtCore/QCryptographicHash> +# include "events/encryptedfile.h" +#endif +using namespace Quotient; class DownloadFileJob::Private { public: Private() : tempFile(new QTemporaryFile()) {} @@ -20,6 +24,10 @@ public: QScopedPointer<QFile> targetFile; QScopedPointer<QFile> tempFile; + +#ifdef Quotient_E2EE_ENABLED + Omittable<EncryptedFile> encryptedFile; +#endif }; QUrl DownloadFileJob::makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri) @@ -38,6 +46,19 @@ DownloadFileJob::DownloadFileJob(const QString& serverName, setObjectName(QStringLiteral("DownloadFileJob")); } +#ifdef Quotient_E2EE_ENABLED +DownloadFileJob::DownloadFileJob(const QString& serverName, + const QString& mediaId, + const EncryptedFile& file, + const QString& localFilename) + : GetContentJob(serverName, mediaId) + , d(localFilename.isEmpty() ? makeImpl<Private>() + : makeImpl<Private>(localFilename)) +{ + setObjectName(QStringLiteral("DownloadFileJob")); + d->encryptedFile = file; +} +#endif QString DownloadFileJob::targetFileName() const { return (d->targetFile ? d->targetFile : d->tempFile)->fileName(); @@ -52,7 +73,7 @@ void DownloadFileJob::doPrepare() setStatus(FileError, "Could not open the target file for writing"); return; } - if (!d->tempFile->isReadable() && !d->tempFile->open(QIODevice::WriteOnly)) { + if (!d->tempFile->isReadable() && !d->tempFile->open(QIODevice::ReadWrite)) { qCWarning(JOBS) << "Couldn't open the temporary file" << d->tempFile->fileName() << "for writing"; setStatus(FileError, "Could not open the temporary download file"); @@ -100,18 +121,46 @@ void DownloadFileJob::beforeAbandon() BaseJob::Status DownloadFileJob::prepareResult() { if (d->targetFile) { - d->targetFile->close(); - if (!d->targetFile->remove()) { - qCWarning(JOBS) << "Failed to remove the target file placeholder"; - return { FileError, "Couldn't finalise the download" }; +#ifdef Quotient_E2EE_ENABLED + if (d->encryptedFile.has_value()) { + d->tempFile->seek(0); + QByteArray encrypted = d->tempFile->readAll(); + + EncryptedFile file = *d->encryptedFile; + const auto decrypted = file.decryptFile(encrypted); + d->targetFile->write(decrypted); + d->tempFile->remove(); + } else { +#endif + d->targetFile->close(); + if (!d->targetFile->remove()) { + qCWarning(JOBS) << "Failed to remove the target file placeholder"; + return { FileError, "Couldn't finalise the download" }; + } + if (!d->tempFile->rename(d->targetFile->fileName())) { + qCWarning(JOBS) << "Failed to rename" << d->tempFile->fileName() + << "to" << d->targetFile->fileName(); + return { FileError, "Couldn't finalise the download" }; + } +#ifdef Quotient_E2EE_ENABLED } - if (!d->tempFile->rename(d->targetFile->fileName())) { - qCWarning(JOBS) << "Failed to rename" << d->tempFile->fileName() - << "to" << d->targetFile->fileName(); - return { FileError, "Couldn't finalise the download" }; +#endif + } else { +#ifdef Quotient_E2EE_ENABLED + if (d->encryptedFile.has_value()) { + d->tempFile->seek(0); + const auto encrypted = d->tempFile->readAll(); + + EncryptedFile file = *d->encryptedFile; + const auto decrypted = file.decryptFile(encrypted); + d->tempFile->write(decrypted); + } else { +#endif + d->tempFile->close(); +#ifdef Quotient_E2EE_ENABLED } - } else - d->tempFile->close(); +#endif + } qCDebug(JOBS) << "Saved a file as" << targetFileName(); return Success; } diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h index f8c62e4b..ffa3d055 100644 --- a/lib/jobs/downloadfilejob.h +++ b/lib/jobs/downloadfilejob.h @@ -4,6 +4,7 @@ #pragma once #include "csapi/content-repo.h" +#include "events/encryptedfile.h" namespace Quotient { class QUOTIENT_API DownloadFileJob : public GetContentJob { @@ -14,6 +15,9 @@ public: DownloadFileJob(const QString& serverName, const QString& mediaId, const QString& localFilename = {}); +#ifdef Quotient_E2EE_ENABLED + DownloadFileJob(const QString& serverName, const QString& mediaId, const EncryptedFile& file, const QString& localFilename = {}); +#endif QString targetFileName() const; private: diff --git a/lib/logging.cpp b/lib/logging.cpp index 15eac69d..460caced 100644 --- a/lib/logging.cpp +++ b/lib/logging.cpp @@ -19,3 +19,4 @@ LOGGING_CATEGORY(SYNCJOB, "quotient.jobs.sync") LOGGING_CATEGORY(THUMBNAILJOB, "quotient.jobs.thumbnail") LOGGING_CATEGORY(NETWORK, "quotient.network") LOGGING_CATEGORY(PROFILER, "quotient.profiler") +LOGGING_CATEGORY(DATABASE, "quotient.database") diff --git a/lib/logging.h b/lib/logging.h index 5bf050a9..fc0a4c99 100644 --- a/lib/logging.h +++ b/lib/logging.h @@ -19,6 +19,7 @@ Q_DECLARE_LOGGING_CATEGORY(SYNCJOB) Q_DECLARE_LOGGING_CATEGORY(THUMBNAILJOB) Q_DECLARE_LOGGING_CATEGORY(NETWORK) Q_DECLARE_LOGGING_CATEGORY(PROFILER) +Q_DECLARE_LOGGING_CATEGORY(DATABASE) namespace Quotient { // QDebug manipulators diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp index d3cc3c37..1d40c5e1 100644 --- a/lib/mxcreply.cpp +++ b/lib/mxcreply.cpp @@ -3,8 +3,15 @@ #include "mxcreply.h" +#include <QtCore/QBuffer> +#include "accountregistry.h" +#include "connection.h" #include "room.h" +#ifdef Quotient_E2EE_ENABLED +#include "events/encryptedfile.h" +#endif + using namespace Quotient; class MxcReply::Private @@ -14,11 +21,14 @@ public: : m_reply(r) {} QNetworkReply* m_reply; + Omittable<EncryptedFile> m_encryptedFile; + QIODevice* m_device = nullptr; }; MxcReply::MxcReply(QNetworkReply* reply) : d(makeImpl<Private>(reply)) { + d->m_device = d->m_reply; reply->setParent(this); connect(d->m_reply, &QNetworkReply::finished, this, [this]() { setError(d->m_reply->error(), d->m_reply->errorString()); @@ -31,11 +41,33 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) : d(makeImpl<Private>(reply)) { reply->setParent(this); - connect(d->m_reply, &QNetworkReply::finished, this, [this, room, eventId]() { + connect(d->m_reply, &QNetworkReply::finished, this, [this]() { setError(d->m_reply->error(), d->m_reply->errorString()); + +#ifdef Quotient_E2EE_ENABLED + if(!d->m_encryptedFile.has_value()) { + d->m_device = d->m_reply; + } else { + EncryptedFile file = *d->m_encryptedFile; + auto buffer = new QBuffer(this); + buffer->setData(file.decryptFile(d->m_reply->readAll())); + buffer->open(ReadOnly); + d->m_device = buffer; + } setOpenMode(ReadOnly); emit finished(); +#else + d->m_device = d->m_reply; +#endif }); + +#ifdef Quotient_E2EE_ENABLED + auto eventIt = room->findInTimeline(eventId); + if(eventIt != room->historyEdge()) { + auto event = eventIt->viewAs<RoomMessageEvent>(); + d->m_encryptedFile = event->content()->fileInfo()->file; + } +#endif } #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) @@ -62,7 +94,7 @@ MxcReply::MxcReply() qint64 MxcReply::readData(char *data, qint64 maxSize) { - return d->m_reply->read(data, maxSize); + return d->m_device->read(data, maxSize); } void MxcReply::abort() diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index 58c3cc3a..f4e7b1af 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -47,6 +47,17 @@ QList<QSslError> NetworkAccessManager::ignoredSslErrors() const return d->ignoredSslErrors; } +void NetworkAccessManager::ignoreSslErrors(bool ignore) const +{ + if (ignore) { + connect(this, &QNetworkAccessManager::sslErrors, this, [](QNetworkReply *reply, const QList<QSslError> &errors) { + reply->ignoreSslErrors(); + }); + } else { + disconnect(this, &QNetworkAccessManager::sslErrors, this, nullptr); + } +} + void NetworkAccessManager::addIgnoredSslError(const QSslError& error) { d->ignoredSslErrors << error; diff --git a/lib/networkaccessmanager.h b/lib/networkaccessmanager.h index 8ff1c6b5..01b0599d 100644 --- a/lib/networkaccessmanager.h +++ b/lib/networkaccessmanager.h @@ -17,6 +17,7 @@ public: QList<QSslError> ignoredSslErrors() const; void addIgnoredSslError(const QSslError& error); void clearIgnoredSslErrors(); + void ignoreSslErrors(bool ignore = true) const; /** Get a pointer to the singleton */ static NetworkAccessManager* instance(); diff --git a/lib/quotient_export.h b/lib/quotient_export.h index 5a6edb0e..56767443 100644 --- a/lib/quotient_export.h +++ b/lib/quotient_export.h @@ -3,6 +3,8 @@ #pragma once +#include <QtCore/qglobal.h> + #ifdef QUOTIENT_STATIC # define QUOTIENT_API # define QUOTIENT_HIDDEN diff --git a/lib/room.cpp b/lib/room.cpp index abd6110c..ea9915c3 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -12,7 +12,6 @@ #include "avatar.h" #include "connection.h" #include "converters.h" -#include "e2ee.h" #include "syncdata.h" #include "user.h" #include "eventstats.h" @@ -66,13 +65,15 @@ #include <functional> #ifdef Quotient_E2EE_ENABLED -#include <account.h> // QtOlm -#include <errors.h> // QtOlm -#include <groupsession.h> // QtOlm +#include "e2ee/e2ee.h" +#include "e2ee/qolmaccount.h" +#include "e2ee/qolmerrors.h" +#include "e2ee/qolminboundsession.h" +#include "database.h" #endif // Quotient_E2EE_ENABLED + using namespace Quotient; -using namespace QtOlm; using namespace std::placeholders; using std::move; #if !(defined __GLIBCXX__ && __GLIBCXX__ <= 20150123) @@ -138,6 +139,8 @@ public: QString prevBatch; QPointer<GetRoomEventsJob> eventsHistoryJob; QPointer<GetMembersByRoomJob> allMembersJob; + // Map from megolm sessionId to set of eventIds + UnorderedMap<QString, QSet<QString>> undecryptedEvents; struct FileTransferPrivateInfo { FileTransferPrivateInfo() = default; @@ -334,40 +337,27 @@ public: bool isLocalUser(const User* u) const { return u == q->localUser(); } #ifdef Quotient_E2EE_ENABLED - // A map from <sessionId, messageIndex> to <event_id, origin_server_ts> - QHash<QPair<QString, uint32_t>, QPair<QString, QDateTime>> - groupSessionIndexRecord; // TODO: cache - // A map from senderKey to a map of sessionId to InboundGroupSession - // Not using QMultiHash, because we want to quickly return - // a number of relations for a given event without enumerating them. - QHash<QPair<QString, QString>, InboundGroupSession*> groupSessions; // TODO: - // cache + // A map from (senderKey, sessionId) to InboundGroupSession + UnorderedMap<std::pair<QString, QString>, QOlmInboundGroupSessionPtr> groupSessions; + bool addInboundGroupSession(QString senderKey, QString sessionId, QString sessionKey) { - if (groupSessions.contains({ senderKey, sessionId })) { - qCDebug(E2EE) << "Inbound Megolm session" << sessionId + if (groupSessions.find({senderKey, sessionId}) != groupSessions.end()) { + qCWarning(E2EE) << "Inbound Megolm session" << sessionId << "with senderKey" << senderKey << "already exists"; return false; } - InboundGroupSession* megolmSession; - try { - megolmSession = new InboundGroupSession(sessionKey.toLatin1(), - InboundGroupSession::Init, - q); - } catch (OlmError* e) { - qCDebug(E2EE) << "Unable to create new InboundGroupSession" - << e->what(); + auto megolmSession = QOlmInboundGroupSession::create(sessionKey.toLatin1()); + if (megolmSession->sessionId() != sessionId) { + qCWarning(E2EE) << "Session ID mismatch in m.room_key event sent " + "from sender with key" << senderKey; return false; } - if (megolmSession->id() != sessionId) { - qCDebug(E2EE) << "Session ID mismatch in m.room_key event sent " - "from sender with key" - << senderKey; - return false; - } - groupSessions.insert({ senderKey, sessionId }, megolmSession); + qCWarning(E2EE) << "Adding inbound session"; + connection->saveMegolmSession(q, senderKey, megolmSession.get()); + groupSessions[{senderKey, sessionId}] = std::move(megolmSession); return true; } @@ -377,44 +367,31 @@ public: const QString& eventId, QDateTime timestamp) { - std::pair<QString, uint32_t> decrypted; - QPair<QString, QString> senderSessionPairKey = - qMakePair(senderKey, sessionId); - if (!groupSessions.contains(senderSessionPairKey)) { - qCDebug(E2EE) << "Unable to decrypt event" << eventId - << "The sender's device has not sent us the keys for " - "this message"; + auto groupSessionIt = groupSessions.find({ senderKey, sessionId }); + if (groupSessionIt == groupSessions.end()) { + // qCWarning(E2EE) << "Unable to decrypt event" << eventId + // << "The sender's device has not sent us the keys for " + // "this message"; return QString(); } - InboundGroupSession* senderSession = - groupSessions.value(senderSessionPairKey); - if (!senderSession) { - qCDebug(E2EE) << "Unable to decrypt event" << eventId - << "senderSessionPairKey:" << senderSessionPairKey; + auto& senderSession = groupSessionIt->second; + auto decryptResult = senderSession->decrypt(cipher); + if(std::holds_alternative<QOlmError>(decryptResult)) { + qCWarning(E2EE) << "Unable to decrypt event" << eventId + << "with matching megolm session:" << std::get<QOlmError>(decryptResult); return QString(); } - try { - decrypted = senderSession->decrypt(cipher); - } catch (OlmError* e) { - qCDebug(E2EE) << "Unable to decrypt event" << eventId - << "with matching megolm session:" << e->what(); - return QString(); - } - QPair<QString, QDateTime> properties = groupSessionIndexRecord.value( - qMakePair(senderSession->id(), decrypted.second)); - if (properties.first.isEmpty()) { - groupSessionIndexRecord.insert(qMakePair(senderSession->id(), - decrypted.second), - qMakePair(eventId, timestamp)); + const auto& [content, index] = std::get<std::pair<QString, uint32_t>>(decryptResult); + const auto& [recordEventId, ts] = q->connection()->database()->groupSessionIndexRecord(q->id(), senderSession->sessionId(), index); + if (recordEventId.isEmpty()) { + q->connection()->database()->addGroupSessionIndexRecord(q->id(), senderSession->sessionId(), index, eventId, timestamp.toMSecsSinceEpoch()); } else { - if ((properties.first != eventId) - || (properties.second != timestamp)) { - qCDebug(E2EE) << "Detected a replay attack on event" << eventId; + if ((eventId != recordEventId) || (ts != timestamp.toMSecsSinceEpoch())) { + qCWarning(E2EE) << "Detected a replay attack on event" << eventId; return QString(); } } - - return decrypted.first; + return content; } #endif // Quotient_E2EE_ENABLED @@ -440,6 +417,21 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) emit baseStateLoaded(); return this == r; // loadedRoomState fires only once per room }); +#ifdef Quotient_E2EE_ENABLED + connectSingleShot(this, &Room::encryption, this, [this, connection](){ + connection->encryptionUpdate(this); + }); + connect(this, &Room::userAdded, this, [this, connection](){ + if(usesEncryption()) { + connection->encryptionUpdate(this); + } + }); + d->groupSessions = connection->loadRoomMegolmSessions(this); + + connect(this, &Room::beforeDestruction, this, [=](){ + connection->database()->clearRoomData(id); + }); +#endif qCDebug(STATE) << "New" << terse << initialJoinState << "Room:" << id; } @@ -1482,20 +1474,20 @@ RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; return {}; #else // Quotient_E2EE_ENABLED - if (encryptedEvent.algorithm() == MegolmV1AesSha2AlgoKey) { - QString decrypted = d->groupSessionDecryptMessage( - encryptedEvent.ciphertext(), encryptedEvent.senderKey(), - encryptedEvent.sessionId(), encryptedEvent.id(), - encryptedEvent.originTimestamp()); - if (decrypted.isEmpty()) { - return {}; - } - return makeEvent<RoomMessageEvent>( - QJsonDocument::fromJson(decrypted.toUtf8()).object()); + if (encryptedEvent.algorithm() != MegolmV1AesSha2AlgoKey) { + qWarning(E2EE) << "Algorithm of the encrypted event with id" + << encryptedEvent.id() << "is not decryptable by the current device"; + return {}; } - qCDebug(E2EE) << "Algorithm of the encrypted event with id" - << encryptedEvent.id() << "is not for the current device"; - return {}; + QString decrypted = d->groupSessionDecryptMessage( + encryptedEvent.ciphertext(), encryptedEvent.senderKey(), + encryptedEvent.sessionId(), encryptedEvent.id(), + encryptedEvent.originTimestamp()); + if (decrypted.isEmpty()) { + // qCWarning(E2EE) << "Encrypted message is empty"; + return {}; + } + return encryptedEvent.createDecrypted(decrypted); #endif // Quotient_E2EE_ENABLED } @@ -1513,8 +1505,25 @@ void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, } if (d->addInboundGroupSession(senderKey, roomKeyEvent.sessionId(), roomKeyEvent.sessionKey())) { - qCDebug(E2EE) << "added new inboundGroupSession:" - << d->groupSessions.count(); + qCWarning(E2EE) << "added new inboundGroupSession:" + << d->groupSessions.size(); + for (const auto& eventId : d->undecryptedEvents[roomKeyEvent.sessionId()]) { + const auto pIdx = d->eventsIndex.constFind(eventId); + if (pIdx == d->eventsIndex.cend()) + continue; + auto& ti = d->timeline[Timeline::size_type(*pIdx - minTimelineIndex())]; + if (auto encryptedEvent = ti.viewAs<EncryptedEvent>()) { + auto decrypted = decryptMessage(*encryptedEvent); + if(decrypted) { + // The reference will survive the pointer being moved + auto& decryptedEvent = *decrypted; + auto oldEvent = ti.replaceEvent(std::move(decrypted)); + decryptedEvent.setOriginalEvent(std::move(oldEvent)); + emit replacedEvent(ti.event(), decrypted->originalEvent()); + d->undecryptedEvents[roomKeyEvent.sessionId()] -= eventId; + } + } + } } #endif // Quotient_E2EE_ENABLED } @@ -1905,7 +1914,6 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) return; } it->setDeparted(); - qCDebug(EVENTS) << "Event txn" << txnId << "has departed"; emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); }); Room::connect(call, &BaseJob::failure, q, @@ -2343,7 +2351,17 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) filePath = QDir::tempPath() % '/' % filePath; qDebug(MAIN) << "File path:" << filePath; } - auto job = connection()->downloadFile(fileUrl, filePath); + DownloadFileJob *job = nullptr; +#ifdef Quotient_E2EE_ENABLED + if(fileInfo->file.has_value()) { + auto file = *fileInfo->file; + job = connection()->downloadFile(fileUrl, file, filePath); + } else { +#endif + job = connection()->downloadFile(fileUrl, filePath); +#ifdef Quotient_E2EE_ENABLED + } +#endif if (isJobPending(job)) { // If there was a previous transfer (completed or failed), overwrite it. d->fileTransfers[eventId] = { job, job->targetFileName() }; @@ -2595,6 +2613,21 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) QElapsedTimer et; et.start(); + +#ifdef Quotient_E2EE_ENABLED + for(long unsigned int i = 0; i < events.size(); i++) { + if(auto* encrypted = eventCast<EncryptedEvent>(events[i])) { + auto decrypted = q->decryptMessage(*encrypted); + if(decrypted) { + auto oldEvent = std::exchange(events[i], std::move(decrypted)); + events[i]->setOriginalEvent(std::move(oldEvent)); + } else { + undecryptedEvents[encrypted->sessionId()] += encrypted->id(); + } + } + } +#endif + { // Pre-process redactions and edits so that events that get // redacted/replaced in the same batch landed in the timeline already @@ -2747,6 +2780,21 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) return; Changes changes {}; + +#ifdef Quotient_E2EE_ENABLED + for(long unsigned int i = 0; i < events.size(); i++) { + if(auto* encrypted = eventCast<EncryptedEvent>(events[i])) { + auto decrypted = q->decryptMessage(*encrypted); + if(decrypted) { + auto oldEvent = std::exchange(events[i], std::move(decrypted)); + events[i]->setOriginalEvent(std::move(oldEvent)); + } else { + undecryptedEvents[encrypted->sessionId()] += encrypted->id(); + } + } + } +#endif + // In case of lazy-loading new members may be loaded with historical // messages. Also, the cache doesn't store events with empty content; // so when such events show up in the timeline they should be properly diff --git a/lib/settings.cpp b/lib/settings.cpp index 2491d89d..510d253c 100644 --- a/lib/settings.cpp +++ b/lib/settings.cpp @@ -138,18 +138,12 @@ void AccountSettings::clearAccessToken() QByteArray AccountSettings::encryptionAccountPickle() { - QString passphrase = ""; // FIXME: add QtKeychain return value("encryption_account_pickle", "").toByteArray(); } void AccountSettings::setEncryptionAccountPickle( const QByteArray& encryptionAccountPickle) { - qCWarning(MAIN) - << "Saving encryption_account_pickle to QSettings is insecure." - " Developers, do it manually or contribute to share QtKeychain " - "logic to libQuotient."; - QString passphrase = ""; // FIXME: add QtKeychain setValue("encryption_account_pickle", encryptionAccountPickle); } diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index b0cd8e4d..78957cbe 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -99,6 +99,34 @@ SyncRoomData::SyncRoomData(QString roomId_, JoinState joinState, fromJson(unreadJson.value(HighlightCountKey), highlightCount); } +QDebug Quotient::operator<<(QDebug dbg, const DevicesList& devicesList) +{ + QDebugStateSaver _(dbg); + QStringList sl; + if (!devicesList.changed.isEmpty()) + sl << QStringLiteral("changed: %1").arg(devicesList.changed.join(", ")); + if (!devicesList.left.isEmpty()) + sl << QStringLiteral("left %1").arg(devicesList.left.join(", ")); + dbg.nospace().noquote() << sl.join(QStringLiteral("; ")); + return dbg; +} + +void JsonObjectConverter<DevicesList>::dumpTo(QJsonObject& jo, + const DevicesList& rs) +{ + addParam<IfNotEmpty>(jo, QStringLiteral("changed"), + rs.changed); + addParam<IfNotEmpty>(jo, QStringLiteral("left"), + rs.left); +} + +void JsonObjectConverter<DevicesList>::fillFrom(const QJsonObject& jo, + DevicesList& rs) +{ + fromJson(jo["changed"_ls], rs.changed); + fromJson(jo["left"_ls], rs.left); +} + SyncData::SyncData(const QString& cacheFileName) { QFileInfo cacheFileInfo { cacheFileName }; @@ -133,6 +161,8 @@ std::pair<int, int> SyncData::cacheVersion() return { MajorCacheVersion, 2 }; } +DevicesList&& SyncData::takeDevicesList() { return std::move(devicesList); } + QJsonObject SyncData::loadJson(const QString& fileName) { QFile roomFile { fileName }; @@ -175,6 +205,10 @@ void SyncData::parseJson(const QJsonObject& json, const QString& baseDir) fromJson(json.value("device_one_time_keys_count"_ls), deviceOneTimeKeysCount_); + if(json.contains("device_lists")) { + fromJson(json.value("device_lists"), devicesList); + } + auto rooms = json.value("rooms"_ls).toObject(); auto totalRooms = 0; auto totalEvents = 0; diff --git a/lib/syncdata.h b/lib/syncdata.h index e29540c2..6b70140d 100644 --- a/lib/syncdata.h +++ b/lib/syncdata.h @@ -41,6 +41,27 @@ struct JsonObjectConverter<RoomSummary> { static void fillFrom(const QJsonObject& jo, RoomSummary& rs); }; +/// Information on e2e device updates. Note: only present on an +/// incremental sync. +struct DevicesList { + /// List of users who have updated their device identity keys, or who + /// now share an encrypted room with the client since the previous + /// sync response. + QStringList changed; + + /// List of users with whom we do not share any encrypted rooms + /// anymore since the previous sync response. + QStringList left; +}; + +QDebug operator<<(QDebug dhg, const DevicesList& devicesList); + +template <> +struct JsonObjectConverter<DevicesList> { + static void dumpTo(QJsonObject &jo, const DevicesList &dev); + static void fillFrom(const QJsonObject& jo, DevicesList& rs); +}; + class SyncRoomData { public: QString roomId; @@ -85,6 +106,7 @@ public: return deviceOneTimeKeysCount_; } SyncDataList&& takeRoomData(); + DevicesList&& takeDevicesList(); QString nextBatch() const { return nextBatch_; } @@ -102,6 +124,7 @@ private: SyncDataList roomData; QStringList unresolvedRoomIds; QHash<QString, int> deviceOneTimeKeysCount_; + DevicesList devicesList; static QJsonObject loadJson(const QString& fileName); }; |