[network/ruqola] src: e2ekeymanager++

Laurent Montel <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 3803e633de8038d241173bfede8b0f6a128e2307 by Laurent Montel.
Committed on 03/08/2026 at 06:00.
Pushed by mlaurent into branch 'master'.

e2ekeymanager++

M  +75   -0    src/core/autotests/e2ekeymanagertest.cpp
M  +2    -0    src/core/autotests/e2ekeymanagertest.h
M  +63   -0    src/core/encryption/e2ekeymanager.cpp
M  +5    -0    src/core/encryption/e2ekeymanager.h
M  +19   -0    src/core/rocketchataccount.cpp
M  +3    -0    src/core/rocketchataccount.h
M  +19   -2    src/widgets/room/roomwidget.cpp

https://invent.kde.org/network/ruqola/-/commit/3803e633de8038d241173bfede8b0f6a128e2307

diff --git a/src/core/autotests/e2ekeymanagertest.cpp b/src/core/autotests/e2ekeymanagertest.cpp
index 461c96fd20..527b56e65a 100644
--- a/src/core/autotests/e2ekeymanagertest.cpp
+++ b/src/core/autotests/e2ekeymanagertest.cpp
@@ -8,6 +8,7 @@
 #include "encryption/e2ekeymanager.h"
 
 #include "config-ruqola.h"
+#include "encryption/encryptionutils.h"
 #include "localdatabase/e2edatabase.h"
 #include "localdatabase/localdatabasemanager.h"
 #include "rocketchataccount.h"
@@ -148,4 +149,78 @@ void E2eKeyManagerTest::shouldHandleMissingOrMalformedServerKeys()
     QVERIFY(account.localDatabaseManager()->e2EDatabase()->deleteKey(u"test-e2e-user-generation"_s));
 }
 
+void E2eKeyManagerTest::shouldDecodeEncryptionKeyWithValidPassword()
+{
+#if !USE_E2E_SUPPORT
+    QSKIP("E2E support is disabled");
+#else
+    QTemporaryDir tempDir;
+    QVERIFY(tempDir.isValid());
+
+    RocketChatAccount account(tempDir.filePath(u"account.ini"_s));
+    account.settings()->setUserId("test-e2e-user-decode-ok"_ba);
+
+    const QString userId = u"test-e2e-user-decode-ok"_s;
+    const QString password = u"my-test-password"_s;
+    const auto rsaKeyPair = EncryptionUtils::generateRSAKey();
+    QVERIFY(!rsaKeyPair.privateKey.isEmpty());
+
+    const QByteArray masterKey = EncryptionUtils::getMasterKey(password, userId);
+    QVERIFY(!masterKey.isEmpty());
+    const QByteArray encryptedPrivateKey = EncryptionUtils::encryptPrivateKey(rsaKeyPair.privateKey, masterKey);
+    QVERIFY(!encryptedPrivateKey.isEmpty());
+
+    QVERIFY(account.localDatabaseManager()->e2EDatabase()->saveKey(userId, encryptedPrivateKey, rsaKeyPair.publicKey));
+
+    E2eKeyManager manager(&account);
+    manager.setStatus(E2eKeyManager::Status::NeedToDecryptKey);
+
+    QSignalSpy doneSpy(&manager, &E2eKeyManager::decodeEncryptionKeyDone);
+    QSignalSpy failedSpy(&manager, &E2eKeyManager::failedDecodeEncryptionKey);
+    QVERIFY(manager.decodeEncryptionKey(password));
+    QCOMPARE(manager.status(), E2eKeyManager::Status::KeyDecrypted);
+    QCOMPARE(doneSpy.count(), 1);
+    QCOMPARE(failedSpy.count(), 0);
+
+    QVERIFY(account.localDatabaseManager()->e2EDatabase()->deleteKey(userId));
+#endif
+}
+
+void E2eKeyManagerTest::shouldFailDecodeEncryptionKeyWithWrongPassword()
+{
+#if !USE_E2E_SUPPORT
+    QSKIP("E2E support is disabled");
+#else
+    QTemporaryDir tempDir;
+    QVERIFY(tempDir.isValid());
+
+    RocketChatAccount account(tempDir.filePath(u"account.ini"_s));
+    account.settings()->setUserId("test-e2e-user-decode-ko"_ba);
+
+    const QString userId = u"test-e2e-user-decode-ko"_s;
+    const QString password = u"right-password"_s;
+    const auto rsaKeyPair = EncryptionUtils::generateRSAKey();
+    QVERIFY(!rsaKeyPair.privateKey.isEmpty());
+
+    const QByteArray masterKey = EncryptionUtils::getMasterKey(password, userId);
+    QVERIFY(!masterKey.isEmpty());
+    const QByteArray encryptedPrivateKey = EncryptionUtils::encryptPrivateKey(rsaKeyPair.privateKey, masterKey);
+    QVERIFY(!encryptedPrivateKey.isEmpty());
+
+    QVERIFY(account.localDatabaseManager()->e2EDatabase()->saveKey(userId, encryptedPrivateKey, rsaKeyPair.publicKey));
+
+    E2eKeyManager manager(&account);
+    manager.setStatus(E2eKeyManager::Status::NeedToDecryptKey);
+
+    QSignalSpy doneSpy(&manager, &E2eKeyManager::decodeEncryptionKeyDone);
+    QSignalSpy failedSpy(&manager, &E2eKeyManager::failedDecodeEncryptionKey);
+    QVERIFY(!manager.decodeEncryptionKey(u"wrong-password"_s));
+    QCOMPARE(manager.status(), E2eKeyManager::Status::NeedToDecryptKey);
+    QCOMPARE(doneSpy.count(), 0);
+    QCOMPARE(failedSpy.count(), 1);
+
+    QVERIFY(account.localDatabaseManager()->e2EDatabase()->deleteKey(userId));
+#endif
+}
+
 #include "moc_e2ekeymanagertest.cpp"
