Skip to content

Commit 6a82609

Browse files
authored
caching_sha2_password can now be used without TLS
Fixed a problem that caused pfr_by_name<T> to fail compilation when T has no fields under older clang/libc++ versions. close #313 close #483
1 parent 90405e7 commit 6a82609

34 files changed

+1732
-458
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ target_link_libraries(
3636
Boost::charconv
3737
Boost::compat
3838
Boost::config
39+
Boost::container
3940
Boost::core
4041
Boost::describe
4142
Boost::endian

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ And with the following databases:
7474
of executing a prepared statement is sent in binary format rather than in text.
7575
- Stored procedures.
7676
- Authentication methods (authentication plugins): mysql_native_password and
77-
caching_sha2_password. These are the default methods in MySQL 5 and MySQL 8,
77+
caching_sha2_password. These are the default methods in MySQL 5/MariaDB and MySQL 8,
7878
respectively.
7979
- Encrypted connections (TLS).
8080
- TCP and UNIX socket transports.

build.jam

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ constant boost_dependencies :
1111
/boost/charconv//boost_charconv
1212
/boost/compat//boost_compat
1313
/boost/config//boost_config
14+
/boost/container//boost_container
1415
/boost/core//boost_core
1516
/boost/describe//boost_describe
1617
/boost/endian//boost_endian

doc/qbk/05_connection_establishment.qbk

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@ This library implements the two most common authentication plugins:
4141
connections. It sends the password hashed, salted by a nonce.
4242
* [mysqllink caching-sha2-pluggable-authentication.html
4343
`caching_sha2_password`]. This is the default plugin for
44-
MySQL 8.0+. It can only be used over secure transports,
45-
like TCP with TLS or UNIX sockets.
44+
MySQL 8.0+. This plugin used to require using TLS.
45+
Since Boost 1.89, this is no longer the case, and it can be used
46+
with plaintext connections, too.
4647

4748

4849
Multi-factor authentication is not yet supported.

include/boost/mysql/client_errc.hpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ enum class client_errc : int
5050
/// The user employs an authentication plugin not known to this library.
5151
unknown_auth_plugin,
5252

53-
/// The authentication plugin requires the connection to use SSL.
53+
/**
54+
* \brief (Legacy) The authentication plugin requires the connection to use SSL.
55+
* This code is no longer used, since all supported plugins support plaintext connections.
56+
*/
5457
auth_plugin_requires_ssl,
5558

5659
/**
@@ -169,6 +172,9 @@ enum class client_errc : int
169172
* (protocol violation).
170173
*/
171174
bad_handshake_packet_type,
175+
176+
/// An OpenSSL function failed and did not provide any extra diagnostics.
177+
unknown_openssl_error,
172178
};
173179

174180
BOOST_MYSQL_DECL

include/boost/mysql/impl/error_categories.ipp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ inline const char* error_to_string(client_errc error)
103103
case client_errc::bad_handshake_packet_type:
104104
return "During handshake, the server sent a packet type that is not allowed in the current state "
105105
"(protocol violation).";
106+
case client_errc::unknown_openssl_error:
107+
return "An OpenSSL function failed and did not provide any extra diagnostics.";
106108
default: return "<unknown MySQL client error>";
107109
}
108110
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//
2+
// Copyright (c) 2019-2025 Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
3+
//
4+
// Distributed under the Boost Software License, Version 1.0. (See accompanying
5+
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6+
//
7+
8+
#ifndef BOOST_MYSQL_IMPL_INTERNAL_SANSIO_AUTH_PLUGIN_COMMON_HPP
9+
#define BOOST_MYSQL_IMPL_INTERNAL_SANSIO_AUTH_PLUGIN_COMMON_HPP
10+
11+
#include <boost/config.hpp>
12+
13+
#include <cstddef>
14+
15+
namespace boost {
16+
namespace mysql {
17+
namespace detail {
18+
19+
// All scrambles in all the plugins we know have this size
20+
BOOST_INLINE_CONSTEXPR std::size_t scramble_size = 20u;
21+
22+
// Hashed passwords vary in size, but they all fit in a buffer like this
23+
BOOST_INLINE_CONSTEXPR std::size_t max_hash_size = 32u;
24+
25+
} // namespace detail
26+
} // namespace mysql
27+
} // namespace boost
28+
29+
#endif

