[network/ruqola] src/core: Now we can decrypt message

Laurent Montel <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 94f70207baa7e1b927726ba79a8d5fb296712c3e by Laurent Montel.
Committed on 05/08/2026 at 17:31.
Pushed by mlaurent into branch 'master'.

Now we can decrypt message

M  +30   -0    src/core/encryption/e2ekeymanager.cpp
M  +3    -0    src/core/encryption/e2ekeymanager.h
M  +72   -14   src/core/encryption/encryptionutils.cpp
M  +12   -6    src/core/model/messagesmodel.cpp
M  +5    -0    src/core/model/roommodel.cpp
M  +1    -0    src/core/model/roommodel.h
M  +1    -0    src/core/rocketchataccount.cpp
M  +22   -0    src/core/room.cpp
M  +6    -0    src/core/room.h
M  +230  -40   src/core/roomencryptionkey.cpp

https://invent.kde.org/network/ruqola/-/commit/94f70207baa7e1b927726ba79a8d5fb296712c3e

diff --git a/src/core/encryption/e2ekeymanager.cpp b/src/core/encryption/e2ekeymanager.cpp
index 078468ac7c..a5c9ba6370 100644
--- a/src/core/encryption/e2ekeymanager.cpp
+++ b/src/core/encryption/e2ekeymanager.cpp
@@ -17,8 +17,10 @@
 #include "localdatabase/e2edatabase.h"
 #include "localdatabase/e2eroomsdatabase.h"
 #include "localdatabase/localdatabasemanager.h"
+#include "model/roommodel.h"
 #include "rocketchataccount.h"
 #include "rocketchataccountsettings.h"
+#include "room.h"
 #include "ruqola_encryption_debug.h"
 #include "ruqolaserverconfig.h"
 #include <qt6keychain/keychain.h>
@@ -157,6 +159,9 @@ bool E2eKeyManager::decodeEncryptionKey(const QString &password)
     RSA_free(privateKey);
     mDecodedPrivateKey = privateKeyPem;
     setStatus(Status::KeyDecrypted);
+    if (decryptRoomsSessionKeys()) {
+        Q_EMIT needRefreshView();
+    }
     Q_EMIT decodeEncryptionKeyDone();
     storePassword(password);
     return true;
@@ -568,4 +573,29 @@ E2eKeyManager::Status E2eKeyManager::needToDecodeEncryptionKey() const
     return Status::Unknown;
 }
 
+bool E2eKeyManager::decryptRoomsSessionKeys()
+{
+#if USE_E2E_SUPPORT
+    if (mDecodedPrivateKey.isEmpty()) {
+        return false;
+    }
+
+    RSA *privateKey = EncryptionUtils::privateKeyFromPEM(mDecodedPrivateKey);
+    if (!privateKey) {
+        qCWarning(RUQOLA_ENCRYPTION_LOG) << "decryptRoomsSessionKeys: failed to load private key from PEM";
+        return false;
+    }
+
+    RoomModel *model = mAccount->roomModel();
+    for (Room *room : model->rooms()) {
+        if (!room->e2EKey().isEmpty()) {
+            room->decryptSessionKeyWithPrivateKey(privateKey);
+        }
+    }
+
+    RSA_free(privateKey);
+#endif
+    return true;
+}
+
 #include "moc_e2ekeymanager.cpp"
diff --git a/src/core/encryption/e2ekeymanager.h b/src/core/encryption/e2ekeymanager.h
index b16559b6f3..cf07cdda19 100644
--- a/src/core/encryption/e2ekeymanager.h
+++ b/src/core/encryption/e2ekeymanager.h
@@ -5,6 +5,7 @@
 */
 
 #pragma once
+#include "config-ruqola.h"
 #include "libruqolacore_export.h"
 
 #include <QObject>
@@ -48,6 +49,7 @@ public:
     void setKeySaved(bool newKeySaved);
 
     void verifyExistingKeyForTest(const QJsonObject &json);
+    [[nodiscard]] bool decryptRoomsSessionKeys();
 
 Q_SIGNALS:
     void needDecodeEncryptionKey();
@@ -57,6 +59,7 @@ Q_SIGNALS:
     void uploadEncryptionKeyFailed();
     void uploadEncryptionKeyDone();
     void verifyKeyDone();
