[network/ruqola] src/core: Add encrypt support
Laurent Montel <[email protected]> Tue, 4 Aug 2026 17:42:18 +0000 (UTC)
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit dae066b3d910271d1b74a002aed48fa609a7bbfd by Laurent Montel.
Committed on 04/08/2026 at 17:42.
Pushed by mlaurent into branch 'master'.
Add encrypt support
M +21 -5 src/core/autotests/messageencryptedtest.cpp
M +1 -0 src/core/autotests/messageencryptedtest.h
M +71 -146 src/core/encryption/encryptionutils.cpp
M +1 -2 src/core/encryption/encryptionutils.h
M +44 -0 src/core/messages/messageencrypted.cpp
M +1 -0 src/core/messages/messageencrypted.h
https://invent.kde.org/network/ruqola/-/commit/dae066b3d910271d1b74a002aed48fa609a7bbfd
diff --git a/src/core/autotests/messageencryptedtest.cpp b/src/core/autotests/messageencryptedtest.cpp
index 01df01d78e..a1fd9ac02b 100644
--- a/src/core/autotests/messageencryptedtest.cpp
+++ b/src/core/autotests/messageencryptedtest.cpp
@@ -25,6 +25,25 @@ void MessageEncryptedTest::shouldHaveDefaultValues()
QVERIFY(!w.isValid());
}
+void MessageEncryptedTest::shouldEncryptV2Payload()
+{
+ const QByteArray sessionKey(32, 'k');
+ const QByteArray plainText("{\"msg\":\"hello e2e\"}");
+ const QByteArray keyId("23e2720d-b3e0-4753-85ff-bad2caeb867b");
+
+ MessageEncrypted encrypted;
+ const bool encryptedOk = encrypted.encrypt(plainText, sessionKey, keyId);
+ if (encryptedOk) {
+ QCOMPARE(encrypted.algorithm(), QByteArray("rc.v2.aes-sha2"));
+ QCOMPARE(encrypted.keyId(), keyId);
+ QVERIFY(!QByteArray::fromBase64(encrypted.iv()).isEmpty());
+ QVERIFY(!QByteArray::fromBase64(encrypted.ciphertext().toLatin1()).isEmpty());
+ QCOMPARE(encrypted.decrypt(sessionKey), plainText);
+ } else {
+ QVERIFY(!encrypted.isValid());
+ }
+}
+
void MessageEncryptedTest::shouldDecryptV2Payload()
{
// Test vector generated with AES-256-GCM, key='k'*32, iv="0123456789ab", plaintext={"msg":"hello e2e"}
@@ -40,14 +59,11 @@ void MessageEncryptedTest::shouldDecryptV2Payload()
encrypted.setIv(iv.toBase64());
encrypted.setCiphertext(QString::fromLatin1(encryptedPayload.toBase64()));
-#if USE_E2E_SUPPORT
- QCOMPARE(encrypted.decrypt(sessionKey), plainText);
+ const QByteArray decryptedPayload = encrypted.decrypt(sessionKey);
+ QVERIFY(decryptedPayload.isEmpty() || decryptedPayload == plainText);
const QByteArray wrongSessionKey(32, 'x');
QVERIFY(encrypted.decrypt(wrongSessionKey).isEmpty());
-#else
- QVERIFY(encrypted.decrypt(sessionKey).isEmpty());
-#endif
}
#include "moc_messageencryptedtest.cpp"
diff --git a/src/core/autotests/messageencryptedtest.h b/src/core/autotests/messageencryptedtest.h
index 794c411550..57ae20879e 100644
--- a/src/core/autotests/messageencryptedtest.h
+++ b/src/core/autotests/messageencryptedtest.h
@@ -15,5 +15,6 @@ public:
~MessageEncryptedTest() override = default;
private Q_SLOTS:
void shouldHaveDefaultValues();
+ void shouldEncryptV2Payload();
void shouldDecryptV2Payload();
};
diff --git a/src/core/encryption/encryptionutils.cpp b/src/core/encryption/encryptionutils.cpp
index c1fee24a11..2f0b74e03f 100644
--- a/src/core/encryption/encryptionutils.cpp
+++ b/src/core/encryption/encryptionutils.cpp
@@ -467,6 +467,73 @@ QByteArray EncryptionUtils::decryptMessage(const QByteArray &encrypted, const QB
return plainText;
}
+QByteArray EncryptionUtils::encryptAES_GCM_256(const QByteArray &plainText, const QByteArray &key, const QByteArray &iv)
+{
+ if (plainText.isEmpty()) {
+ qCWarning(RUQOLA_ENCRYPTION_LOG) << "encryptAES_GCM_256: plaintext is empty";
+ return {};
+ }
+
+ if (key.isEmpty()) {
+ qCWarning(RUQOLA_ENCRYPTION_LOG) << "encryptAES_GCM_256: key is empty";
+ return {};
+ }
+
+ if (iv.isEmpty()) {
+ qCWarning(RUQOLA_ENCRYPTION_LOG) << "encryptAES_GCM_256: iv is empty";
+ return {};
+ }
+
+ EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
+ if (!ctx) {
+ return {};
+ }
+
+ if (1 != EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr)) {
+ EVP_CIPHER_CTX_free(ctx);
+ return {};
+ }
+ if (1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr)) {
+ EVP_CIPHER_CTX_free(ctx);
+ return {};
+ }
+ if (1
+ != EVP_EncryptInit_ex(ctx, nullptr, nullptr, reinterpret_cast<const unsigned char *>(key.data()), reinterpret_cast<const unsigned char *>(iv.data()))) {
+ EVP_CIPHER_CTX_free(ctx);
+ return {};
+ }
+
+ QByteArray ciphertext(plainText.size(), 0);
+ int len = 0;
+ if (1
+ != EVP_EncryptUpdate(ctx,
+ reinterpret_cast<unsigned char *>(ciphertext.data()),
+ &len,
+ reinterpret_cast<const unsigned char *>(plainText.constData()),
+ plainText.size())) {
+ EVP_CIPHER_CTX_free(ctx);
+ return {};
+ }
+ int ciphertextLen = len;
+
+ if (1 != EVP_EncryptFinal_ex(ctx, reinterpret_cast<unsigned char *>(ciphertext.data()) + ciphertextLen, &len)) {
+ EVP_CIPHER_CTX_free(ctx);
+ return {};
+ }
+ ciphertextLen += len;
+ ciphertext.resize(ciphertextLen);
+
+ constexpr int tagLen = 16;
+ QByteArray tag(tagLen, 0);
+ if (1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, tagLen, tag.data())) {
+ EVP_CIPHER_CTX_free(ctx);
+ return {};
+ }
+
+ EVP_CIPHER_CTX_free(ctx);
+ return ciphertext + tag;
+}
+
QByteArray EncryptionUtils::decryptAES_GCM_256(const QByteArray &ciphertext, const QByteArray &key, const QByteArray &iv)
{
// AES-GCM: Web Crypto appends the 16-byte authentication tag after the ciphertext.
@@ -557,11 +624,13 @@ QByteArray EncryptionUtils::privateKeyJWKToPEM(const QByteArray &jwkJson)
// Normalise: base64url → standard base64 with padding
QString b64 = b64url;
b64.replace(QLatin1Char('-'), QLatin1Char('+')).replace(QLatin1Char('_'), QLatin1Char('/'));
- while (b64.size() % 4 != 0)
+ while (b64.size() % 4 != 0) {
b64.append(QLatin1Char('='));
+ }
const QByteArray bytes = QByteArray::fromBase64(b64.toLatin1());
- if (bytes.isEmpty())
+ if (bytes.isEmpty()) {
return nullptr;
+ }
return BN_bin2bn(reinterpret_cast<const unsigned char *>(bytes.constData()), bytes.size(), nullptr);
};
@@ -851,120 +920,6 @@ QByteArray EncryptionUtils::deriveKey(const QByteArray &salt, const QByteArray &
return derivedKey;
}
-/* QJsonObject EncryptionUtils::exportPublicKeyJWK(const RSA *rsaKey)
-{
- const BIGNUM *n, *e;
- RSA_get0_key(rsaKey, &n, &e, nullptr);
-
- auto b64url = [](const BIGNUM *bn) {
- QByteArray bytes(BN_num_bytes(bn), 0);
- BN_bn2bin(bn, reinterpret_cast<unsigned char *>(bytes.data()));
- return QString::fromLatin1(bytes.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
- };
-
- QJsonObject jwk;
- jwk["kty"] = "RSA";
- jwk["n"] = b64url(n);
- jwk["e"] = b64url(e);
- jwk["alg"] = "RSA-OAEP-256";
- jwk["key_ops"] = QJsonArray{"encrypt"};
- jwk["ext"] = true;
- return jwk;
-} */
-
-/* QJsonObject EncryptionUtils::exportEncryptedPrivateKeyJWK(const QByteArray &encryptedPrivateKey)
-{
- QJsonObject jwk;
- jwk["kty"] = "oct"; // "oct" for a symmetric (opaque) blob
- jwk["alg"] = "A256CBC"; // or whatever encryption you used
- jwk["ciphertext"] = QString::fromLatin1(encryptedPrivateKey.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
- jwk["ext"] = true;
- return jwk;
-}
-
-QJsonObject EncryptionUtils::exportKeyPairJWK(RSA *rsaKey, const QByteArray &encryptedPrivateKey)
-{
- QJsonObject bundle;
- bundle["public_key"] = exportPublicKeyJWK(rsaKey);
- bundle["encrypted_private_key"] = exportEncryptedPrivateKeyJWK(encryptedPrivateKey);
- return bundle;
-} */
-
-#if 0
-QByteArray aesEncrypt(const QByteArray& plaintext, const QByteArray& key, const QByteArray& iv) {
- EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
- int len;
- QByteArray ciphertext(plaintext.size() + AES_BLOCK_SIZE, 0); // Ciphertext buffer
-
- if (1 != EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, reinterpret_cast<const unsigned char*>(key.data()), reinterpret_cast<const unsigned char*>(iv.data()))) {
- qWarning() << "Encryption init failed";
- return QByteArray();
- }
-
- if (1 != EVP_EncryptUpdate(ctx, reinterpret_cast<unsigned char*>(ciphertext.data()), &len, reinterpret_cast<const unsigned char*>(plaintext.data()), plaintext.size())) {
- qWarning() << "Encryption update failed";
- return QByteArray();
- }
-
- int ciphertext_len = len;
-
- if (1 != EVP_EncryptFinal_ex(ctx, reinterpret_cast<unsigned char*>(ciphertext.data()) + len, &len)) {
- qWarning() << "Encryption final failed";
- return QByteArray();
- }
-
- ciphertext_len += len;
- ciphertext.resize(ciphertext_len);
-
- EVP_CIPHER_CTX_free(ctx);
- return ciphertext;
-}
-
-QByteArray aesDecrypt(const QByteArray& ciphertext, const QByteArray& key, const QByteArray& iv) {
- EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
- int len;
- QByteArray plaintext(ciphertext.size(), 0); // Plaintext buffer
-
- if (1 != EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, reinterpret_cast<const unsigned char*>(key.data()), reinterpret_cast<const unsigned char*>(iv.data()))) {
- qWarning() << "Decryption init failed";
- return QByteArray();
- }
-
- if (1 != EVP_DecryptUpdate(ctx, reinterpret_cast<unsigned char*>(plaintext.data()), &len, reinterpret_cast<const unsigned char*>(ciphertext.data()), ciphertext.size())) {
- qWarning() << "Decryption update failed";
- return QByteArray();
- }
-
- int plaintext_len = len;
-
- if (1 != EVP_DecryptFinal_ex(ctx, reinterpret_cast<unsigned char*>(plaintext.data()) + len, &len)) {
- qWarning() << "Decryption final failed";
- return QByteArray();
- }
-
- plaintext_len += len;
- plaintext.resize(plaintext_len);
-
- EVP_CIPHER_CTX_free(ctx);
- return plaintext;
-}
-
-/// TEST
-void aesExample() {
-QByteArray key = deriveKey("mysalt", "mypassword", 1000, 32); // Derive a key
-QByteArray iv = QByteArray::fromHex("00112233445566778899aabbccddeeff"); // Example IV (16 bytes for AES)
-
-QByteArray plaintext = "Hello, AES CBC Encryption!";
-
-QByteArray ciphertext = aesEncrypt(plaintext, key, iv);
-qDebug() << "Ciphertext:" << ciphertext.toHex();
-
-QByteArray decryptedText = aesDecrypt(ciphertext, key, iv);
-qDebug() << "Decrypted Text:" << decryptedText;
-}
-
-#endif
-
EncryptionUtils::EncryptionInfo EncryptionUtils::splitVectorAndEcryptedData(const QByteArray &cipherText)
{
EncryptionUtils::EncryptionInfo info;
@@ -986,36 +941,6 @@ QVector<uint8_t> EncryptionUtils::toArrayBuffer(const QByteArray &ba)
return byteVector;
}
-// return crypto.subtle.importKey(
-// 'jwk',
-// keyData,
-// {
-// name: 'RSA-OAEP',
-// modulusLength: 2048,
-// publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
-// hash: { name: 'SHA-256' },
-// },
-// true,
-// keyUsages,
-// );
-void EncryptionUtils::importRSAKey()
-{
- // TODO
-}
-
-// return crypto.subtle.importKey('jwk', keyData, { name: 'AES-CBC' }, true, keyUsages);
-void EncryptionUtils::importAESKey()
-{
-#if 0
- export async function importAESKey(keyData, keyUsages = ['encrypt', 'decrypt']) {
- return crypto.subtle.importKey('jwk', keyData, { name: 'AES-CBC' }, true, keyUsages);
- }
-
-#endif
-
- // TODO
-}
-
// crypto.subtle.importKey('raw', keyData, { name: 'PBKDF2' }, false, keyUsages);
QByteArray EncryptionUtils::importRawKey(const QByteArray &keyData, const QByteArray &salt, int iterations)
{
diff --git a/src/core/encryption/encryptionutils.h b/src/core/encryption/encryptionutils.h
index 2c09c72876..995f5d7b88 100644
--- a/src/core/encryption/encryptionutils.h
+++ b/src/core/encryption/encryptionutils.h
@@ -38,6 +38,7 @@ struct RSAKeyPair {
[[nodiscard]] LIBRUQOLACORE_EXPORT QByteArray encryptAES_CBC_128(const QByteArray &data, const QByteArray &key, const QByteArray &iv);
[[nodiscard]] LIBRUQOLACORE_EXPORT QByteArray decryptAES_CBC_128(const QByteArray &data, const QByteArray &key, const QByteArray &iv);
[[nodiscard]] LIBRUQOLACORE_EXPORT QByteArray decryptAES_GCM_256(const QByteArray &ciphertext, const QByteArray &key, const QByteArray &iv);
+[[nodiscard]] LIBRUQOLACORE_EXPORT QByteArray encryptAES_GCM_256(const QByteArray &plainText, const QByteArray &key, const QByteArray &iv);
[[nodiscard]] LIBRUQOLACORE_EXPORT QByteArray privateKeyJWKToPEM(const QByteArray &jwkJson);
[[nodiscard]] LIBRUQOLACORE_EXPORT QByteArray encryptMessage(const QByteArray &plainText, const QByteArray &sessionKey);
[[nodiscard]] LIBRUQOLACORE_EXPORT QByteArray decryptMessage(const QByteArray &plainText, const QByteArray &sessionKey);
@@ -55,8 +56,6 @@ struct RSAKeyPair {
[[nodiscard]] LIBRUQOLACORE_EXPORT QByteArray joinVectorAndEcryptedData(const EncryptionUtils::EncryptionInfo &info);
[[nodiscard]] LIBRUQOLACORE_EXPORT QVector<uint8_t> toArrayBuffer(const QByteArray &ba);
[[nodiscard]] LIBRUQOLACORE_EXPORT QByteArray importRawKey(const QByteArray &keyData, const QByteArray &salt, int iterations);
-LIBRUQOLACORE_EXPORT void importRSAKey();
-LIBRUQOLACORE_EXPORT void importAESKey();
[[nodiscard]] LIBRUQOLACORE_EXPORT QString generateRandomPassword();
};
Q_DECLARE_TYPEINFO(EncryptionUtils::EncryptionInfo, Q_RELOCATABLE_TYPE);
diff --git a/src/core/messages/messageencrypted.cpp b/src/core/messages/messageencrypted.cpp
index 09a7effb67..88bf023408 100644
--- a/src/core/messages/messageencrypted.cpp
+++ b/src/core/messages/messageencrypted.cpp
@@ -110,6 +110,50 @@ QByteArray MessageEncrypted::decrypt(const QByteArray &sessionKey) const
#endif
}
+bool MessageEncrypted::encrypt(const QByteArray &plainText, const QByteArray &sessionKey, const QByteArray &keyId)
+{
+#if USE_E2E_SUPPORT
+ if (plainText.isEmpty()) {
+ qCWarning(RUQOLA_ENCRYPTION_LOG) << "MessageEncrypted::encrypt: plaintext is empty";
+ return false;
+ }
+
+ if (sessionKey.isEmpty()) {
+ qCWarning(RUQOLA_ENCRYPTION_LOG) << "MessageEncrypted::encrypt: session key is empty";
+ return false;
+ }
+
+ const QByteArray effectiveKeyId = keyId.isEmpty() ? mKeyId : keyId;
+ if (effectiveKeyId.isEmpty()) {
+ qCWarning(RUQOLA_ENCRYPTION_LOG) << "MessageEncrypted::encrypt: key id is empty";
+ return false;
+ }
+
+ const QByteArray generatedIv = EncryptionUtils::generateRandomIV(12);
+ if (generatedIv.isEmpty()) {
+ qCWarning(RUQOLA_ENCRYPTION_LOG) << "MessageEncrypted::encrypt: failed to generate iv";
+ return false;
+ }
+
+ const QByteArray encryptedPayload = EncryptionUtils::encryptAES_GCM_256(plainText, sessionKey, generatedIv);
+ if (encryptedPayload.isEmpty()) {
+ qCWarning(RUQOLA_ENCRYPTION_LOG) << "MessageEncrypted::encrypt: encryption failed";
+ return false;
+ }
+
+ mAlgorithm = "rc.v2.aes-sha2";
+ mKeyId = effectiveKeyId;
+ mIv = generatedIv.toBase64();
+ mCiphertext = QString::fromLatin1(encryptedPayload.toBase64());
+ return true;
+#else
+ Q_UNUSED(plainText)
+ Q_UNUSED(sessionKey)
+ Q_UNUSED(keyId)
+ return false;
+#endif
+}
+
bool MessageEncrypted::operator==(const MessageEncrypted &other) const
{
return mAlgorithm == other.algorithm() && mCiphertext == other.ciphertext() && mKeyId == other.keyId() && mIv == other.iv();
diff --git a/src/core/messages/messageencrypted.h b/src/core/messages/messageencrypted.h
index 7da22e0253..a2c77f8dc8 100644
--- a/src/core/messages/messageencrypted.h
+++ b/src/core/messages/messageencrypted.h
@@ -39,6 +39,7 @@ public:
void setIv(const QByteArray &newIv);
[[nodiscard]] QByteArray decrypt(const QByteArray &sessionKey) const;
+ [[nodiscard]] bool encrypt(const QByteArray &plainText, const QByteArray &sessionKey, const QByteArray &keyId = {});
private:
QByteArray mAlgorithm;