[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.