Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 29 additions & 6 deletions src/Functions/FunctionsAES.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,37 @@ std::string_view foldEncryptionKeyInMySQLCompatitableMode(size_t cipher_key_size
return std::string_view(folded_key.data(), cipher_key_size);
}

const EVP_CIPHER * getCipherByName(std::string_view cipher_name)
EVP_CIPHER_ptr fetchCipher(std::string_view cipher_name)
{
// NOTE: cipher obtained not via EVP_CIPHER_fetch() would cause extra work on each context reset
// with EVP_CIPHER_CTX_reset() or EVP_EncryptInit_ex(), but using EVP_CIPHER_fetch()
// causes data race, so we stick to the slower but safer alternative here.
/// A cipher obtained via EVP_get_cipherbyname has prov == NULL, which makes OpenSSL 3.x
/// implicitly call EVP_CIPHER_fetch (with locking and provider lookups) on every
/// EVP_EncryptInit_ex / EVP_DecryptInit_ex invocation. Fetching the cipher explicitly
/// here returns a provider-backed cipher and avoids that per-row overhead.
/// Returns nullptr for an unknown cipher name; the caller reports it as an invalid mode.
/// We need a zero-terminated string here:
auto * fetched = EVP_CIPHER_fetch(nullptr, std::string{cipher_name}.c_str(), nullptr);
if (!fetched)
/// For an unknown name EVP_CIPHER_fetch pushes an "unsupported" entry onto the
/// thread-local OpenSSL error queue. The caller turns nullptr into a BAD_ARGUMENTS
/// "Invalid mode" without draining the queue via getOpenSSLErrors, so clear it here to
/// avoid leaking this stale entry into an unrelated OpenSSL error later reported on the
/// same thread (unlike EVP_get_cipherbyname, which never touched the queue).
ERR_clear_error();
return EVP_CIPHER_ptr(fetched, EVP_CIPHER_free);
}

/// We need zero-terminated string here:
return EVP_get_cipherbyname(std::string{cipher_name}.c_str());
std::string_view ecbEquivalentCipherName(std::string_view mode)
{
/// Only plain AES block ciphers. The list is intentionally exact: e.g.
/// aes-128-cbc-hmac-sha1 also reports EVP_CIPH_CBC_MODE but is not a plain
/// block cipher and must not take the block-composed fast path.
if (mode == "aes-128-ecb" || mode == "aes-128-cbc")
return "aes-128-ecb";
if (mode == "aes-192-ecb" || mode == "aes-192-cbc")
return "aes-192-ecb";
if (mode == "aes-256-ecb" || mode == "aes-256-cbc")
return "aes-256-ecb";
return {};
}

}
Expand Down
502 changes: 483 additions & 19 deletions src/Functions/FunctionsAES.h

Large diffs are not rendered by default.

31 changes: 27 additions & 4 deletions src/Functions/FunctionsHashing.h
Original file line number Diff line number Diff line change
Expand Up @@ -249,13 +249,36 @@ struct HalfMD5Impl
} buf;

using EVP_MD_CTX_ptr = std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)>;
const auto ctx = EVP_MD_CTX_ptr(EVP_MD_CTX_new(), EVP_MD_CTX_free);

/// A context is initialized with the MD5 digest once, then only copied on each call
/// (the same approach as in FunctionsStringHashFixedString.cpp). Copying an already
/// initialized context with `EVP_MD_CTX_copy_ex` is faster than re-initializing with
/// `EVP_md5` every time: in OpenSSL 3.x the latter re-fetches the digest from the
/// provider method store under a read lock, and with this function called once per row
/// all hashing threads serialize on it (with musl's `pthread_rwlock` on aarch64 this
/// doubled `cryptographic_hashes` times in CI, `__pthread_rwlock_tryrdlock` dominating
/// the profile).
static const EVP_MD_CTX_ptr ctx_template = []
{
EVP_MD_CTX_ptr new_ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free);
if (!new_ctx)
throw Exception(ErrorCodes::OPENSSL_ERROR, "EVP_MD_CTX_new failed: {}", getOpenSSLErrors());
if (EVP_DigestInit_ex(new_ctx.get(), EVP_md5(), nullptr) != 1)
throw Exception(ErrorCodes::OPENSSL_ERROR, "EVP_DigestInit_ex failed: {}", getOpenSSLErrors());
return new_ctx;
}();

