Skip to content

Commit

Permalink
Spans all over
Browse files Browse the repository at this point in the history
  • Loading branch information
Kitsune Ral committed Dec 21, 2023
1 parent abebca5 commit 428f12f
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 134 deletions.
21 changes: 10 additions & 11 deletions Quotient/database.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -490,18 +490,19 @@ QString Database::edKeyForKeyId(const QString& userId, const QString& edKeyId)

void Database::storeEncrypted(const QString& name, const QByteArray& key)
{
auto iv = getRandom<16>();
auto result = aesCtr256Encrypt(key, m_picklingKey.viewAsByteArray().left(32), iv.viewAsByteArray());
if (!result.has_value()) {
//TODO: Error
}
auto iv = getRandom<AesBlockSize>();
auto result =
aesCtr256Encrypt(key, asCBytes(m_picklingKey).first<AesKeySize>(), iv);
if (!result.has_value())
return;

auto cipher = result.value().toBase64();
auto query = prepareQuery(QStringLiteral("INSERT INTO encrypted(name, cipher, iv) VALUES(:name, :cipher, :iv);"));
auto deleteQuery = prepareQuery(QStringLiteral("DELETE FROM encrypted WHERE name=:name;"));
deleteQuery.bindValue(":name"_ls, name);
query.bindValue(":name"_ls, name);
query.bindValue(":cipher"_ls, cipher);
query.bindValue(":iv"_ls, iv.viewAsByteArray().toBase64());
query.bindValue(":iv"_ls, iv.toBase64());
transaction();
execute(deleteQuery);
execute(query);
Expand All @@ -518,9 +519,7 @@ QByteArray Database::loadEncrypted(const QString& name)
}
auto cipher = QByteArray::fromBase64(query.value("cipher"_ls).toString().toLatin1());
auto iv = QByteArray::fromBase64(query.value("iv"_ls).toString().toLatin1());
auto result = aesCtr256Decrypt(cipher, m_picklingKey.viewAsByteArray().left(32), iv);
if (!result.has_value()) {
//TODO: Error
}
return result.value();
return aesCtr256Decrypt(cipher, asCBytes(m_picklingKey).first<AesKeySize>(),
asCBytes<AesBlockSize>(iv))
.move_value_or({});
}
153 changes: 84 additions & 69 deletions Quotient/e2ee/cryptoutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ inline std::pair<int, bool> checkedSize(
std::type_identity_t<SizeT> maxSize = std::numeric_limits<int>::max())
// ^ NB: usage of type_identity_t disables type deduction
{
if (uncheckedSize < maxSize)
Q_ASSERT(uncheckedSize >= 0 && maxSize >= 0);
if (uncheckedSize <= maxSize)
return { static_cast<int>(uncheckedSize), false };

qCCritical(E2EE) << "Cryptoutils:" << uncheckedSize
Expand All @@ -59,75 +60,90 @@ inline std::pair<int, bool> checkedSize(
return { maxSize, true };
}

// NOLINTBEGIN(cppcoreguidelines-pro-bounds-array-to-pointer-decay)
// TODO: remove NOLINT brackets once we're on clang-tidy 18
#define CLAMP_SIZE(SizeVar_, ByteArray_, ...) \
const auto [SizeVar_, ByteArray_##Clamped] = \
checkedSize((ByteArray_).size() __VA_OPT__(, ) __VA_ARGS__); \
if (ByteArray_##Clamped) { \
qCCritical(E2EE).nospace() \
<< __func__ << ": " #ByteArray_ " is " << (ByteArray_).size() \
<< " bytes long, too much for OpenSSL and overall suspicious"; \
Q_ASSERT(!ByteArray_##Clamped); /* Always false */ \
return SslPayloadTooLong; \
} \
// End of macro

#define CALL_OPENSSL(Call_) \
do { \
if ((Call_) <= 0) { \
qCWarning(E2EE) << __func__ << "failed to call OpenSSL API:" \
<< ERR_error_string(ERR_get_error(), nullptr); \
return ERR_get_error(); \
} \
} while (false) // End of macro
// NOLINTEND(cppcoreguidelines-pro-bounds-array-to-pointer-decay)

SslExpected<QByteArray> Quotient::pbkdf2HmacSha512(const QByteArray& password,
const QByteArray& salt,
int iterations, int keyLength)
{
CLAMP_SIZE(passwordSize, password)
CLAMP_SIZE(saltSize, salt)
auto output = zeroedByteArray(keyLength);
if (!PKCS5_PBKDF2_HMAC(password.data(), password.size(), reinterpret_cast<const unsigned char *>(salt.data()), salt.length(), iterations, EVP_sha512(), keyLength, reinterpret_cast<unsigned char *>(output.data()))) {
qWarning() << ERR_error_string(ERR_get_error(), nullptr);
return ERR_get_error();
}
CALL_OPENSSL(
PKCS5_PBKDF2_HMAC(password.data(), passwordSize, asCBytes(salt).data(),
saltSize, iterations, EVP_sha512(), keyLength,
reinterpret_cast<unsigned char*>(output.data())));
return output;
}

SslExpected<QByteArray> Quotient::aesCtr256Encrypt(const QByteArray& plaintext,
const QByteArray& key,
const QByteArray& iv)
byte_view_t<AesKeySize> key,
byte_view_t<AesBlockSize> iv)
{
const auto [plaintextSize, clamped] =
checkedSize(plaintext.size(),
std::numeric_limits<int>::max() - AesBlockSize);
Q_ASSERT(!clamped); // Normally the caller should check this
CLAMP_SIZE(plaintextSize, plaintext,
std::numeric_limits<int>::max() - iv.size())

auto encrypted = getRandom(unsignedSize(plaintext) + AesBlockSize);
auto encrypted = getRandom(static_cast<size_t>(plaintextSize) + iv.size());
auto encryptedSpan = asWritableCBytes(encrypted);
constexpr auto mask = static_cast<uint8_t>(~(1U << (63 / 8)));
encrypted[15 - 63 % 8] &= mask;
encryptedSpan[15 - 63 % 8] &= mask;

const ContextHolder ctx(EVP_CIPHER_CTX_new(), &EVP_CIPHER_CTX_free);
if (!ctx) {
qWarning() << ERR_error_string(ERR_get_error(), nullptr);
qCCritical(E2EE) << __func__ << "failed to create SSL context:"
<< ERR_error_string(ERR_get_error(), nullptr);
return ERR_get_error();
}

if (EVP_EncryptInit_ex(ctx.get(), EVP_aes_256_ctr(), nullptr, reinterpret_cast<const unsigned char*>(key.constData()), reinterpret_cast<const unsigned char*>(iv.constData())) != 1) {
qWarning() << ERR_error_string(ERR_get_error(), nullptr);
return ERR_get_error();
}
CALL_OPENSSL(EVP_EncryptInit_ex(ctx.get(), EVP_aes_256_ctr(), nullptr,
key.data(), iv.data()));

int length = 0;
if (EVP_EncryptUpdate(ctx.get(), encrypted.data(), &length, reinterpret_cast<const unsigned char *>(plaintext.constData()), plaintextSize) != 1) {
qWarning() << ERR_error_string(ERR_get_error(), nullptr);
return ERR_get_error();
}
int encryptedLength = 0;
CALL_OPENSSL(EVP_EncryptUpdate(ctx.get(), encrypted.data(), &encryptedLength,
reinterpret_cast<const unsigned char*>(
plaintext.constData()),
plaintextSize));
Q_ASSERT(encryptedLength >= 0);

int ciphertextLength = length;
if (EVP_EncryptFinal_ex(ctx.get(), encrypted.data() + length, &length) != 1) {
qWarning() << ERR_error_string(ERR_get_error(), nullptr);
return ERR_get_error();
}
int tailLength = -1; // Normally becomes 0 after the next call
CALL_OPENSSL(EVP_EncryptFinal_ex(
ctx.get(),
encryptedSpan.subspan(static_cast<size_t>(encryptedLength)).data(),
&tailLength));

ciphertextLength += length;
return encrypted.viewAsByteArray().left(ciphertextLength);
return encrypted.copyToByteArray(encryptedLength + tailLength);
}

#define CALL_OPENSSL(Call_) \
do { \
if ((Call_) != 1) { \
qWarning() << ERR_error_string(ERR_get_error(), nullptr); \
return ERR_get_error(); \
} \
} while (false) // End of macro

SslExpected<HkdfKeys> Quotient::hkdfSha256(const QByteArray& key,
const QByteArray& salt,
const QByteArray& info)
{
const auto saltSize = checkedSize(salt.size()).first,
keySize = checkedSize(key.size()).first,
infoSize = checkedSize(info.size()).first;
constexpr QByteArray::size_type ResultSize = 64u;
auto result = zeroedByteArray(ResultSize);
HkdfKeys result;
const ContextHolder context(EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, nullptr),
&EVP_PKEY_CTX_free);

Expand All @@ -142,23 +158,21 @@ SslExpected<HkdfKeys> Quotient::hkdfSha256(const QByteArray& key,
CALL_OPENSSL(EVP_PKEY_CTX_add1_hkdf_info(
context.get(), reinterpret_cast<const unsigned char*>(info.constData()),
infoSize));
size_t outputLength = ResultSize;
size_t outputLength = result.size();
CALL_OPENSSL(EVP_PKEY_derive(context.get(),
reinterpret_cast<unsigned char*>(result.data()),
&outputLength));
if (outputLength != ResultSize) {
if (outputLength != result.size()) {
qCCritical(E2EE) << "hkdfSha256: the shared secret is" << outputLength
<< "bytes instead of" << ResultSize;
<< "bytes instead of" << result.size();
Q_ASSERT(false);
return WrongDerivedKeyLength;
}

auto macKey = result.mid(32);
result.resize(32);
return HkdfKeys{std::move(result), std::move(macKey)};
return result;
}

SslExpected<QByteArray> Quotient::hmacSha256(const QByteArray& hmacKey,
SslExpected<QByteArray> Quotient::hmacSha256(byte_view_t<HmacKeySize> hmacKey,
const QByteArray& data)
{
unsigned int len = SHA256_DIGEST_LENGTH;
Expand All @@ -171,8 +185,8 @@ SslExpected<QByteArray> Quotient::hmacSha256(const QByteArray& hmacKey,
}

SslExpected<QByteArray> Quotient::aesCtr256Decrypt(const QByteArray& ciphertext,
const QByteArray& key,
const QByteArray& iv)
byte_view_t<AesKeySize> key,
byte_view_t<AesBlockSize> iv)
{
const ContextHolder context(EVP_CIPHER_CTX_new(), &EVP_CIPHER_CTX_free);
if (!context) {
Expand All @@ -183,30 +197,31 @@ SslExpected<QByteArray> Quotient::aesCtr256Decrypt(const QByteArray& ciphertext,
return ERR_get_error();
}

int length = 0;
int plaintextLength = 0;
const int ciphertextSize = checkedSize(ciphertext.size()).first;
auto decrypted = zeroedByteArray(ciphertext.size());

if (EVP_DecryptInit_ex(context.get(), EVP_aes_256_ctr(), nullptr, reinterpret_cast<const unsigned char *>(key.constData()), reinterpret_cast<const unsigned char *>(iv.constData())) != 1) {
qWarning() << ERR_error_string(ERR_get_error(), nullptr);
return ERR_get_error();
const auto [ciphertextSize, clamped] = checkedSize(ciphertext.size());
if (clamped) {
qCCritical(E2EE) << "The ciphertext is too long to be decrypted";
Q_ASSERT(!clamped); // We just don't deal with such sizes in Matrix?
return SslPayloadTooLong;
}

if (EVP_DecryptUpdate(context.get(), reinterpret_cast<unsigned char*>(decrypted.data()), &length, reinterpret_cast<const unsigned char*>(ciphertext.constData()), ciphertextSize) != 1) {
qWarning() << ERR_error_string(ERR_get_error(), nullptr);
return ERR_get_error();
}
FixedBuffer<> decrypted(static_cast<size_t>(ciphertextSize),
FixedBufferBase::FillWithZeros);
CALL_OPENSSL(EVP_DecryptInit_ex(context.get(), EVP_aes_256_ctr(), nullptr,
key.data(), iv.data()));

plaintextLength = length;
if (EVP_DecryptFinal_ex(context.get(), reinterpret_cast<unsigned char*>(decrypted.data()) + length, &length) != 1) {
qWarning() << ERR_error_string(ERR_get_error(), nullptr);
return ERR_get_error();
}
int decryptedLength = 0;
CALL_OPENSSL(EVP_DecryptUpdate(
context.get(), decrypted.data(), &decryptedLength,
reinterpret_cast<const unsigned char*>(ciphertext.constData()),
ciphertextSize));

int tailLength = -1; // Normally becomes 0 after the next call
CALL_OPENSSL(EVP_DecryptFinal_ex(
context.get(),
asWritableCBytes(decrypted).subspan(static_cast<size_t>(decryptedLength)).data(),
&tailLength));

plaintextLength += length;
decrypted.resize(plaintextLength);
return decrypted;
return decrypted.copyToByteArray(decryptedLength + tailLength);
}

QOlmExpected<QByteArray> Quotient::curve25519AesSha2Decrypt(
Expand Down
46 changes: 29 additions & 17 deletions Quotient/e2ee/cryptoutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,23 @@
#include <QtCore/QString>

namespace Quotient {
struct QUOTIENT_API HkdfKeys {
//! @brief Key to be used for AES encryption / decryption
QByteArray aes;
//! @brief Key to be used for MAC creation / verification
QByteArray mac;

// Common remark: OpenSSL is a private dependency of libQuotient, meaning that
// headers of libQuotient can't include OpenSSL headers. Instead, alias
// the value or type along with a comment (see SslErrorCode e.g.) and add
// static_assert in the .cpp file to check it against the OpenSSL definition.

constexpr auto DefaultPbkdf2KeyLength = 32u;
constexpr auto AesKeySize = 32u;
constexpr auto AesBlockSize = 16u; // AES_BLOCK_SIZE
constexpr auto HmacKeySize = 32u;

struct QUOTIENT_API HkdfKeys
: public FixedBuffer<AesKeySize + HmacKeySize> {
//! \brief Key to be used for AES encryption / decryption
auto aes() const { return asCBytes(*this).first<AesKeySize>(); }
//! \brief Key to be used for MAC creation / verification
auto mac() const { return asCBytes(*this).last<HmacKeySize>(); }
};

struct QUOTIENT_API Curve25519Encrypted {
Expand All @@ -25,17 +37,14 @@ struct QUOTIENT_API Curve25519Encrypted {
QByteArray ephemeral;
};

// OpenSSL is a private dependency; can't refer to OpenSSL symbols from here

constexpr auto DefaultPbkdf2KeyLength = 32u;
constexpr auto AesBlockSize = 16u; // AES_BLOCK_SIZE

// NOLINTNEXTLINE(google-runtime-int): the type is copied from OpenSSL
using SslErrorCode = unsigned long; // decltype(ERR_get_error())

constexpr SslErrorCode SslErrorUserOffset = 128; // ERR_LIB_USER
[[maybe_unused]] constexpr SslErrorCode WrongDerivedKeyLength =
SslErrorUserOffset + 1;
enum SslErrorCodes : SslErrorCode {
SslErrorUserOffset = 128, // ERR_LIB_USER; never use this bare
WrongDerivedKeyLength = SslErrorUserOffset + 1,
SslPayloadTooLong = SslErrorUserOffset + 2
};

//! Same as QOlmExpected but for wrapping OpenSSL instead of Olm calls
template <typename T>
Expand All @@ -59,8 +68,9 @@ QUOTIENT_API SslExpected<HkdfKeys> hkdfSha256(const QByteArray& key,
const QByteArray& info);

//! Calculate a MAC from the given key and data
QUOTIENT_API SslExpected<QByteArray> hmacSha256(const QByteArray& hmacKey,
const QByteArray& data);
QUOTIENT_API SslExpected<QByteArray> hmacSha256(
byte_view_t<HmacKeySize> hmacKey,
const QByteArray& data);

//! \brief Decrypt the data using Curve25519-AES-Sha256
//! \note ciphertext must be given as base64
Expand All @@ -77,13 +87,15 @@ QUOTIENT_API QOlmExpected<Curve25519Encrypted> curve25519AesSha2Encrypt(
//!
//! key and iv have a length of 32 bytes
QUOTIENT_API SslExpected<QByteArray> aesCtr256Encrypt(
const QByteArray& plaintext, const QByteArray& key, const QByteArray& iv);
const QByteArray& plaintext, byte_view_t<AesKeySize> key,
byte_view_t<AesBlockSize> iv);

//! \brief Decrypt data using AES-CTR-256
//!
//! key and iv have a length of 32 bytes
QUOTIENT_API SslExpected<QByteArray> aesCtr256Decrypt(
const QByteArray& ciphertext, const QByteArray& key, const QByteArray& iv);
const QByteArray& ciphertext, byte_view_t<AesKeySize> key,
byte_view_t<AesBlockSize> iv);

QUOTIENT_API QByteArray base58Decode(const QByteArray& encoded);

Expand Down
7 changes: 7 additions & 0 deletions Quotient/e2ee/e2ee_common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ QByteArray Quotient::byteArrayForOlm(size_t bufferSize)
return {};
}

void Quotient::_impl::reportSpanShortfall(QByteArray::size_type inputSize,
size_t neededSize)
{
qCCritical(E2EE) << "Not enough bytes to create a valid span: " << inputSize
<< '<' << neededSize << "- undefined behaviour imminent";
}

void Quotient::fillFromSecureRng(std::span<byte_t> bytes)
{
// Discussion of QRandomGenerator::system() vs. OpenSSL's RAND_bytes
Expand Down
Loading

0 comments on commit 428f12f

Please sign in to comment.