mirror of
https://github.com/launchbadge/sqlx.git
synced 2025-12-30 13:20:59 +00:00
wip(mysql): impl native auth scramble
This commit is contained in:
parent
7750168b80
commit
86576106e8
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -1729,6 +1729,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"memchr",
|
||||
"percent-encoding",
|
||||
"sha-1",
|
||||
"sqlx-core",
|
||||
"string",
|
||||
"url",
|
||||
|
||||
@ -3,7 +3,7 @@ use sqlx::prelude::*;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let _conn = <MySqlConnection>::connect("mysql://root:password@localhost:3307/main").await?;
|
||||
let _conn = <MySqlConnection>::connect("mysql://root:password@localhost:3307").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -37,3 +37,4 @@ bytes = "1.0"
|
||||
memchr = "2.3"
|
||||
bitflags = "1.2"
|
||||
string = { version = "0.2.1", default-features = false }
|
||||
sha-1 = "0.9.2"
|
||||
|
||||
13
sqlx-mysql/src/auth.rs
Normal file
13
sqlx-mysql/src/auth.rs
Normal file
@ -0,0 +1,13 @@
|
||||
pub(crate) mod native;
|
||||
// mod caching_sha2;
|
||||
// mod sha256;
|
||||
|
||||
// XOR(x, y)
|
||||
// If len(y) < len(x), wrap around inside y
|
||||
fn xor_eq(x: &mut [u8], y: &[u8]) {
|
||||
let y_len = y.len();
|
||||
|
||||
for i in 0..x.len() {
|
||||
x[i] ^= y[i % y_len];
|
||||
}
|
||||
}
|
||||
35
sqlx-mysql/src/auth/native.rs
Normal file
35
sqlx-mysql/src/auth/native.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use bytes::{buf::Chain, Bytes};
|
||||
use sha1::{Digest, Sha1};
|
||||
|
||||
use super::xor_eq;
|
||||
|
||||
// https://mariadb.com/kb/en/connection/#mysql_native_password-plugin
|
||||
// https://dev.mysql.com/doc/internals/en/secure-password-authentication.html
|
||||
|
||||
pub(crate) fn scramble(nonce: &Chain<Bytes, Bytes>, password: &str) -> Vec<u8> {
|
||||
// SHA1( password ) ^ SHA1( nonce + SHA1( SHA1( password ) ) )
|
||||
|
||||
let mut hasher = Sha1::new();
|
||||
|
||||
hasher.update(password);
|
||||
|
||||
// SHA1( password )
|
||||
let mut pw_sha1 = hasher.finalize_reset();
|
||||
|
||||
hasher.update(&pw_sha1);
|
||||
|
||||
// SHA1( SHA1( password ) )
|
||||
let pw_sha1_sha1 = hasher.finalize_reset();
|
||||
|
||||
// NOTE: use the first 20 bytes of the nonce, we MAY have gotten a nul terminator
|
||||
hasher.update(nonce.first_ref());
|
||||
hasher.update(&nonce.last_ref()[..20 - nonce.first_ref().len()]);
|
||||
hasher.update(&pw_sha1_sha1);
|
||||
|
||||
// SHA1( seed + SHA1( SHA1( password ) ) )
|
||||
let nonce_pw_sha1_sha1 = hasher.finalize();
|
||||
|
||||
xor_eq(&mut pw_sha1, &nonce_pw_sha1_sha1);
|
||||
|
||||
pw_sha1.to_vec()
|
||||
}
|
||||
@ -4,7 +4,7 @@ use sqlx_core::io::{Deserialize, Serialize};
|
||||
use sqlx_core::{AsyncRuntime, Error, Result, Runtime};
|
||||
|
||||
use crate::protocol::{Capabilities, ErrPacket, Handshake, HandshakeResponse, OkPacket};
|
||||
use crate::{MySqlConnectOptions, MySqlConnection, MySqlDatabaseError};
|
||||
use crate::{auth, MySqlConnectOptions, MySqlConnection, MySqlDatabaseError};
|
||||
|
||||
// https://dev.mysql.com/doc/internals/en/connection-phase.html
|
||||
|
||||
@ -18,22 +18,51 @@ use crate::{MySqlConnectOptions, MySqlConnection, MySqlDatabaseError};
|
||||
|
||||
fn make_auth_response(
|
||||
auth_plugin_name: Option<&str>,
|
||||
username: &str,
|
||||
username: Option<&str>,
|
||||
password: Option<&str>,
|
||||
nonce: &Chain<Bytes, Bytes>,
|
||||
) -> Vec<u8> {
|
||||
vec![]
|
||||
) -> Result<Option<Vec<u8>>> {
|
||||
match (auth_plugin_name, password) {
|
||||
// NOTE: for no authentication plugin, we assume mysql_native_password
|
||||
// this means we have no support for mysql_old_password (pre mysql 4)
|
||||
// if you need this, please open an issue
|
||||
(Some("mysql_native_password"), Some(password)) | (None, Some(password)) => {
|
||||
Ok(Some(auth::native::scramble(nonce, password)))
|
||||
}
|
||||
|
||||
(_, None) => Ok(None),
|
||||
|
||||
// an unsupported plugin error looks like this in the official client:
|
||||
// ERROR 2059 (HY000): Authentication plugin 'caching_sha2_password' cannot be loaded: /usr/local/mysql/lib/plugin/caching_sha2_password.so: cannot open shared object file: No such file or directory
|
||||
|
||||
// and renders like this in SQLx:
|
||||
// Error: 2059 (HY000): Authentication plugin 'caching_sha2_password' cannot be loaded
|
||||
(Some(plugin), _) => Err(Error::Connect(Box::new(MySqlDatabaseError(ErrPacket::new(
|
||||
2059,
|
||||
&format!("Authentication plugin '{}' cannot be loaded", plugin),
|
||||
))))),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_handshake_response<Rt: Runtime>(options: &MySqlConnectOptions<Rt>) -> HandshakeResponse<'_> {
|
||||
HandshakeResponse {
|
||||
auth_plugin_name: None,
|
||||
auth_response: None,
|
||||
fn make_handshake_response<'a, Rt: Runtime>(
|
||||
handshake: &'a Handshake,
|
||||
options: &'a MySqlConnectOptions<Rt>,
|
||||
) -> Result<HandshakeResponse<'a>> {
|
||||
let auth_response = make_auth_response(
|
||||
handshake.auth_plugin_name.as_deref(),
|
||||
options.get_username(),
|
||||
options.get_password(),
|
||||
&handshake.auth_plugin_data,
|
||||
)?;
|
||||
|
||||
Ok(HandshakeResponse {
|
||||
auth_plugin_name: handshake.auth_plugin_name.as_deref(),
|
||||
auth_response,
|
||||
charset: 45, // [utf8mb4]
|
||||
database: options.get_database(),
|
||||
max_packet_size: 1024,
|
||||
username: options.get_username(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
impl<Rt> MySqlConnection<Rt>
|
||||
@ -53,7 +82,7 @@ where
|
||||
let handshake = self_.read_packet_async().await?;
|
||||
self_.recv_handshake(&handshake);
|
||||
|
||||
self_.write_packet(make_handshake_response(options))?;
|
||||
self_.write_packet(make_handshake_response(&handshake, options)?)?;
|
||||
|
||||
self_.stream.flush_async().await?;
|
||||
|
||||
@ -87,7 +116,7 @@ where
|
||||
// https://dev.mysql.com/doc/internals/en/mysql-packet.html
|
||||
self.stream.read_async(4).await?;
|
||||
|
||||
let payload_len: usize = self.stream.get(0, 3).get_int_le(3) as usize;
|
||||
let payload_len: usize = self.stream.get(0, 3).get_uint_le(3) as usize;
|
||||
|
||||
// FIXME: handle split packets
|
||||
assert_ne!(payload_len, 0xFF_FF_FF);
|
||||
|
||||
@ -25,6 +25,7 @@ mod io;
|
||||
mod options;
|
||||
mod protocol;
|
||||
mod error;
|
||||
mod auth;
|
||||
|
||||
#[cfg(feature = "blocking")]
|
||||
mod blocking;
|
||||
|
||||
@ -18,6 +18,22 @@ pub(crate) struct ErrPacket {
|
||||
pub(crate) error_message: String<Bytes>,
|
||||
}
|
||||
|
||||
impl ErrPacket {
|
||||
pub(crate) fn new(code: u16, message: &str) -> Self {
|
||||
let message_bytes = Bytes::copy_from_slice(message.as_bytes());
|
||||
let state_bytes = Bytes::from_static(b"HY000");
|
||||
|
||||
// UNSAFE: the UTF-8 string is converted to bytes right above. The string crate has a
|
||||
// safe method for creation from Rust str but it pulls in an old version of Bytes
|
||||
#[allow(unsafe_code)]
|
||||
let (message, state) = unsafe {
|
||||
(String::from_utf8_unchecked(message_bytes), String::from_utf8_unchecked(state_bytes))
|
||||
};
|
||||
|
||||
Self { error_code: code, sql_state: Some(state), error_message: message }
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize<'_, Capabilities> for ErrPacket {
|
||||
fn deserialize_with(mut buf: Bytes, capabilities: Capabilities) -> Result<Self> {
|
||||
let tag = buf.get_u8();
|
||||
@ -57,7 +73,10 @@ mod tests {
|
||||
|
||||
assert_eq!(ok.sql_state, None);
|
||||
assert_eq!(ok.error_code, 1251);
|
||||
assert_eq!(&ok.error_message, "Client does not support authentication protocol requested by server; consider upgrading MySQL client");
|
||||
assert_eq!(
|
||||
&ok.error_message,
|
||||
"Client does not support authentication protocol requested by server; consider upgrading MySQL client"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -15,7 +15,7 @@ pub(crate) struct HandshakeResponse<'a> {
|
||||
pub(crate) charset: u8,
|
||||
pub(crate) username: Option<&'a str>,
|
||||
pub(crate) auth_plugin_name: Option<&'a str>,
|
||||
pub(crate) auth_response: Option<&'a [u8]>,
|
||||
pub(crate) auth_response: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl Serialize<'_, Capabilities> for HandshakeResponse<'_> {
|
||||
@ -29,7 +29,7 @@ impl Serialize<'_, Capabilities> for HandshakeResponse<'_> {
|
||||
|
||||
buf.write_maybe_str_nul(self.username);
|
||||
|
||||
let auth_response = self.auth_response.unwrap_or_default();
|
||||
let auth_response = self.auth_response.as_deref().unwrap_or_default();
|
||||
|
||||
if capabilities.contains(Capabilities::PLUGIN_AUTH_LENENC_DATA) {
|
||||
buf.write_bytes_lenenc(auth_response);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user