[network/ruqola] src: Implement upload key support and verify if it works

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

Implement upload key support and verify if it works

M  +32   -0    src/core/autotests/e2ekeymanagertest.cpp
M  +1    -0    src/core/autotests/e2ekeymanagertest.h
M  +45   -8    src/core/encryption/e2ekeymanager.cpp
M  +6    -0    src/core/encryption/e2ekeymanager.h
M  +4    -0    src/widgets/room/roomwidget.cpp

https://invent.kde.org/network/ruqola/-/commit/8c85166b65654c6115405e5d073298f822e5b890

diff --git a/src/core/autotests/e2ekeymanagertest.cpp b/src/core/autotests/e2ekeymanagertest.cpp
index de687623c0..e4649899b7 100644
--- a/src/core/autotests/e2ekeymanagertest.cpp
+++ b/src/core/autotests/e2ekeymanagertest.cpp
@@ -149,6 +149,38 @@ void E2eKeyManagerTest::shouldHandleMissingOrMalformedServerKeys()
     QVERIFY(account.localDatabaseManager()->e2EDatabase()->deleteKey(u"test-e2e-user-generation"_s));
 }
 
+void E2eKeyManagerTest::shouldKeepGenerationStateAndAllowRetryWhenUploadFails()
+{
+#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()->setAccountName(u"e2e-test-account"_s);
+    account.settings()->setServerUrl(u"http://localhost:3000"_s);
+    account.settings()->setUserName(u"e2e-test-user"_s);
+    account.settings()->setUserId("test-e2e-user-upload-retry"_ba);
+    account.settings()->setAuthToken(u"token"_s);
+
+    E2eKeyManager manager(&account);
+    QSignalSpy uploadFailedSpy(&manager, &E2eKeyManager::uploadEncryptionKeyFailed);
+
+    // Empty server payload triggers local key generation and upload attempt.
+    manager.verifyExistingKeyForTest(QJsonObject{});
+    QCOMPARE(manager.status(), E2eKeyManager::Status::NeedToGenerateKey);
+    QCOMPARE(uploadFailedSpy.count(), 1);
+
+    // Retry should attempt another upload with the same pending generated key data.
+    QVERIFY(!manager.retryUploadGeneratedKey());
+    QCOMPARE(manager.status(), E2eKeyManager::Status::NeedToGenerateKey);
+    QCOMPARE(uploadFailedSpy.count(), 2);
+
+    QVERIFY(account.localDatabaseManager()->e2EDatabase()->deleteKey(u"test-e2e-user-upload-retry"_s));
+#endif
+}
+
 void E2eKeyManagerTest::shouldPostponeDecryption()
 {
     E2eKeyManager manager(nullptr);
diff --git a/src/core/autotests/e2ekeymanagertest.h b/src/core/autotests/e2ekeymanagertest.h
index a286b741cb..65c2d69acd 100644
--- a/src/core/autotests/e2ekeymanagertest.h
+++ b/src/core/autotests/e2ekeymanagertest.h
@@ -21,6 +21,7 @@ private Q_SLOTS:
     void shouldSetNeedToDecryptStatusFromBase64StringPayload();
     void shouldSetNeedToDecryptStatusFromBinaryObjectPayload();
     void shouldHandleMissingOrMalformedServerKeys();
+    void shouldKeepGenerationStateAndAllowRetryWhenUploadFails();
     void shouldPostponeDecryption();
     void shouldDecodeEncryptionKeyWithValidPassword();
     void shouldFailDecodeEncryptionKeyWithWrongPassword();
diff --git a/src/core/encryption/e2ekeymanager.cpp b/src/core/encryption/e2ekeymanager.cpp
index 161f6da43e..33059b2615 100644
--- a/src/core/encryption/e2ekeymanager.cpp
+++ b/src/core/encryption/e2ekeymanager.cpp
@@ -103,6 +103,20 @@ void E2eKeyManager::postponeDecryption()
     Q_EMIT decodeEncryptionKeyPostponed();
 }
 