thread_local EVP_MD_CTX_ptr ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free);

if (!ctx)
throw Exception(ErrorCodes::OPENSSL_ERROR, "EVP_MD_CTX_new failed: {}", getOpenSSLErrors());
{
ctx.reset(EVP_MD_CTX_new());
if (!ctx)
throw Exception(ErrorCodes::OPENSSL_ERROR, "EVP_MD_CTX_new failed: {}", getOpenSSLErrors());
}

if (EVP_DigestInit_ex(ctx.get(), EVP_md5(), nullptr) != 1)
throw Exception(ErrorCodes::OPENSSL_ERROR, "EVP_DigestInit_ex failed: {}", getOpenSSLErrors());
if (EVP_MD_CTX_copy_ex(ctx.get(), ctx_template.get()) != 1)
throw Exception(ErrorCodes::OPENSSL_ERROR, "EVP_MD_CTX_copy_ex failed: {}", getOpenSSLErrors());

if (EVP_DigestUpdate(ctx.get(), begin, size) != 1)
throw Exception(ErrorCodes::OPENSSL_ERROR, "EVP_DigestUpdate failed: {}", getOpenSSLErrors());
Expand Down
56 changes: 56 additions & 0 deletions tests/performance/encrypt_decrypt.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<test>
<!-- Non-empty plaintext encrypt/decrypt performance test.
Tests realistic workloads with constant and variable keys/IVs across ECB, CBC, CTR, and GCM modes.
Complements encrypt_decrypt_empty_string.xml which only tests the empty plaintext NO-OP path. -->

<substitutions>
<substitution>
<name>func</name>
<values>
<!-- Constant key + constant IV: encrypt -->
<value>encrypt('aes-128-ecb', materialize(plaintext), key16)</value>
<value>encrypt('aes-128-cbc', materialize(plaintext), key16, iv16)</value>
<value>encrypt('aes-128-ctr', materialize(plaintext), key16, iv16)</value>
<value>encrypt('aes-128-gcm', materialize(plaintext), key16, iv12)</value>

<!-- Constant key + constant IV: encrypt then decrypt -->
<value>decrypt('aes-128-ecb', encrypt('aes-128-ecb', materialize(plaintext), key16), key16)</value>
<value>decrypt('aes-128-cbc', encrypt('aes-128-cbc', materialize(plaintext), key16, iv16), key16, iv16)</value>
<value>decrypt('aes-128-gcm', encrypt('aes-128-gcm', materialize(plaintext), key16, iv12), key16, iv12)</value>

<!-- Variable key, constant IV: encrypt (key changes per row, full init per row) -->
<value>encrypt('aes-128-cbc', materialize(plaintext), randomString(16), iv16)</value>
<value>encrypt('aes-128-ecb', materialize(plaintext), randomString(16))</value>

<!-- Alternating keys: every row misses the key schedule cache -->
<value>encrypt('aes-128-cbc', materialize(plaintext), concat(repeat('k', 15), toString(number % 2)), iv16)</value>

<!-- Constant key, variable IV: encrypt (IV changes per row, only the IV is reset) -->
<value>encrypt('aes-128-cbc', materialize(plaintext), key16, randomString(16))</value>

<!-- Variable key / variable IV round trips (deterministic per-row values,
so decrypt sees the same key/IV as encrypt) -->
<value>decrypt('aes-128-cbc', encrypt('aes-128-cbc', materialize(plaintext), vkey16, iv16), vkey16, iv16)</value>
<value>decrypt('aes-128-ecb', encrypt('aes-128-ecb', materialize(plaintext), vkey16), vkey16)</value>
<value>decrypt('aes-128-cbc', encrypt('aes-128-cbc', materialize(plaintext), key16, viv16), key16, viv16)</value>
</values>
</substitution>
<substitution>
<name>table</name>
<values>
<value>numbers(3000000)</value>
</values>
</substitution>
<substitution>
<name>plaintext</name>
<values>
<value>toString(number)</value>
</values>
</substitution>
</substitutions>

