aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLinus Jahn <lnj@kaidan.im>2022-09-14 22:36:11 +0200
committerLinus Jahn <lnj@kaidan.im>2022-09-24 18:11:58 +0200
commitc8bc1db682c165853ad51e2806f932e4fd0b0597 (patch)
tree8bd06a8b72ef038ee2ffaca8881656df950d84b8
parent9d9d0b22664c6860a005818e9e787670aec389ff (diff)
downloadqxmpp-c8bc1db682c165853ad51e2806f932e4fd0b0597.tar.gz
Add file encryption functions and Encryption/DecryptionDevice
The devices allow it to encrypt or decrypt data on the fly when reading or writing data.
-rw-r--r--CMakeLists.txt15
-rw-r--r--src/CMakeLists.txt6
-rw-r--r--src/client/QXmppFileEncryption.cpp279
-rw-r--r--src/client/QXmppFileEncryption.h74
-rw-r--r--tests/CMakeLists.txt4
-rw-r--r--tests/qxmppfileencryption/tst_qxmppfileencryption.cpp107
6 files changed, 482 insertions, 3 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5f0a3f97..e2859dce 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -16,6 +16,11 @@ set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${PROJECT_SOURCE_DIR}/cmake/modules"
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Network Xml)
find_package(Qt${QT_VERSION_MAJOR} 5.9.2 REQUIRED COMPONENTS Core Network Xml)
+# QCA (optional)
+find_package(Qca-qt${QT_VERSION_MAJOR})
+if(${QT_VERSION_MAJOR} EQUAL 6)
+ find_package(Qt6Core5Compat)
+endif()
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
@@ -28,6 +33,7 @@ option(BUILD_DOCUMENTATION "Build API documentation." OFF)
option(BUILD_EXAMPLES "Build examples." ON)
option(BUILD_OMEMO "Build the OMEMO module" OFF)
option(WITH_GSTREAMER "Build with GStreamer support for Jingle" OFF)
+option(WITH_QCA "Build with QCA for OMEMO or encrypted file sharing" ${Qca-qt${QT_VERSION_MAJOR}_FOUND})
add_definitions(
-DQT_DISABLE_DEPRECATED_BEFORE=0x050F00
@@ -44,12 +50,15 @@ if(BUILD_OMEMO)
pkg_check_modules(OmemoC REQUIRED IMPORTED_TARGET libomemo-c)
# QCA
- find_package(Qca-qt${QT_VERSION_MAJOR} REQUIRED)
- if(${QT_VERSION_MAJOR} EQUAL 6)
- find_package(Qt6Core5Compat REQUIRED)
+ if(NOT WITH_QCA)
+ message(FATAL_ERROR "OMEMO requires QCA (Qt Cryptographic Architecture)")
endif()
endif()
+if(WITH_QCA)
+ add_definitions(-DWITH_QCA)
+endif()
+
add_subdirectory(src)
if(BUILD_TESTS)
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 9b82ff1d..bd79a5a7 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -333,6 +333,12 @@ if(WITH_GSTREAMER)
)
endif()
+if(WITH_QCA)
+ target_sources(qxmpp PRIVATE client/QXmppFileEncryption.cpp client/QcaInitializer.cpp)
+ target_link_libraries(qxmpp PRIVATE qca-qt${QT_VERSION_MAJOR})
+ target_compile_definitions(qxmpp PRIVATE -DWITH_QCA)
+endif()
+
install(
TARGETS qxmpp
DESTINATION "${CMAKE_INSTALL_LIBDIR}"
diff --git a/src/client/QXmppFileEncryption.cpp b/src/client/QXmppFileEncryption.cpp
new file mode 100644
index 00000000..99ccd474
--- /dev/null
+++ b/src/client/QXmppFileEncryption.cpp
@@ -0,0 +1,279 @@
+// SPDX-FileCopyrightText: 2022 Linus Jahn <lnj@kaidan.im>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "QXmppFileEncryption.h"
+
+#include <QByteArray>
+#include <QtCrypto>
+
+using namespace QCA;
+
+constexpr std::size_t AES128_BLOCK_SIZE = 128 / 8;
+constexpr std::size_t AES256_BLOCK_SIZE = 256 / 8;
+constexpr int GCM_IV_SIZE = 12;
+
+namespace QXmpp::Private::Encryption {
+
+static QString cipherName(QXmpp::Cipher cipher)
+{
+ switch (cipher) {
+ case Aes128GcmNoPad:
+ return QStringLiteral("aes128");
+ case Aes256GcmNoPad:
+ case Aes256CbcPkcs7:
+ return QStringLiteral("aes256");
+ }
+ Q_UNREACHABLE();
+}
+
+static std::size_t blockSize(QXmpp::Cipher cipher)
+{
+ switch (cipher) {
+ case Aes128GcmNoPad:
+ return AES128_BLOCK_SIZE;
+ case Aes256GcmNoPad:
+ case Aes256CbcPkcs7:
+ return AES256_BLOCK_SIZE;
+ }
+ Q_UNREACHABLE();
+}
+
+static QCA::Cipher::Mode cipherMode(QXmpp::Cipher cipher)
+{
+ switch (cipher) {
+ case Aes128GcmNoPad:
+ case Aes256GcmNoPad:
+ return QCA::Cipher::GCM;
+ case Aes256CbcPkcs7:
+ return QCA::Cipher::CBC;
+ }
+ Q_UNREACHABLE();
+}
+
+static QCA::Cipher::Padding padding(QXmpp::Cipher cipher)
+{
+ switch (cipher) {
+ case Aes128GcmNoPad:
+ case Aes256GcmNoPad:
+ return QCA::Cipher::NoPadding;
+ case Aes256CbcPkcs7:
+ return QCA::Cipher::PKCS7;
+ }
+ Q_UNREACHABLE();
+}
+
+QCA::Direction toQcaDirection(Direction direction)
+{
+ switch (direction) {
+ case Encode:
+ return QCA::Encode;
+ case Decode:
+ return QCA::Decode;
+ }
+ Q_UNREACHABLE();
+}
+
+static std::size_t roundUpToBlockSize(qint64 size, std::size_t blockSize)
+{
+ Q_ASSERT(size >= 0);
+ return (size / blockSize + 1) * blockSize;
+}
+
+QByteArray process(const QByteArray &data, QXmpp::Cipher cipherConfig, Direction direction, const QByteArray &key, const QByteArray &iv)
+{
+ return QCA::Cipher(cipherName(cipherConfig),
+ cipherMode(cipherConfig),
+ padding(cipherConfig),
+ toQcaDirection(direction),
+ SymmetricKey(key),
+ InitializationVector(iv))
+ .process(MemoryRegion(data))
+ .toByteArray();
+}
+
+QByteArray generateKey(QXmpp::Cipher cipher)
+{
+ return Random::randomArray(int(blockSize(cipher))).toByteArray();
+}
+
+QByteArray generateInitializationVector(QXmpp::Cipher config)
+{
+ switch (config) {
+ case Aes128GcmNoPad:
+ case Aes256GcmNoPad:
+ return Random::randomArray(GCM_IV_SIZE).toByteArray();
+ case Aes256CbcPkcs7:
+ return Random::randomArray(int(blockSize(config))).toByteArray();
+ }
+ Q_UNREACHABLE();
+}
+
+EncryptionDevice::EncryptionDevice(std::unique_ptr<QIODevice> input,
+ Cipher config,
+ const QByteArray &key,
+ const QByteArray &iv)
+ : m_cipherConfig(config),
+ m_input(std::move(input)),
+ m_cipher(std::make_unique<QCA::Cipher>(
+ cipherName(config),
+ cipherMode(config),
+ padding(config),
+ QCA::Encode,
+ SymmetricKey(key),
+ InitializationVector(iv)))
+{
+ // output must not be sequential
+ Q_ASSERT(!m_input->isSequential());
+
+ Q_ASSERT(m_outputBuffer.empty());
+
+ setOpenMode(m_input->openMode() & QIODevice::ReadOnly);
+}
+
+EncryptionDevice::~EncryptionDevice() = default;
+
+bool EncryptionDevice::open(OpenMode mode)
+{
+ return m_input->open(mode);
+}
+
+void EncryptionDevice::close()
+{
+ m_input->close();
+}
+
+bool EncryptionDevice::isSequential() const
+{
+ return false;
+}
+
+qint64 EncryptionDevice::size() const
+{
+ switch (m_cipherConfig) {
+ case Aes128GcmNoPad:
+ case Aes256GcmNoPad:
+ return m_input->size();
+ case Aes256CbcPkcs7: {
+ // padding is done with 128 bits blocks
+ return roundUpToBlockSize(m_input->size(), 128 / 8);
+ }
+ }
+ Q_UNREACHABLE();
+}
+
+qint64 EncryptionDevice::readData(char *data, qint64 len)
+{
+ auto requestedLen = len;
+ qint64 read = 0;
+
+ {
+ // try to read from output buffer
+ qint64 outputBufferRead = std::min(qint64(m_outputBuffer.size()), len);
+ std::copy_n(m_outputBuffer.cbegin(), outputBufferRead, data);
+ m_outputBuffer.erase(m_outputBuffer.begin(), m_outputBuffer.begin() + outputBufferRead);
+ read += outputBufferRead;
+ len -= outputBufferRead;
+ }
+
+ if (len > 0) {
+ // read from input and encrypt new data
+
+ // output buffer is empty here
+ Q_ASSERT(m_outputBuffer.empty());
+
+ // read unencrypted data (may read one block more than needed)
+ auto inputBufferSize = roundUpToBlockSize(len, blockSize(m_cipherConfig));
+ Q_ASSERT(inputBufferSize > 0);
+ QByteArray inputBuffer;
+ inputBuffer.resize(inputBufferSize);
+ inputBuffer.resize(m_input->read(inputBuffer.data(), inputBufferSize));
+
+ // process input buffer
+ auto processed = [&]() {
+ if (inputBuffer.isEmpty()) {
+ m_finalized = true;
+ return m_cipher->final();
+ }
+ // encrypt data
+ return m_cipher->process(MemoryRegion(inputBuffer));
+ }();
+
+ // split up into part for user and put rest into output buffer
+ auto processedReadBytes = std::min(qint64(processed.size()), len);
+ std::copy_n(processed.constData(), processedReadBytes, data + read);
+ read += processedReadBytes;
+ len -= processedReadBytes;
+
+ Q_ASSERT(processed.size() >= processedReadBytes);
+ auto restBytes = size_t(processed.size() - processedReadBytes);
+ m_outputBuffer.resize(restBytes);
+ std::copy_n(processed.constData() + processedReadBytes, restBytes, m_outputBuffer.data());
+ }
+
+ Q_ASSERT((len + read) == requestedLen);
+ return read;
+}
+
+qint64 EncryptionDevice::writeData(const char *, qint64)
+{
+ return 0;
+}
+
+DecryptionDevice::DecryptionDevice(std::unique_ptr<QIODevice> input,
+ Cipher config,
+ const QByteArray &key,
+ const QByteArray &iv)
+ : m_cipherConfig(config),
+ m_output(std::move(input)),
+ m_cipher(std::make_unique<QCA::Cipher>(
+ cipherName(config),
+ cipherMode(config),
+ padding(config),
+ QCA::Decode,
+ SymmetricKey(key),
+ InitializationVector(iv)))
+{
+ // output must not be sequential
+ Q_ASSERT(!m_output->isSequential());
+
+ Q_ASSERT(m_outputBuffer.empty());
+
+ setOpenMode(m_output->openMode() & QIODevice::WriteOnly);
+}
+
+DecryptionDevice::~DecryptionDevice() = default;
+
+bool DecryptionDevice::open(OpenMode mode)
+{
+ return m_output->open(mode);
+}
+
+void DecryptionDevice::close()
+{
+ m_output->close();
+}
+
+bool DecryptionDevice::isSequential() const
+{
+ return true;
+}
+
+qint64 DecryptionDevice::size() const
+{
+ return 0;
+}
+
+qint64 DecryptionDevice::readData(char *, qint64)
+{
+ return 0;
+}
+
+qint64 DecryptionDevice::writeData(const char *data, qint64 len)
+{
+ auto decrypted = m_cipher->process(QByteArray(data, len));
+ m_output->write(decrypted.constData(), decrypted.size());
+ return len;
+}
+
+} // namespace QXmpp::Private::Encryption
diff --git a/src/client/QXmppFileEncryption.h b/src/client/QXmppFileEncryption.h
new file mode 100644
index 00000000..b1108b22
--- /dev/null
+++ b/src/client/QXmppFileEncryption.h
@@ -0,0 +1,74 @@
+// SPDX-FileCopyrightText: 2022 Linus Jahn <lnj@kaidan.im>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#ifndef QXMPPFILEENCRYPTION_H
+#define QXMPPFILEENCRYPTION_H
+
+#include "QXmppGlobal.h"
+
+#include <memory>
+
+#include <QIODevice>
+
+namespace QCA {
+class Cipher;
+class Initializer;
+} // namespace QCA
+
+namespace QXmpp::Private::Encryption {
+
+enum Direction {
+ Encode,
+ Decode,
+};
+
+QByteArray process(const QByteArray &data, Cipher cipherConfig, Direction direction, const QByteArray &key, const QByteArray &iv);
+QByteArray generateKey(Cipher cipher);
+QByteArray generateInitializationVector(Cipher);
+
+// export for tests
+class QXMPP_EXPORT EncryptionDevice : public QIODevice
+{
+public:
+ EncryptionDevice(std::unique_ptr<QIODevice> input, Cipher config, const QByteArray &key, const QByteArray &iv);
+ ~EncryptionDevice() override;
+
+ bool open(QIODevice::OpenMode mode) override;
+ void close() override;
+ bool isSequential() const override;
+ qint64 size() const override;
+ qint64 readData(char *data, qint64 maxlen) override;
+ qint64 writeData(const char *data, qint64 len) override;
+
+private:
+ Cipher m_cipherConfig;
+ bool m_finalized = false;
+ std::vector<char> m_outputBuffer;
+ std::unique_ptr<QIODevice> m_input;
+ std::unique_ptr<QCA::Cipher> m_cipher;
+};
+
+class DecryptionDevice : public QIODevice
+{
+public:
+ DecryptionDevice(std::unique_ptr<QIODevice> output, Cipher config, const QByteArray &key, const QByteArray &iv);
+ ~DecryptionDevice() override;
+
+ bool open(QIODevice::OpenMode mode) override;
+ void close() override;
+ bool isSequential() const override;
+ qint64 size() const override;
+ qint64 readData(char *data, qint64 maxlen) override;
+ qint64 writeData(const char *data, qint64 len) override;
+
+private:
+ Cipher m_cipherConfig;
+ std::vector<char> m_outputBuffer;
+ std::unique_ptr<QIODevice> m_output;
+ std::unique_ptr<QCA::Cipher> m_cipher;
+};
+
+} // namespace QXmpp::Private::Encryption
+
+#endif // QXMPPFILEENCRYPTION_H
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index e8427a61..e43b50c6 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -77,6 +77,10 @@ add_simple_test(qxmppvcardiq)
add_simple_test(qxmppvcardmanager)
add_simple_test(qxmppversioniq)
+if(WITH_QCA)
+ add_simple_test(qxmppfileencryption)
+endif()
+
if(WITH_GSTREAMER)
add_simple_test(qxmppcallmanager)
endif()
diff --git a/tests/qxmppfileencryption/tst_qxmppfileencryption.cpp b/tests/qxmppfileencryption/tst_qxmppfileencryption.cpp
new file mode 100644
index 00000000..dec935f3
--- /dev/null
+++ b/tests/qxmppfileencryption/tst_qxmppfileencryption.cpp
@@ -0,0 +1,107 @@
+// SPDX-FileCopyrightText: 2022 Linus Jahn <lnj@kaidan.im>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "QXmppFileEncryption.h"
+
+#include "QcaInitializer_p.h"
+#include <QtTest>
+
+using namespace QXmpp;
+using namespace QXmpp::Private;
+using namespace QXmpp::Private::Encryption;
+
+class tst_QXmppFileEncryption : public QObject
+{
+ Q_OBJECT
+
+private:
+ Q_SLOT void basic();
+ Q_SLOT void deviceEncrypt();
+ Q_SLOT void deviceDecrypt();
+ Q_SLOT void paddingSize();
+};
+
+void tst_QXmppFileEncryption::basic()
+{
+ QcaInitializer encInit;
+
+ QByteArray data =
+ "This is an example text message";
+ QByteArray key = "12345678901234567890123456789012";
+ QByteArray iv = "data";
+
+ auto encrypted = process(data, Aes256CbcPkcs7, Encode, key, iv);
+ qDebug() << data.size() << "->" << encrypted.size();
+ auto decrypted = process(encrypted, Aes256CbcPkcs7, Decode, key, iv);
+ QCOMPARE(decrypted, data);
+}
+
+void tst_QXmppFileEncryption::deviceEncrypt()
+{
+ QcaInitializer encInit;
+
+ QByteArray data =
+ "v2qtI8tx5DxM6axUAZ+xsEwrtb0VYafAPlMWqpVMG+5PBE5wbZ7MZhDUEIdFkxchOIJqt";
+ QByteArray key = "12345678901234567890123456789012";
+ QByteArray iv = "12345678901234567890123456789012";
+
+ auto buffer = std::make_unique<QBuffer>(&data);
+ buffer->open(QIODevice::ReadOnly);
+
+ EncryptionDevice encDev(std::move(buffer), Aes256CbcPkcs7, key, iv);
+
+ auto encrypted = encDev.readAll();
+
+ auto decrypted = process(encrypted, Aes256CbcPkcs7, Decode, key, iv);
+ QCOMPARE(decrypted, data);
+}
+
+void tst_QXmppFileEncryption::deviceDecrypt()
+{
+ QcaInitializer encInit;
+
+ QByteArray data =
+ "v2qtI8tx5DxM6axUAZ+xsEwrtb0VYafAPlMWqpVMG+5PBE5wbZ7MZhDUEIdFkxchOIJqt";
+ QByteArray key = "12345678901234567890123456789012";
+ QByteArray iv = "12345678901234567890123456789012";
+
+ auto encrypted = process(data, Aes256CbcPkcs7, Encode, key, iv);
+
+ QByteArray decrypted;
+ auto buffer = std::make_unique<QBuffer>(&decrypted);
+ buffer->open(QIODevice::WriteOnly);
+
+ DecryptionDevice decDev(std::move(buffer), Aes256CbcPkcs7, key, iv);
+ decDev.write(encrypted);
+
+ QCOMPARE(decrypted, data);
+}
+
+void tst_QXmppFileEncryption::paddingSize()
+{
+ constexpr auto MAX_BYTES_TEST = 1024;
+
+ QcaInitializer encInit;
+
+ QByteArray key = "12345678901234567890123456789012";
+ QByteArray iv = "12345678901234567890123456789012";
+
+ for (int i = 1; i <= MAX_BYTES_TEST; i++) {
+ QByteArray data(i, 'a');
+ auto buffer = std::make_unique<QBuffer>(&data);
+ buffer->open(QIODevice::ReadOnly);
+
+ EncryptionDevice encDev(std::move(buffer), Aes256CbcPkcs7, key, iv);
+ auto reportedSize = encDev.size();
+ auto encryptedData = encDev.readAll();
+
+ QCOMPARE(reportedSize, encryptedData.size());
+
+ auto decryptedData = process(encryptedData, Aes256CbcPkcs7, Decode, key, iv);
+ QCOMPARE(decryptedData, data);
+ }
+}
+
+QTEST_MAIN(tst_QXmppFileEncryption)
+#include "tst_qxmppfileencryption.moc"