diff options
| author | Linus Jahn <lnj@kaidan.im> | 2022-09-14 22:36:11 +0200 |
|---|---|---|
| committer | Linus Jahn <lnj@kaidan.im> | 2022-09-24 18:11:58 +0200 |
| commit | c8bc1db682c165853ad51e2806f932e4fd0b0597 (patch) | |
| tree | 8bd06a8b72ef038ee2ffaca8881656df950d84b8 /src/client | |
| parent | 9d9d0b22664c6860a005818e9e787670aec389ff (diff) | |
| download | qxmpp-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')
| -rw-r--r-- | src/client/QXmppFileEncryption.cpp | 279 | ||||
| -rw-r--r-- | src/client/QXmppFileEncryption.h | 74 |
2 files changed, 353 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 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 |