diff --git a/src/core/autotests/e2ekeymanagertest.h b/src/core/autotests/e2ekeymanagertest.h
index 3827c7e71b..f691027f45 100644
--- a/src/core/autotests/e2ekeymanagertest.h
+++ b/src/core/autotests/e2ekeymanagertest.h
@@ -21,4 +21,6 @@ private Q_SLOTS:
     void shouldSetNeedToDecryptStatusFromBase64StringPayload();
     void shouldSetNeedToDecryptStatusFromBinaryObjectPayload();
     void shouldHandleMissingOrMalformedServerKeys();
+    void shouldDecodeEncryptionKeyWithValidPassword();
+    void shouldFailDecodeEncryptionKeyWithWrongPassword();
 };
diff --git a/src/core/encryption/e2ekeymanager.cpp b/src/core/encryption/e2ekeymanager.cpp
index dde197dc0b..161f6da43e 100644
--- a/src/core/encryption/e2ekeymanager.cpp
+++ b/src/core/encryption/e2ekeymanager.cpp
@@ -40,6 +40,69 @@ void E2eKeyManager::decodeEncryptionKey()
     }
 }
 
+bool E2eKeyManager::decodeEncryptionKey(const QString &password)
+{
+#if USE_E2E_SUPPORT
+    if (!mAccount || password.isEmpty()) {
+        setStatus(Status::NeedToDecryptKey);
+        Q_EMIT failedDecodeEncryptionKey();
+        return false;
+    }
+
+    const QString userId = QString::fromLatin1(mAccount->settings()->userId());
+    if (userId.isEmpty()) {
+        setStatus(Status::NeedToDecryptKey);
+        Q_EMIT failedDecodeEncryptionKey();
+        return false;
+    }
+
+    QByteArray encryptedPrivateKey;
+    QByteArray publicKey;
+    if (!mAccount->localDatabaseManager()->e2EDatabase()->loadKey(userId, encryptedPrivateKey, publicKey)) {
+        qCWarning(RUQOLA_ENCRYPTION_LOG) << "Unable to decode E2E key: no local encrypted private key found";
+        setStatus(Status::NeedToDecryptKey);
+        Q_EMIT failedDecodeEncryptionKey();
+        return false;
+    }
+
+    const QByteArray masterKey = EncryptionUtils::getMasterKey(password, userId);
+    if (masterKey.isEmpty()) {
+        setStatus(Status::NeedToDecryptKey);
+        Q_EMIT failedDecodeEncryptionKey();
+        return false;
+    }
+
+    const QByteArray privateKeyPem = EncryptionUtils::decryptPrivateKey(encryptedPrivateKey, masterKey);
+    if (privateKeyPem.isEmpty()) {
+        setStatus(Status::NeedToDecryptKey);
+        Q_EMIT failedDecodeEncryptionKey();
+        return false;
+    }
+
+    RSA *privateKey = EncryptionUtils::privateKeyFromPEM(privateKeyPem);
+    if (!privateKey) {
+        setStatus(Status::NeedToDecryptKey);
+        Q_EMIT failedDecodeEncryptionKey();
+        return false;
+    }
+
+    RSA_free(privateKey);
+    mDecodedPrivateKey = privateKeyPem;
+    setStatus(Status::KeyDecrypted);
+    Q_EMIT decodeEncryptionKeyDone();
+    return true;
+#else
+    Q_UNUSED(password)
+    return false;
+#endif
+}
+
+void E2eKeyManager::postponeDecryption()
+{
+    setStatus(Status::DecryptionPostponned);
+    Q_EMIT decodeEncryptionKeyPostponed();
+}
+
 QString E2eKeyManager::generateRandomPassword() const
 {
 #if USE_E2E_SUPPORT
diff --git a/src/core/encryption/e2ekeymanager.h b/src/core/encryption/e2ekeymanager.h
index ad9839c6a5..2308ac1937 100644
--- a/src/core/encryption/e2ekeymanager.h
+++ b/src/core/encryption/e2ekeymanager.h
@@ -25,6 +25,8 @@ public:
     ~E2eKeyManager() override;
 
     void decodeEncryptionKey();
+    [[nodiscard]] bool decodeEncryptionKey(const QString &password);
+    void postponeDecryption();
 
     void fetchMyKeys();
 
@@ -43,11 +45,14 @@ public:
 Q_SIGNALS:
     void needDecodeEncryptionKey();
     void failedDecodeEncryptionKey();
+    void decodeEncryptionKeyDone();
+    void decodeEncryptionKeyPostponed();
     void verifyKeyDone();
 
 private:
     LIBRUQOLACORE_NO_EXPORT void verifyExistingKey(const QJsonObject &json);
     Status mStatus = Status::Unknown;
     QString mGeneratedPassword;
+    QByteArray mDecodedPrivateKey;
     RocketChatAccount *const mAccount;
 };
