From 86576106e8ba7fa625be1a110c72b5da708d7c05 Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Sun, 3 Jan 2021 18:23:51 -0800 Subject: [PATCH] wip(mysql): impl native auth scramble --- Cargo.lock | 1 + examples/quickstart/src/main.rs | 2 +- sqlx-mysql/Cargo.toml | 1 + sqlx-mysql/src/auth.rs | 13 +++++ sqlx-mysql/src/auth/native.rs | 35 +++++++++++++ sqlx-mysql/src/connection/establish.rs | 51 +++++++++++++++---- sqlx-mysql/src/lib.rs | 1 + sqlx-mysql/src/protocol/err.rs | 21 +++++++- sqlx-mysql/src/protocol/handshake_response.rs | 4 +- 9 files changed, 114 insertions(+), 15 deletions(-) create mode 100644 sqlx-mysql/src/auth.rs create mode 100644 sqlx-mysql/src/auth/native.rs diff --git a/Cargo.lock b/Cargo.lock index 212bf68e..ba15f716 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1729,6 +1729,7 @@ dependencies = [ "futures-util", "memchr", "percent-encoding", + "sha-1", "sqlx-core", "string", "url", diff --git a/examples/quickstart/src/main.rs b/examples/quickstart/src/main.rs index fe4e3798..481d7111 100644 --- a/examples/quickstart/src/main.rs +++ b/examples/quickstart/src/main.rs @@ -3,7 +3,7 @@ use sqlx::prelude::*; #[tokio::main] async fn main() -> anyhow::Result<()> { - let _conn = ::connect("mysql://root:password@localhost:3307/main").await?; + let _conn = ::connect("mysql://root:password@localhost:3307").await?; Ok(()) } diff --git a/sqlx-mysql/Cargo.toml b/sqlx-mysql/Cargo.toml index 24709462..28c94e3b 100644 --- a/sqlx-mysql/Cargo.toml +++ b/sqlx-mysql/Cargo.toml @@ -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" diff --git a/sqlx-mysql/src/auth.rs b/sqlx-mysql/src/auth.rs new file mode 100644 index 00000000..7ddbbc6d --- /dev/null +++ b/sqlx-mysql/src/auth.rs @@ -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]; + } +} diff --git a/sqlx-mysql/src/auth/native.rs b/sqlx-mysql/src/auth/native.rs new file mode 100644 index 00000000..220d8aae --- /dev/null +++ b/sqlx-mysql/src/auth/native.rs @@ -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, password: &str) -> Vec { + // 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() +} diff --git a/sqlx-mysql/src/connection/establish.rs b/sqlx-mysql/src/connection/establish.rs index e7f18a7e..2adf4986 100644 --- a/sqlx-mysql/src/connection/establish.rs +++ b/sqlx-mysql/src/connection/establish.rs @@ -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, -) -> Vec { - vec![] +) -> Result>> { + 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(options: &MySqlConnectOptions) -> HandshakeResponse<'_> { - HandshakeResponse { - auth_plugin_name: None, - auth_response: None, +fn make_handshake_response<'a, Rt: Runtime>( + handshake: &'a Handshake, + options: &'a MySqlConnectOptions, +) -> Result> { + 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 MySqlConnection @@ -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); diff --git a/sqlx-mysql/src/lib.rs b/sqlx-mysql/src/lib.rs index f35bf755..a0f0fcfd 100644 --- a/sqlx-mysql/src/lib.rs +++ b/sqlx-mysql/src/lib.rs @@ -25,6 +25,7 @@ mod io; mod options; mod protocol; mod error; +mod auth; #[cfg(feature = "blocking")] mod blocking; diff --git a/sqlx-mysql/src/protocol/err.rs b/sqlx-mysql/src/protocol/err.rs index b9fe9f37..bbf70e40 100644 --- a/sqlx-mysql/src/protocol/err.rs +++ b/sqlx-mysql/src/protocol/err.rs @@ -18,6 +18,22 @@ pub(crate) struct ErrPacket { pub(crate) error_message: String, } +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 { 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] diff --git a/sqlx-mysql/src/protocol/handshake_response.rs b/sqlx-mysql/src/protocol/handshake_response.rs index 9b8ac498..a7763ed7 100644 --- a/sqlx-mysql/src/protocol/handshake_response.rs +++ b/sqlx-mysql/src/protocol/handshake_response.rs @@ -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>, } 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);