[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