+bool E2eKeyManager::retryUploadGeneratedKey()
+{
+#if USE_E2E_SUPPORT
+    if (!mAccount || mPendingUploadPublicKey.isEmpty() || mPendingUploadPrivateKey.isEmpty()) {
+        return false;
+    }
+
+    setStatus(Status::NeedToGenerateKey);
+    return startUploadGeneratedKey(mPendingUploadPublicKey, mPendingUploadPrivateKey);
+#else
+    return false;
+#endif
+}
+
 QString E2eKeyManager::generateRandomPassword() const
 {
 #if USE_E2E_SUPPORT
@@ -206,24 +220,47 @@ void E2eKeyManager::verifyExistingKey(const QJsonObject &json)
 
     (void)mAccount->localDatabaseManager()->e2EDatabase()->saveKey(userId, encryptedGeneratedPrivateKey, rsaKeyPair.publicKey);
 
+    // Local key material is ready at this point, so keep generation state even if upload cannot start.
+    setStatus(Status::NeedToGenerateKey);
+    startUploadGeneratedKey(rsaKeyPair.publicKey, encryptedGeneratedPrivateKey);
+#else
+    setStatus(Status::Unknown);
+#endif
+}
+
+bool E2eKeyManager::startUploadGeneratedKey(const QByteArray &publicKey, const QByteArray &encryptedPrivateKey)
+{
+    if (!mAccount || publicKey.isEmpty() || encryptedPrivateKey.isEmpty()) {
+        return false;
+    }
+
+    mPendingUploadPublicKey = publicKey;
+    mPendingUploadPrivateKey = encryptedPrivateKey;
+
     auto setJob = new RocketChatRestApi::SetUserPublicAndPrivateKeysJob(this);
     mAccount->restApi()->initializeRestApiJob(setJob);
 
     RocketChatRestApi::SetUserPublicAndPrivateKeysJob::SetUserPublicAndPrivateKeysInfo info;
-    info.rsaPublicKey = QString::fromUtf8(rsaKeyPair.publicKey);
-    info.rsaPrivateKey = QString::fromLatin1(encryptedGeneratedPrivateKey.toBase64());
+    info.rsaPublicKey = QString::fromUtf8(publicKey);
+    info.rsaPrivateKey = QString::fromLatin1(encryptedPrivateKey.toBase64());
     setJob->setSetUserPublicAndPrivateKeysInfo(info);
 
-    // Local key material is ready at this point, so keep generation state even if upload cannot start.
-    setStatus(Status::NeedToGenerateKey);
+    connect(setJob, &RocketChatRestApi::SetUserPublicAndPrivateKeysJob::setUserPublicAndPrivateKeysDone, this, [this]() {
+        Q_EMIT uploadEncryptionKeyDone();
+    });
+    connect(setJob, &RocketChatRestApi::RestApiAbstractJob::failed, this, [this](const QString &, const QString &) {
+        setStatus(Status::NeedToGenerateKey);
+        Q_EMIT uploadEncryptionKeyFailed();
+    });
 
     if (!setJob->start()) {
         qCWarning(RUQOLA_ENCRYPTION_LOG) << "Unable to upload generated E2E keypair";
-        return;
+        setStatus(Status::NeedToGenerateKey);
+        Q_EMIT uploadEncryptionKeyFailed();
+        return false;
     }
-#else
-    setStatus(Status::Unknown);
-#endif
+
+    return true;
 }
 
 void E2eKeyManager::verifyExistingKeyForTest(const QJsonObject &json)
diff --git a/src/core/encryption/e2ekeymanager.h b/src/core/encryption/e2ekeymanager.h
index 2308ac1937..674c6e16bd 100644
--- a/src/core/encryption/e2ekeymanager.h
+++ b/src/core/encryption/e2ekeymanager.h
@@ -27,6 +27,7 @@ public:
     void decodeEncryptionKey();
     [[nodiscard]] bool decodeEncryptionKey(const QString &password);
     void postponeDecryption();
+    [[nodiscard]] bool retryUploadGeneratedKey();
 
     void fetchMyKeys();
 
@@ -47,12 +48,17 @@ Q_SIGNALS:
     void failedDecodeEncryptionKey();
     void decodeEncryptionKeyDone();
     void decodeEncryptionKeyPostponed();
+    void uploadEncryptionKeyFailed();
+    void uploadEncryptionKeyDone();
     void verifyKeyDone();
 
 private:
     LIBRUQOLACORE_NO_EXPORT void verifyExistingKey(const QJsonObject &json);
+    LIBRUQOLACORE_NO_EXPORT bool startUploadGeneratedKey(const QByteArray &publicKey, const QByteArray &encryptedPrivateKey);
     Status mStatus = Status::Unknown;
     QString mGeneratedPassword;
     QByteArray mDecodedPrivateKey;
+    QByteArray mPendingUploadPublicKey;
+    QByteArray mPendingUploadPrivateKey;
     RocketChatAccount *const mAccount;
 };
diff --git a/src/widgets/room/roomwidget.cpp b/src/widgets/room/roomwidget.cpp
index 9406a8bf39..2e1f3421cc 100644
--- a/src/widgets/room/roomwidget.cpp
+++ b/src/widgets/room/roomwidget.cpp
@@ -294,6 +294,10 @@ void RoomWidget::slotGenerateNewPassword()
     QPointer<E2eCopyPasswordDialog> dlg = new E2eCopyPasswordDialog(mCurrentRocketChatAccount, this);
     if (dlg->exec()) {
         mCurrentRocketChatAccount->settings()->setKeySaved(true);
+        // Retry upload in case the initial key publication failed (e.g. temporary network/server issue).
+        if (!mCurrentRocketChatAccount->e2eKeyManager()->retryUploadGeneratedKey()) {
+            qCWarning(RUQOLAWIDGETS_LOG) << "Unable to retry uploading generated E2E key";
+        }
         // TODO save it in kwalletmanagers ?
     }
     // Hide it.
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.