diff --git a/src/core/rocketchataccount.cpp b/src/core/rocketchataccount.cpp
index 2894c389c7..d035639bd9 100644
--- a/src/core/rocketchataccount.cpp
+++ b/src/core/rocketchataccount.cpp
@@ -302,6 +302,9 @@ RocketChatAccount::RocketChatAccount(const QString &accountFileName, QObject *pa
     setDefaultAuthentication(mSettings->authMethodType());
     mNotificationPreferences->setCustomSoundManager(mCustomSoundManager);
     connect(mE2eKeyManager, &E2eKeyManager::verifyKeyDone, this, &RocketChatAccount::slotVerifyKeysDone);
+    connect(mE2eKeyManager, &E2eKeyManager::decodeEncryptionKeyDone, this, &RocketChatAccount::slotE2eDecodeKeyDone);
+    connect(mE2eKeyManager, &E2eKeyManager::failedDecodeEncryptionKey, this, &RocketChatAccount::slotE2eDecodeKeyFailed);
+    connect(mE2eKeyManager, &E2eKeyManager::decodeEncryptionKeyPostponed, this, &RocketChatAccount::slotE2eDecodeKeyPostponed);
     connect(mMemoryManager, &MemoryManager::clearApplicationSettingsModelRequested, mAppsMarketPlaceModel, &AppsMarketPlaceModel::clear);
     connect(mMemoryManager, &MemoryManager::cleanRoomHistoryRequested, mRoomModel, &RoomModel::cleanRoomHistory);
 
@@ -3339,6 +3342,22 @@ void RocketChatAccount::slotVerifyKeysDone()
 #endif
 }
 