+    void needRefreshView();
 
 private:
     LIBRUQOLACORE_NO_EXPORT void readPassword();
diff --git a/src/core/encryption/encryptionutils.cpp b/src/core/encryption/encryptionutils.cpp
index c9595b3c57..6a124e3657 100644
--- a/src/core/encryption/encryptionutils.cpp
+++ b/src/core/encryption/encryptionutils.cpp
@@ -16,6 +16,7 @@
 #include <QJsonParseError>
 #include <QRandomGenerator>
 #include <QUuid>
+#include <openssl/evp.h>
 
 using namespace Qt::Literals::StringLiterals;
 
@@ -362,16 +363,32 @@ QByteArray EncryptionUtils::encryptSessionKey(const QByteArray &sessionKey, RSA
         return {};
     }
 
-    QByteArray encryptedSessionKey(RSA_size(publicKey), 0);
-    const int bytes = RSA_public_encrypt(sessionKey.size(),
-                                         reinterpret_cast<const unsigned char *>(sessionKey.constData()),
+    const int rsaSize = RSA_size(publicKey);
+    QByteArray padded(rsaSize, 0);
+    if (RSA_padding_add_PKCS1_OAEP_mgf1(reinterpret_cast<unsigned char *>(padded.data()),
+                                        rsaSize,
+                                        reinterpret_cast<const unsigned char *>(sessionKey.constData()),
+                                        sessionKey.size(),
+                                        nullptr,
+                                        0,
+                                        EVP_sha256(),
+                                        EVP_sha256())
+        != 1) {
+        qCWarning(RUQOLA_ENCRYPTION_LOG) << "Session key encryption failed: OAEP-SHA256 padding failed";
+        return {};
+    }
+
+    QByteArray encryptedSessionKey(rsaSize, 0);
+    const int bytes = RSA_public_encrypt(rsaSize,
+                                         reinterpret_cast<const unsigned char *>(padded.constData()),
                                          reinterpret_cast<unsigned char *>(encryptedSessionKey.data()),
                                          publicKey,
-                                         RSA_PKCS1_OAEP_PADDING);
-    if (bytes == -1) {
+                                         RSA_NO_PADDING);
+    if (bytes != rsaSize) {
         qCWarning(RUQOLA_ENCRYPTION_LOG) << "Session key encryption failed!";
         return {};
     }
+
     encryptedSessionKey.resize(bytes);
     return encryptedSessionKey;
 }