include/boost/mysql/impl/internal/sansio/caching_sha2_password.hpp

Lines changed: 65 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,18 @@
1818
#include <boost/mysql/impl/internal/protocol/impl/protocol_types.hpp>
1919
#include <boost/mysql/impl/internal/protocol/impl/serialization_context.hpp>
2020
#include <boost/mysql/impl/internal/protocol/static_buffer.hpp>
21+
#include <boost/mysql/impl/internal/sansio/auth_plugin_common.hpp>
2122
#include <boost/mysql/impl/internal/sansio/connection_state_data.hpp>
23+
#include <boost/mysql/impl/internal/sansio/csha2p_encrypt_password.hpp>
2224

25+
#include <boost/asio/ssl/error.hpp>
26+
#include <boost/container/small_vector.hpp>
2327
#include <boost/core/span.hpp>
2428
#include <boost/system/result.hpp>
29+
#include <boost/system/system_category.hpp>
2530

2631
#include <array>
32+
#include <cstddef>
2733
#include <cstdint>
2834
#include <openssl/sha.h>
2935

@@ -35,59 +41,53 @@ namespace mysql {
3541
namespace detail {
3642

3743
// Constants
38-
BOOST_INLINE_CONSTEXPR std::size_t csha2p_challenge_length = 20;
39-
BOOST_INLINE_CONSTEXPR std::size_t csha2p_response_length = 32;
44+
BOOST_INLINE_CONSTEXPR std::size_t csha2p_hash_size = 32;
4045
BOOST_INLINE_CONSTEXPR const char* csha2p_plugin_name = "caching_sha2_password";
46+
static_assert(csha2p_hash_size <= max_hash_size, "");
47+
static_assert(csha2p_hash_size == SHA256_DIGEST_LENGTH, "Buffer size mismatch");
4148

4249
inline void csha2p_hash_password_impl(
4350
string_view password,
44-
span<const std::uint8_t, csha2p_challenge_length> challenge,
45-
span<std::uint8_t, csha2p_response_length> output
51+
span<const std::uint8_t, scramble_size> scramble,
52+
span<std::uint8_t, csha2p_hash_size> output
4653
)
4754
{
48-
static_assert(csha2p_response_length == SHA256_DIGEST_LENGTH, "Buffer size mismatch");
49-
50-
// SHA(SHA(password_sha) concat challenge) XOR password_sha
55+
// SHA(SHA(password_sha) concat scramble) XOR password_sha
5156
// hash1 = SHA(pass)
52-
std::array<std::uint8_t, csha2p_response_length> password_sha;
57+
std::array<std::uint8_t, csha2p_hash_size> password_sha;
5358
SHA256(reinterpret_cast<const unsigned char*>(password.data()), password.size(), password_sha.data());
5459

55-
// SHA(password_sha) concat challenge = buffer
56-
std::array<std::uint8_t, csha2p_response_length + csha2p_challenge_length> buffer;
60+
// SHA(password_sha) concat scramble = buffer
61+
std::array<std::uint8_t, csha2p_hash_size + scramble_size> buffer;
5762
SHA256(password_sha.data(), password_sha.size(), buffer.data());
58-
std::memcpy(buffer.data() + csha2p_response_length, challenge.data(), csha2p_challenge_length);
63+
std::memcpy(buffer.data() + csha2p_hash_size, scramble.data(), scramble.size());
5964

60-
// SHA(SHA(password_sha) concat challenge) = SHA(buffer) = salted_password
61-
std::array<std::uint8_t, csha2p_response_length> salted_password;
65+
// SHA(SHA(password_sha) concat scramble) = SHA(buffer) = salted_password
66+
std::array<std::uint8_t, csha2p_hash_size> salted_password;
6267
SHA256(buffer.data(), buffer.size(), salted_password.data());
6368

6469
// salted_password XOR password_sha
65-
for (unsigned i = 0; i < csha2p_response_length; ++i)
70+
for (unsigned i = 0; i < csha2p_hash_size; ++i)
6671
{
6772
output[i] = salted_password[i] ^ password_sha[i];
6873
}
6974
}
7075

71-
inline system::result<static_buffer<32>> csha2p_hash_password(
76+
inline static_buffer<max_hash_size> csha2p_hash_password(
7277
string_view password,
73-
span<const std::uint8_t> challenge
78+
span<const std::uint8_t, scramble_size> scramble
7479
)
7580
{
76-
// If the challenge doesn't match the expected size,
77-
// something wrong is going on and we should fail
78-
if (challenge.size() != csha2p_challenge_length)
79-
return client_errc::protocol_value_error;
80-
8181
// Empty passwords are not hashed
8282
if (password.empty())
8383
return {};
8484

8585
// Run the algorithm
86-
static_buffer<32> res(csha2p_response_length);
86+
static_buffer<max_hash_size> res(csha2p_hash_size);
8787
csha2p_hash_password_impl(
8888
password,
89-
span<const std::uint8_t, csha2p_challenge_length>(challenge),
90-
span<std::uint8_t, csha2p_response_length>(res.data(), csha2p_response_length)
89+
scramble,
90+
span<std::uint8_t, csha2p_hash_size>(res.data(), csha2p_hash_size)
9191
);
9292
return res;
9393
}
@@ -106,13 +106,32 @@ class csha2p_algo
106106
return server_data.size() == 1u && server_data[0] == 3;
107107
}
108108

109+
static next_action encrypt_password(
110+
connection_state_data& st,
111+
std::uint8_t& seqnum,
112+
string_view password,
113+
span<const std::uint8_t, scramble_size> scramble,
114+
span<const std::uint8_t> server_key
115+
)
116+
{
117+
container::small_vector<std::uint8_t, 512> buff;
118+
auto ec = csha2p_encrypt_password(password, scramble, server_key, buff, asio::error::ssl_category);
119+
if (ec)
120+
return ec;
121+
return st.write(
122+
string_eof{string_view(reinterpret_cast<const char*>(buff.data()), buff.size())},
123+
seqnum
124+
);
125+
}
126+
109127
public:
110128
csha2p_algo() = default;
111129

112130
next_action resume(
113131
connection_state_data& st,
114132
span<const std::uint8_t> server_data,
115133
string_view password,
134+
span<const std::uint8_t, scramble_size> scramble,
116135
bool secure_channel,
117136
std::uint8_t& seqnum
118137
)
@@ -124,19 +143,34 @@ class csha2p_algo
124143
// or told us to read again because an OK packet or error packet is coming.
125144
if (is_perform_full_auth(server_data))
126145
{
127-
// At this point, we don't support full auth over insecure channels
128-
if (!secure_channel)
146+
if (secure_channel)
129147
{
130-
return make_error_code(client_errc::auth_plugin_requires_ssl);
131-
}
148+
// We should send a packet with just the password, as a NULL-terminated string
149+
BOOST_MYSQL_YIELD(resume_point_, 1, st.write(string_null{password}, seqnum))
132150

133-
// We should send a packet with just the password, as a NULL-terminated string
134-
BOOST_MYSQL_YIELD(resume_point_, 1, st.write(string_null{password}, seqnum))
151+
// The server shouldn't send us any more packets
152+
return error_code(client_errc::bad_handshake_packet_type);
153+
}
154+
else
155+
{
156+
// Request the server's public key
157+
BOOST_MYSQL_YIELD(resume_point_, 2, st.write(int1{2}, seqnum))
158+
159+
// Encrypt the password with the key we were given
160+
BOOST_MYSQL_YIELD(
161+
resume_point_,
162+
3,
163+
encrypt_password(st, seqnum, password, scramble, server_data)
164+
)
165+
166+
// The server shouldn't send us any more packets
167+
return error_code(client_errc::bad_handshake_packet_type);
168+
}
135169
}
136170
else if (is_fast_auth_ok(server_data))
137171
{
138172
// We should wait for the server to send an OK or an error
139-
BOOST_MYSQL_YIELD(resume_point_, 2, st.read(seqnum))
173+
BOOST_MYSQL_YIELD(resume_point_, 4, st.read(seqnum))
140174
}
141175
else
142176
{

0 commit comments

Comments
 (0)