+void RocketChatAccount::slotE2eDecodeKeyDone()
+{
+    setE2EPasswordMustBeDecrypt(false);
+}
+
+void RocketChatAccount::slotE2eDecodeKeyFailed()
+{
+    setE2EPasswordMustBeDecrypt(true);
+    Q_EMIT needToDecryptE2EPassword();
+}
+
+void RocketChatAccount::slotE2eDecodeKeyPostponed()
+{
+    setE2EPasswordMustBeDecrypt(true);
+}
+
 MemoryManager *RocketChatAccount::memoryManager() const
 {
     return mMemoryManager;
diff --git a/src/core/rocketchataccount.h b/src/core/rocketchataccount.h
index 9712ef98ae..529d25f0e8 100644
--- a/src/core/rocketchataccount.h
+++ b/src/core/rocketchataccount.h
@@ -595,6 +595,9 @@ private:
     LIBRUQOLACORE_NO_EXPORT void slotReconnectToDdpServer();
     LIBRUQOLACORE_NO_EXPORT void resetDdp();
     LIBRUQOLACORE_NO_EXPORT void slotVerifyKeysDone();
+    LIBRUQOLACORE_NO_EXPORT void slotE2eDecodeKeyDone();
+    LIBRUQOLACORE_NO_EXPORT void slotE2eDecodeKeyFailed();
+    LIBRUQOLACORE_NO_EXPORT void slotE2eDecodeKeyPostponed();
     LIBRUQOLACORE_NO_EXPORT void slotDDpLoginStatusChanged();
     LIBRUQOLACORE_NO_EXPORT void slotRESTLoginStatusChanged();
     LIBRUQOLACORE_NO_EXPORT void slotRoomOpenChanged(const QByteArray &rid);
diff --git a/src/widgets/room/roomwidget.cpp b/src/widgets/room/roomwidget.cpp
index 2e830a2de3..9406a8bf39 100644
--- a/src/widgets/room/roomwidget.cpp
+++ b/src/widgets/room/roomwidget.cpp
@@ -254,11 +254,28 @@ void RoomWidget::createE2eDecodeEncryptionKeyFailedWidget()
 
 void RoomWidget::slotDecodeEncryptionKey()
 {
+    if (!mCurrentRocketChatAccount) {
+        return;
+    }
+
     QPointer<E2ePasswordDecodeKeyDialog> dlg = new E2ePasswordDecodeKeyDialog(this);
     if (dlg->exec()) {
-        // TODO we saved it => don't ask it again
         const QString password = dlg->password();
-        // TODO generate private key
+        if (mCurrentRocketChatAccount->e2eKeyManager()->decodeEncryptionKey(password)) {
+            if (mE2eDecodeEncryptionKeyWidget) {
+                mE2eDecodeEncryptionKeyWidget->animatedHide();
+            }
+            if (mE2eDecodeEncryptionKeyFailedWidget) {
+                mE2eDecodeEncryptionKeyFailedWidget->animatedHide();
+            }
+        } else {
+            if (!mE2eDecodeEncryptionKeyFailedWidget) {
+                createE2eDecodeEncryptionKeyFailedWidget();
+            }
+            mE2eDecodeEncryptionKeyFailedWidget->animatedShow();
+        }
+    } else {
+        mCurrentRocketChatAccount->e2eKeyManager()->postponeDecryption();
     }
     delete dlg;
 }
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.