<!-- allow OpenSSL-related code to load ciphers and warm-up -->
<fill_query>WITH {plaintext} as plaintext, repeat('k', 16) as key16, repeat('iv', 8) as iv16, substring(iv16, 1, 12) as iv12, substring(concat(toString(number), key16), 1, 16) as vkey16, substring(concat(toString(number), iv16), 1, 16) as viv16 SELECT count() FROM {table} WHERE NOT ignore({func}) LIMIT 1</fill_query>

<query>WITH {plaintext} as plaintext, repeat('k', 16) as key16, repeat('iv', 8) as iv16, substring(iv16, 1, 12) as iv12, substring(concat(toString(number), key16), 1, 16) as vkey16, substring(concat(toString(number), iv16), 1, 16) as viv16 SELECT count() FROM {table} WHERE NOT ignore({func})</query>
</test>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
aes-128-cbc 1 1 1
aes-128-cbc 1 1 1
aes-128-cbc 1 1 1
aes-128-ctr 1 1 1
aes-128-ctr 1 1 1
aes-128-ctr 1 1 1
aes-128-ofb 1 1 1
aes-128-ofb 1 1 1
aes-128-ofb 1 1 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
-- Tags: no-fasttest
-- Tag no-fasttest: Depends on OpenSSL

-- Encrypting/decrypting with a constant key (and absent or empty IV) must produce exactly
-- the same result as with a non-constant key, for every row. This guards against any
-- constant-key specialization diverging from the generic path -- e.g. by reusing a single
-- cipher context across rows and leaking the IV state of the previous row into the next.
-- The per-row (correct) path is forced by materializing the key into a non-constant column,
-- so every comparison below must be 1, independently of the data, for all stateful modes.
-- https://github.com/ClickHouse/ClickHouse/pull/99105

-- The input has a repeated row so that a leak of IV state across rows would change the
-- second occurrence and break the equality.

WITH unhex('00112233445566778899aabbccddeeff') AS key
SELECT
'aes-128-cbc' AS mode,
hex(encrypt('aes-128-cbc', p, key)) = hex(encrypt('aes-128-cbc', p, materialize(key))) AS encrypt_no_iv_matches,
hex(encrypt('aes-128-cbc', p, key, '')) = hex(encrypt('aes-128-cbc', p, materialize(key), '')) AS encrypt_empty_iv_matches,
decrypt('aes-128-cbc', encrypt('aes-128-cbc', p, key), key) = p AS roundtrip_no_iv_ok
FROM (SELECT arrayJoin(['the quick brown!', 'the quick brown!', 'lazy dog jumped!']) AS p)
ORDER BY p;

WITH unhex('00112233445566778899aabbccddeeff') AS key
SELECT
'aes-128-ctr' AS mode,
hex(encrypt('aes-128-ctr', p, key)) = hex(encrypt('aes-128-ctr', p, materialize(key))) AS encrypt_no_iv_matches,
hex(encrypt('aes-128-ctr', p, key, '')) = hex(encrypt('aes-128-ctr', p, materialize(key), '')) AS encrypt_empty_iv_matches,
decrypt('aes-128-ctr', encrypt('aes-128-ctr', p, key), key) = p AS roundtrip_no_iv_ok
FROM (SELECT arrayJoin(['the quick brown!', 'the quick brown!', 'lazy dog jumped!']) AS p)
ORDER BY p;

WITH unhex('00112233445566778899aabbccddeeff') AS key
SELECT
'aes-128-ofb' AS mode,
hex(encrypt('aes-128-ofb', p, key)) = hex(encrypt('aes-128-ofb', p, materialize(key))) AS encrypt_no_iv_matches,
hex(encrypt('aes-128-ofb', p, key, '')) = hex(encrypt('aes-128-ofb', p, materialize(key), '')) AS encrypt_empty_iv_matches,
decrypt('aes-128-ofb', encrypt('aes-128-ofb', p, key), key) = p AS roundtrip_no_iv_ok
FROM (SELECT arrayJoin(['the quick brown!', 'the quick brown!', 'lazy dog jumped!']) AS p)
ORDER BY p;
Loading
Loading