Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-08-17 08:51:24

0001 //
0002 // Copyright (c) 2019-2025 Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
0003 //
0004 // Distributed under the Boost Software License, Version 1.0. (See accompanying
0005 // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
0006 //
0007 
0008 #ifndef BOOST_MYSQL_IMPL_INTERNAL_SANSIO_CSHA2P_ENCRYPT_PASSWORD_HPP
0009 #define BOOST_MYSQL_IMPL_INTERNAL_SANSIO_CSHA2P_ENCRYPT_PASSWORD_HPP
0010 
0011 // Having this in a separate file allows us to mock the OpenSSL API in the tests
0012 
0013 #include <boost/mysql/client_errc.hpp>
0014 #include <boost/mysql/error_code.hpp>
0015 #include <boost/mysql/string_view.hpp>
0016 
0017 #include <boost/mysql/impl/internal/sansio/auth_plugin_common.hpp>
0018 
0019 #include <boost/assert/source_location.hpp>
0020 #include <boost/container/small_vector.hpp>
0021 #include <boost/core/span.hpp>
0022 #include <boost/system/error_category.hpp>
0023 #include <boost/system/system_category.hpp>
0024 
0025 #include <cstdint>
0026 #include <memory>
0027 #include <openssl/bio.h>
0028 #include <openssl/err.h>
0029 #include <openssl/evp.h>
0030 #include <openssl/pem.h>
0031 #include <openssl/rsa.h>
0032 
0033 namespace boost {
0034 namespace mysql {
0035 namespace detail {
0036 
0037 // The OpenSSL category is passed as parameter to avoid including asio/ssl headers here.
0038 // Doing so would make mocking OpenSSL more difficult (more functions used)
0039 inline error_code translate_openssl_error(
0040     unsigned long code,
0041     const source_location* loc,
0042     const system::error_category& openssl_category
0043 )
0044 {
0045     // If ERR_SYSTEM_ERROR is true, the error code is a system error.
0046     // This function only exists since OpenSSL 3
0047 #if OPENSSL_VERSION_NUMBER >= 0x30000000L
0048     if (ERR_SYSTEM_ERROR(code))
0049     {
0050         return error_code(ERR_GET_REASON(code), system::system_category(), loc);
0051     }
0052 #endif
0053 
0054     // In OpenSSL < 3, error codes > 0x80000000 are reserved for the user,
0055     // so it's unlikely that we will encounter these here. Overflow here
0056     // is implementation-defined behavior (and not UB), so we're fine.
0057     // This is what Asio does, anyway.
0058     int int_code = static_cast<int>(code);
0059 
0060     // An error code of zero would mean success, while this function is always
0061     // called because an OpenSSL primitive failed. It might indicate that OpenSSL
0062     // did not provide any extra error information. But it should still be an error
0063     if (int_code == 0)
0064         return error_code(client_errc::unknown_openssl_error, loc);
0065     else
0066         return error_code(int_code, openssl_category, loc);
0067 }
0068 
0069 inline container::small_vector<std::uint8_t, 512> csha2p_salt_password(
0070     string_view password,
0071     span<const std::uint8_t, scramble_size> scramble
0072 )
0073 {
0074     // Salt the password, as a NULL-terminated string
0075     container::small_vector<std::uint8_t, 512> res(password.size() + 1u, 0);
0076     for (std::size_t i = 0; i < password.size(); ++i)
0077         res[i] = password[i] ^ scramble[i % scramble.size()];
0078 
0079     // Add the NULL terminator. It should be salted, too. Since 0 ^ U = U,
0080     // the byte should be the scramble at the position we're in
0081     res[password.size()] = scramble[password.size() % scramble.size()];
0082 
0083     return res;
0084 }
0085 
0086 inline error_code csha2p_encrypt_password(
0087     string_view password,
0088     span<const std::uint8_t, scramble_size> scramble,
0089     span<const std::uint8_t> server_key,
0090     container::small_vector<std::uint8_t, 512>& output,
0091     const system::error_category& openssl_category
0092 )
0093 {
0094     // RAII helpers
0095     struct bio_deleter
0096     {
0097         void operator()(BIO* bio) const noexcept { BIO_free(bio); }
0098     };
0099     using unique_bio = std::unique_ptr<BIO, bio_deleter>;
0100 
0101     struct evp_pkey_deleter
0102     {
0103         void operator()(EVP_PKEY* pkey) const noexcept { EVP_PKEY_free(pkey); }
0104     };
0105     using unique_evp_pkey = std::unique_ptr<EVP_PKEY, evp_pkey_deleter>;
0106 
0107     struct evp_pkey_ctx_deleter
0108     {
0109         void operator()(EVP_PKEY_CTX* ctx) const noexcept { EVP_PKEY_CTX_free(ctx); }
0110     };
0111     using unique_evp_pkey_ctx = std::unique_ptr<EVP_PKEY_CTX, evp_pkey_ctx_deleter>;
0112 
0113     // Apply a sanity check to the key buffer size
0114     constexpr std::size_t max_key_buffer_size = 1024u * 1024u;  // 1MB
0115     if (server_key.size() > max_key_buffer_size)
0116     {
0117         static constexpr auto loc = BOOST_CURRENT_LOCATION;
0118         return error_code(client_errc::protocol_value_error, &loc);
0119     }
0120 
0121     // Try to parse the private key
0122     unique_bio bio{BIO_new_mem_buf(server_key.data(), static_cast<int>(server_key.size()))};
0123     if (!bio)
0124     {
0125         static constexpr auto loc = BOOST_CURRENT_LOCATION;
0126         return translate_openssl_error(ERR_get_error(), &loc, openssl_category);
0127     }
0128     unique_evp_pkey key(PEM_read_bio_PUBKEY(bio.get(), nullptr, nullptr, nullptr));
0129     if (!key)
0130     {
0131         static constexpr auto loc = BOOST_CURRENT_LOCATION;
0132         return translate_openssl_error(ERR_get_error(), &loc, openssl_category);
0133     }
0134 
0135     // Salt the password
0136     auto salted_password = csha2p_salt_password(password, scramble);
0137 
0138     // Set up the encryption context
0139     unique_evp_pkey_ctx ctx(EVP_PKEY_CTX_new(key.get(), nullptr));
0140     if (!ctx)
0141     {
0142         static constexpr auto loc = BOOST_CURRENT_LOCATION;
0143         return translate_openssl_error(ERR_get_error(), &loc, openssl_category);
0144     }
0145     if (EVP_PKEY_encrypt_init(ctx.get()) <= 0)
0146     {
0147         static constexpr auto loc = BOOST_CURRENT_LOCATION;
0148         return translate_openssl_error(ERR_get_error(), &loc, openssl_category);
0149     }
0150     int rsa_pad_res = EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_OAEP_PADDING);
0151     if (rsa_pad_res <= 0)
0152     {
0153         // If the server passed us a key type that does not support encryption,
0154         // OpenSSL returns -2 and does not add an error to the stack (ERR_get_error returns 0).
0155         // This shouldn't happen with real servers, so we re-use an existing error code and set
0156         // the source location to allow diagnosis
0157         static constexpr auto loc = BOOST_CURRENT_LOCATION;
0158         if (rsa_pad_res == -2)
0159             return error_code(client_errc::protocol_value_error, &loc);
0160         else
0161             return translate_openssl_error(ERR_get_error(), &loc, openssl_category);
0162     }
0163 
0164     // Allocate a buffer for encryption
0165     int max_size = EVP_PKEY_size(key.get());
0166     if (max_size <= 0)
0167     {
0168         static constexpr auto loc = BOOST_CURRENT_LOCATION;
0169         return translate_openssl_error(ERR_get_error(), &loc, openssl_category);
0170     }
0171     output.resize(max_size);
0172 
0173     // Encrypt
0174     std::size_t actual_size = static_cast<std::size_t>(max_size);
0175     if (EVP_PKEY_encrypt(
0176             ctx.get(),
0177             output.data(),
0178             &actual_size,
0179             salted_password.data(),
0180             salted_password.size()
0181         ) <= 0)
0182     {
0183         static constexpr auto loc = BOOST_CURRENT_LOCATION;
0184         return translate_openssl_error(ERR_get_error(), &loc, openssl_category);
0185     }
0186 
0187     // Adjust size
0188     BOOST_ASSERT(actual_size <= output.size());
0189     output.resize(actual_size);
0190 
0191     // Done
0192     return error_code();
0193 }
0194 
0195 }  // namespace detail
0196 }  // namespace mysql
0197 }  // namespace boost
0198 
0199 #endif