@@ -379,21 +396,62 @@ QByteArray EncryptionUtils::encryptSessionKey(const QByteArray &sessionKey, RSA
 QByteArray EncryptionUtils::decryptSessionKey(const QByteArray &encryptedSessionKey, RSA *privateKey)
 {
     if (encryptedSessionKey.isEmpty() || !privateKey) {
-        qCWarning(RUQOLA_ENCRYPTION_LOG) << "Session key decryption failed: invalid input";
+        qCWarning(RUQOLA_ENCRYPTION_LOG) << "Session key decryption failed: invalid input" << encryptedSessionKey << " privateKey " << privateKey;
         return {};
     }
 
-    QByteArray decryptedSessionKey(RSA_size(privateKey), 0);
-    const int bytes = RSA_private_decrypt(encryptedSessionKey.size(),
-                                          reinterpret_cast<const unsigned char *>(encryptedSessionKey.constData()),
-                                          reinterpret_cast<unsigned char *>(decryptedSessionKey.data()),
-                                          privateKey,
-                                          RSA_PKCS1_OAEP_PADDING);
-    if (bytes == -1) {
+    const int rsaSize = RSA_size(privateKey);
+    if (encryptedSessionKey.size() != rsaSize) {
+        qCWarning(RUQOLA_ENCRYPTION_LOG) << "Session key decryption failed: encrypted key size" << encryptedSessionKey.size() << "does not match RSA size"
+                                         << rsaSize;
+        return {};
+    }
+
+    QByteArray encoded(rsaSize, 0);
+    const int encodedLen = RSA_private_decrypt(encryptedSessionKey.size(),
+                                               reinterpret_cast<const unsigned char *>(encryptedSessionKey.constData()),
+                                               reinterpret_cast<unsigned char *>(encoded.data()),
+                                               privateKey,
+                                               RSA_NO_PADDING);
+    if (encodedLen != rsaSize) {
         qCWarning(RUQOLA_ENCRYPTION_LOG) << "Session key decryption failed!";
         return {};
     }
-    decryptedSessionKey.resize(bytes);
+
+    auto decryptWithHash = [&](const EVP_MD *oaepMd, const EVP_MD *mgf1Md, const char *label) -> QByteArray {
+        QByteArray out(rsaSize, 0);
+        const int decodedLen = RSA_padding_check_PKCS1_OAEP_mgf1(reinterpret_cast<unsigned char *>(out.data()),
+                                                                 out.size(),
+                                                                 reinterpret_cast<const unsigned char *>(encoded.constData()),
+                                                                 encodedLen,
+                                                                 rsaSize,
+                                                                 nullptr,
+                                                                 0,
+                                                                 oaepMd,
+                                                                 mgf1Md);
+        if (decodedLen < 0) {
+            qCDebug(RUQOLA_ENCRYPTION_LOG) << "Session key OAEP decode failed with" << label;
+            return {};
+        }
+        qCDebug(RUQOLA_ENCRYPTION_LOG) << "Session key OAEP decode succeeded with" << label << "decodedLen=" << decodedLen;
+        out.resize(decodedLen);
+        return out;
+    };
+
+    // Rocket.Chat uses RSA-OAEP with SHA-256. Some environments encode MGF1
+    // with SHA-1 while keeping OAEP hash at SHA-256, so try both first.
+    QByteArray decryptedSessionKey = decryptWithHash(EVP_sha256(), EVP_sha256(), "oaep=sha256 mgf1=sha256");
+    if (decryptedSessionKey.isEmpty()) {
+        decryptedSessionKey = decryptWithHash(EVP_sha256(), EVP_sha1(), "oaep=sha256 mgf1=sha1");
+    }
+    if (decryptedSessionKey.isEmpty()) {
+        // Backward compatibility for previously stored OAEP-SHA1 ciphertexts.
+        decryptedSessionKey = decryptWithHash(EVP_sha1(), EVP_sha1(), "oaep=sha1 mgf1=sha1");
+    }
+
+    if (decryptedSessionKey.isEmpty()) {
+        qCWarning(RUQOLA_ENCRYPTION_LOG) << "Session key decryption failed!";
+    }
     return decryptedSessionKey;
 }
 
diff --git a/src/core/model/messagesmodel.cpp b/src/core/model/messagesmodel.cpp
index 2c469137c2..94e6f0b601 100644
--- a/src/core/model/messagesmodel.cpp
+++ b/src/core/model/messagesmodel.cpp
@@ -21,6 +21,8 @@
 #include "ruqolaserverconfig.h"
 #include "textconverter.h"
 #include "utils.h"
+#include <QJsonDocument>
+#include <QJsonObject>
 
 #include <KLocalizedString>
 
@@ -435,12 +437,16 @@ QString MessagesModel::convertedText(const Message &message, const QString &sear
     if (message.messageType() == Message::System) {
         return message.systemMessageText();
     } else if (message.messageType() == Message::EncryptedText) {
-        // TODO allow to decrypt message
-#if 0
-        qDebug() << "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD" << *message.messageEncrypted();
-        qDebug() << " xxxxxxxxxx " << message.messageEncrypted()->decrypt("a2e332e5-0b27-4d8f-82e8-66c833ae7860");
-#endif
-        // message.messageEncrypted()->decrypt()
+        // TODO move in messageEncrypted
+        const QByteArray content = message.messageEncrypted()->decrypt(mRoom->sessionKey());
+        qDebug() << " cxontent " << content;
+        if (!content.isEmpty()) {
+            const QJsonDocument doc = QJsonDocument::fromJson(content);
+            const QJsonObject obj = doc.object();
+            if (obj.contains("msg"_L1)) {
+                return obj["msg"_L1].toString();
+            }
+        }
         return message.systemMessageText();
     } else {
         QStringList highlightWords;
diff --git a/src/core/model/roommodel.cpp b/src/core/model/roommodel.cpp
index 27cf99ce1a..360498e5ff 100644
--- a/src/core/model/roommodel.cpp
+++ b/src/core/model/roommodel.cpp
@@ -683,4 +683,9 @@ QString RoomModel::sectionName(Section sectionId)
     return u"ERROR"_s;
 }
 
+const QList<Room *> &RoomModel::rooms() const
+{
+    return mRoomsList;
+}
+
 #include "moc_roommodel.cpp"
diff --git a/src/core/model/roommodel.h b/src/core/model/roommodel.h
index 98bf4eafe2..03967c368e 100644
--- a/src/core/model/roommodel.h
+++ b/src/core/model/roommodel.h
@@ -124,6 +124,7 @@ public:
     [[nodiscard]] QList<Room *> findRoomNameConstains(const QString &str) const;
     void cleanRoomHistory();
     void deserializeRoom(const QJsonObject &room);
+    [[nodiscard]] const QList<Room *> &rooms() const;
 Q_SIGNALS:
     void needToUpdateNotification();
     void roomNeedAttention();
diff --git a/src/core/rocketchataccount.cpp b/src/core/rocketchataccount.cpp
index ba31c13e66..000fdbc13b 100644
--- a/src/core/rocketchataccount.cpp
+++ b/src/core/rocketchataccount.cpp
@@ -308,6 +308,7 @@ RocketChatAccount::RocketChatAccount(const QString &accountFileName, QObject *pa
     connect(mE2eKeyManager, &E2eKeyManager::decodeEncryptionKeyPostponed, this, &RocketChatAccount::slotE2eDecodeKeyPostponed);
     connect(mE2eKeyManager, &E2eKeyManager::uploadEncryptionKeyDone, this, &RocketChatAccount::slotE2eUploadKeyDone);
     connect(mE2eKeyManager, &E2eKeyManager::uploadEncryptionKeyFailed, this, &RocketChatAccount::slotE2eUploadKeyFailed);
+    connect(mE2eKeyManager, &E2eKeyManager::needRefreshView, this, &RocketChatAccount::needUpdateMessageView);
     connect(mMemoryManager, &MemoryManager::clearApplicationSettingsModelRequested, mAppsMarketPlaceModel, &AppsMarketPlaceModel::clear);
     connect(mMemoryManager, &MemoryManager::cleanRoomHistoryRequested, mRoomModel, &RoomModel::cleanRoomHistory);
 
diff --git a/src/core/room.cpp b/src/core/room.cpp
index e2164cd87a..84f0dc493a 100644
--- a/src/core/room.cpp
+++ b/src/core/room.cpp
@@ -12,6 +12,7 @@
 #include "model/usersforroommodel.h"
 #include "rocketchataccount.h"
 #include "ruqola_debug.h"
+#include "ruqola_encryption_debug.h"
 #include "ruqola_memory_management_debug.h"
 #include "ruqolaserverconfig.h"
 #include "textconverter.h"
@@ -1205,6 +1206,14 @@ void Room::setJoinCodeRequired(bool joinCodeRequired)
     }
 }
 
+QByteArray Room::sessionKey() const
+{
+    if (mRoomEncryptionKey) {
+        return mRoomEncryptionKey->sessionKey();
+    }
+    return {};
+}
+
 QString Room::e2eKeyId() const
 {
     if (mRoomEncryptionKey) {
@@ -1249,6 +1258,19 @@ void Room::setE2EKey(const QString &e2EKey)
     }
 }
 
+#if USE_E2E_SUPPORT
+void Room::decryptSessionKeyWithPrivateKey(RSA *privateKey)
+{
+    if (!mRoomEncryptionKey) {
+        return;
+    }
+    qCDebug(RUQOLA_ENCRYPTION_LOG) << "Room::decryptSessionKeyWithPrivateKey"
+                                   << "roomName=" << name() << "roomId=" << roomId() << "e2eKeyId=" << mRoomEncryptionKey->e2eKeyId()
+                                   << "e2eKeyLen=" << mRoomEncryptionKey->e2EKey().size() << "hasPrivateKey=" << (privateKey != nullptr);
+    mRoomEncryptionKey->decryptWithPrivateKey(privateKey);
+}
+#endif
+
 bool Room::encrypted() const
 {
     return roomStateValue(Room::Encrypted);
diff --git a/src/core/room.h b/src/core/room.h
index a7ba171b5d..d0dd9a1b92 100644
--- a/src/core/room.h
+++ b/src/core/room.h
@@ -14,6 +14,7 @@
 #include <QPointer>
 
 #include "channelcounterinfo.h"
+#include "config-ruqola.h"
 #include "libruqolacore_export.h"
 #include "retentioninfo.h"
 #include "roomencryptionkey.h"
@@ -201,6 +202,10 @@ public:
     [[nodiscard]] QString e2eKeyId() const;
     void setE2eKeyId(const QString &e2eKeyId);
 
+#if USE_E2E_SUPPORT
+    void decryptSessionKeyWithPrivateKey(RSA *privateKey);
+#endif
+
     [[nodiscard]] bool joinCodeRequired() const;
     void setJoinCodeRequired(bool joinCodeRequired);
 
@@ -302,6 +307,7 @@ public:
 
     [[nodiscard]] bool userIsMuted(const QString &username);
 
+    [[nodiscard]] QByteArray sessionKey() const;
 Q_SIGNALS:
     void highlightsWordChanged();
     void nameChanged();
diff --git a/src/core/roomencryptionkey.cpp b/src/core/roomencryptionkey.cpp
index 85241162c2..75a88d693f 100644
--- a/src/core/roomencryptionkey.cpp
+++ b/src/core/roomencryptionkey.cpp
@@ -5,11 +5,151 @@
 */
 
 #include "roomencryptionkey.h"
+#include "ruqola_encryption_debug.h"
 #include "ruqola_room_memory_debug.h"
+#include <QCryptographicHash>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QUuid>
+#include <QVector>
 #if USE_E2E_SUPPORT
 #include "encryption/encryptionutils.h"
 #endif
 
+namespace
+{
+QByteArray decodeBase64Variants(const QByteArray &text)
+{
+    if (text.isEmpty()) {
+        return {};
+    }
+
+    // Prefer URL-safe decoding when URL-safe alphabet is present.
+    const bool looksBase64Url = text.contains('-') || text.contains('_');
+    const QByteArray first = QByteArray::fromBase64(text, looksBase64Url ? QByteArray::Base64UrlEncoding : QByteArray::Base64Encoding);
+    const QByteArray second = QByteArray::fromBase64(text, looksBase64Url ? QByteArray::Base64Encoding : QByteArray::Base64UrlEncoding);
+
+    if (!first.isEmpty() && !second.isEmpty()) {
+        // Keep the longest candidate to avoid truncated decodes.
+        return (first.size() >= second.size()) ? first : second;
+    }
+    if (!first.isEmpty()) {
+        return first;
+    }
+    return second;
+}
+
+#if USE_E2E_SUPPORT
+QVector<QByteArray> decodeAllBase64Variants(const QString &text)
+{
+    QVector<QByteArray> out;
+    const QByteArray bytes = text.toLatin1();
+
+    const QByteArray plain = QByteArray::fromBase64(bytes, QByteArray::Base64Encoding);
+    if (!plain.isEmpty()) {
+        out.append(plain);
+    }
+
+    const QByteArray url = QByteArray::fromBase64(bytes, QByteArray::Base64UrlEncoding);
+    if (!url.isEmpty() && !out.contains(url)) {
+        out.append(url);
+    }
+    return out;
+}
+
+QVector<QByteArray> encryptedKeyCandidates(const QString &fullE2EKey, const QString &selectedPayload, const QString &knownKeyId)
+{
+    QVector<QByteArray> out;
+
+    auto appendDecoded = [&](const QString &candidateText) {
+        if (candidateText.isEmpty()) {
+            return;
+        }
+        const auto decoded = decodeAllBase64Variants(candidateText);
+        for (const QByteArray &d : decoded) {
+            if (!out.contains(d)) {
+                out.append(d);
+            }
+        }
+    };
+
+    appendDecoded(selectedPayload);
+    appendDecoded(fullE2EKey);
+
+    // Some payloads are keyId(36) + ciphertext even when keyId is not UUID-shaped
+    // or not yet known in this object. Try fixed-length splits as fallbacks.
+    if (fullE2EKey.size() > 36) {
+        appendDecoded(fullE2EKey.mid(36));
+        const QChar possibleSeparator = fullE2EKey.at(36);
+        if (possibleSeparator == QLatin1Char(':') || possibleSeparator == QLatin1Char('|') || possibleSeparator == QLatin1Char('.')) {
+            appendDecoded(fullE2EKey.mid(37));
+        }
+    }
+
+    if (!knownKeyId.isEmpty() && fullE2EKey.startsWith(knownKeyId)) {
+        const QString suffix = fullE2EKey.mid(knownKeyId.size());
+        appendDecoded(suffix);
+        if (!suffix.isEmpty() && (suffix.at(0) == QLatin1Char(':') || suffix.at(0) == QLatin1Char('|') || suffix.at(0) == QLatin1Char('.'))) {
+            appendDecoded(suffix.mid(1));
+        }
+    }
+
+    if (fullE2EKey.size() > 36) {
+        const QString possibleKeyId = fullE2EKey.left(36);
+        if (!QUuid(possibleKeyId).isNull()) {
+            appendDecoded(fullE2EKey.mid(36));
+        }
+    }
+
+    return out;
+}
+
+QByteArray normalizeSessionKeyPayload(const QByteArray &decryptedPayload)
+{
+    if (decryptedPayload.size() == 32) {
+        return decryptedPayload;
+    }
+
+    const QByteArray trimmed = decryptedPayload.trimmed();
+    if (trimmed.isEmpty()) {
+        return {};
+    }
+
+    // Some payloads are base64/base64url text of the raw 32-byte key.
+    if (const QByteArray decoded = decodeBase64Variants(trimmed); decoded.size() == 32) {
+        return decoded;
+    }
+
+    // Some Rocket.Chat payloads are JSON (JWK-like), containing the key in "k".
+    const QJsonDocument doc = QJsonDocument::fromJson(trimmed);
+    if (!doc.isNull()) {
+        if (doc.isObject()) {
+            const QJsonObject obj = doc.object();
+            if (const QString k = obj.value(QStringLiteral("k")).toString(); !k.isEmpty()) {
+                const QByteArray decodedK = QByteArray::fromBase64(k.toLatin1(), QByteArray::Base64UrlEncoding);
+                if (decodedK.size() == 32) {
+                    return decodedK;
+                }
+            }
+            if (const QString key = obj.value(QStringLiteral("key")).toString(); !key.isEmpty()) {
+                if (const QByteArray decodedKey = decodeBase64Variants(key.toLatin1()); decodedKey.size() == 32) {
+                    return decodedKey;
+                }
+            }
+            if (const QString binary = obj.value(QStringLiteral("$binary")).toString(); !binary.isEmpty()) {
+                const QByteArray decodedBinary = QByteArray::fromBase64(binary.toLatin1());
+                if (decodedBinary.size() == 32) {
+                    return decodedBinary;
+                }
+            }
+        }
+    }
+
+    return {};
+}
+#endif
+}
+
 RoomEncryptionKey::RoomEncryptionKey()
 {
     qCDebug(RUQOLA_ROOM_MEMORY_LOG) << " RoomEncryptionKey created " << this;
@@ -40,40 +180,64 @@ void RoomEncryptionKey::parseSessionKey()
         mE2eKeyId.clear();
         return;
     }
+    // Rocket.Chat payloads may be either:
+    // 1) keyId(36 UUID) + encryptedKey(base64/base64url)
+    // 2) encryptedKey(base64/base64url) only, with keyId in a separate field.
+    const QByteArray fullCandidate = decodeBase64Variants(mE2EKey.toLatin1());
+    QByteArray tailCandidate;
+    QString prefixedKeyId;
+    QString prefixedCipherText;
 
-    // Format E2EKey: keyId(36) + encryptedKey(base64)
-    if (mE2EKey.size() < 36) {
-        qCWarning(RUQOLA_ROOM_MEMORY_LOG) << "E2EKey too short:" << mE2EKey.size();
-        mSessionKey.clear();
-        return;
+    if (!mE2eKeyId.isEmpty() && mE2EKey.startsWith(mE2eKeyId)) {
+        prefixedKeyId = mE2eKeyId;
+        prefixedCipherText = mE2EKey.mid(mE2eKeyId.size());
+        if (!prefixedCipherText.isEmpty()
+            && (prefixedCipherText.at(0) == QLatin1Char(':') || prefixedCipherText.at(0) == QLatin1Char('|') || prefixedCipherText.at(0) == QLatin1Char('.'))) {
+            prefixedCipherText = prefixedCipherText.mid(1);
+        }
+        tailCandidate = decodeBase64Variants(prefixedCipherText.toLatin1());
+    } else if (mE2EKey.size() > 36) {
+        const QString possibleKeyId = mE2EKey.left(36);
+        if (!QUuid(possibleKeyId).isNull()) {
+            prefixedKeyId = possibleKeyId;
+            prefixedCipherText = mE2EKey.mid(36);
+            tailCandidate = decodeBase64Variants(prefixedCipherText.toLatin1());
+        }
     }
 
-    mE2eKeyId = mE2EKey.left(36); // ← UUID
-
-    // Extraire encryptedKey (base64)
-    mEncryptedKeyBase64 = mE2EKey.mid(36);
-
-    if (mEncryptedKeyBase64.isEmpty()) {
-        qCWarning(RUQOLA_ROOM_MEMORY_LOG) << "E2EKey encryptedKey part is empty";
+    bool useFullPayload = false;
+    if (!tailCandidate.isEmpty()) {
+        // When a prefixed form is detected, it is usually the canonical payload.
+        useFullPayload = false;
+    } else if (!fullCandidate.isEmpty()) {
+        useFullPayload = true;
+    } else {
+        qCWarning(RUQOLA_ENCRYPTION_LOG) << "Failed to decode E2EKey from base64/base64url";
         mSessionKey.clear();
         return;
     }
-
-    // Validate base64 can be decoded
-    const QByteArray encryptedKey = QByteArray::fromBase64(mEncryptedKeyBase64.toLatin1());
-    if (encryptedKey.isEmpty()) {
-        qCWarning(RUQOLA_ROOM_MEMORY_LOG) << "Failed to decode E2EKey from base64";
-        mSessionKey.clear();
-        return;
+    if (useFullPayload) {
+        mEncryptedKeyBase64 = mE2EKey;
+    } else {
+        if (mE2eKeyId.isEmpty()) {
+            mE2eKeyId = prefixedKeyId;
+        }
+        mEncryptedKeyBase64 = prefixedCipherText;
     }
 
-    if (encryptedKey.size() != 256) {
-        qCWarning(RUQOLA_ROOM_MEMORY_LOG) << "Invalid encryptedKey size:" << encryptedKey.size() << "(expected 256 for RSA-2048)";
+    // Validate encoded payload can be decoded as base64/base64url.
+    const QByteArray encryptedKey = decodeBase64Variants(mEncryptedKeyBase64.toLatin1());
+    if (encryptedKey.isEmpty()) {
+        qCWarning(RUQOLA_ENCRYPTION_LOG) << "Failed to decode E2EKey from base64/base64url";
         mSessionKey.clear();
         return;
     }
+    const QByteArray keyFingerprint = QCryptographicHash::hash(encryptedKey, QCryptographicHash::Sha256).toHex().left(16);
+    qCDebug(RUQOLA_ENCRYPTION_LOG) << "E2EKey parsed candidate" << "keyId=" << mE2eKeyId << "base64Len=" << mEncryptedKeyBase64.size()
+                                   << "decodedLen=" << encryptedKey.size() << "format=" << (useFullPayload ? "full" : "prefixed")
+                                   << "sha256[:16]=" << keyFingerprint;
 
-    qDebug() << "E2EKey parsed - keyId:" << mE2eKeyId << "encryptedKey size:" << encryptedKey.size();
+    qCDebug(RUQOLA_ENCRYPTION_LOG) << "E2EKey parsed - keyId:" << mE2eKeyId << "encryptedKey size:" << encryptedKey.size();
     // Waiting for RSA private key to decrypt session key
 }
 
@@ -89,42 +253,68 @@ void RoomEncryptionKey::setE2eKeyId(const QString &newE2eKeyId)
 #if USE_E2E_SUPPORT
 void RoomEncryptionKey::decryptWithPrivateKey(RSA *privateKey)
 {
+    qCDebug(RUQOLA_ENCRYPTION_LOG) << "RoomEncryptionKey::decryptWithPrivateKey start" << "keyId=" << mE2eKeyId;
     if (!privateKey) {
-        qCWarning(RUQOLA_ROOM_MEMORY_LOG) << "Private key is null, cannot decrypt session key";
+        qCWarning(RUQOLA_ENCRYPTION_LOG) << "Private key is null, cannot decrypt session key";
         mSessionKey.clear();
         return;
     }
 
     if (mEncryptedKeyBase64.isEmpty()) {
-        qCWarning(RUQOLA_ROOM_MEMORY_LOG) << "No encrypted key available for decryption";
+        qCWarning(RUQOLA_ENCRYPTION_LOG) << "No encrypted key available for decryption";
         mSessionKey.clear();
         return;
     }
 
-    // Decode base64 to binary
-    const QByteArray encryptedKey = QByteArray::fromBase64(mEncryptedKeyBase64.toLatin1());
-
-    if (encryptedKey.size() != 256) {
-        qCWarning(RUQOLA_ROOM_MEMORY_LOG) << "Invalid encryptedKey size for decryption:" << encryptedKey.size();
+    const int rsaSize = RSA_size(privateKey);
+    const QVector<QByteArray> keyCandidates = encryptedKeyCandidates(mE2EKey, mEncryptedKeyBase64, mE2eKeyId);
+    if (keyCandidates.isEmpty()) {
+        qCWarning(RUQOLA_ENCRYPTION_LOG) << "Failed to decode encrypted key from base64/base64url";
         mSessionKey.clear();
         return;
     }
 
-    // Decrypt using RSA private key
-    mSessionKey = EncryptionUtils::decryptSessionKey(encryptedKey, privateKey);
+    QByteArray lastDecryptedPayload;
+    QVector<int> candidateSizes;
+    candidateSizes.reserve(keyCandidates.size());
 
-    if (mSessionKey.isEmpty()) {
-        qCWarning(RUQOLA_ROOM_MEMORY_LOG) << "Failed to decrypt session key with private key";
-        return;
-    }
+    for (int i = 0; i < keyCandidates.size(); ++i) {
+        const QByteArray &encryptedKey = keyCandidates.at(i);
+        candidateSizes.append(encryptedKey.size());
 
-    if (mSessionKey.size() != 32) {
-        qCWarning(RUQOLA_ROOM_MEMORY_LOG) << "Invalid decrypted session key size:" << mSessionKey.size() << "(expected 32)";
-        mSessionKey.clear();
-        return;
+        const QByteArray keyFingerprint = QCryptographicHash::hash(encryptedKey, QCryptographicHash::Sha256).toHex().left(16);
+        qCDebug(RUQOLA_ENCRYPTION_LOG) << "Decrypting room session key"
+                                       << "keyId=" << mE2eKeyId << "candidate=" << i << "candidateCount=" << keyCandidates.size()
+                                       << "base64Len=" << mEncryptedKeyBase64.size() << "decodedLen=" << encryptedKey.size() << "rsaSize=" << rsaSize
+                                       << "sha256[:16]=" << keyFingerprint;
+
+        if (encryptedKey.size() != rsaSize) {
+            continue;
+        }
+
+        // Decrypt using RSA private key.
+        const QByteArray decryptedPayload = EncryptionUtils::decryptSessionKey(encryptedKey, privateKey);
+        if (decryptedPayload.isEmpty()) {
+            continue;
+        }
+
+        lastDecryptedPayload = decryptedPayload;
+        mSessionKey = normalizeSessionKeyPayload(decryptedPayload);
+        if (mSessionKey.size() == 32) {
+            qCDebug(RUQOLA_ENCRYPTION_LOG) << "Session key successfully decrypted for keyId:" << mE2eKeyId;
+            return;
+        }
     }
 
-    qDebug() << "Session key successfully decrypted for keyId:" << mE2eKeyId;
+    if (lastDecryptedPayload.isEmpty()) {
+        qCWarning(RUQOLA_ENCRYPTION_LOG) << "Invalid encryptedKey size candidates:" << candidateSizes << "(expected" << rsaSize
+                                         << "for current RSA private key)";
+    } else {
+        qCWarning(RUQOLA_ENCRYPTION_LOG) << "Invalid decrypted session key size:" << mSessionKey.size()
+                                         << "(expected 32), payloadLen=" << lastDecryptedPayload.size()
+                                         << "payloadPreview=" << QString::fromLatin1(lastDecryptedPayload.left(48));
+    }
+    mSessionKey.clear();
 }
 #endif
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.