aboutsummaryrefslogtreecommitdiff
path: root/src/client/QXmppFileEncryption.cpp
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 /src/client/QXmppFileEncryption.cpp
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.
Diffstat (limited to 'src/client/QXmppFileEncryption.cpp')
-rw-r--r--src/client/QXmppFileEncryption.cpp279
1 files changed, 279 insertions, 0 deletions
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