wip(mysql): impl native auth scramble

This commit is contained in:
Ryan Leckey 2021-01-03 18:23:51 -08:00
parent 7750168b80
commit 86576106e8
9 changed files with 114 additions and 15 deletions

1
Cargo.lock generated
View File

@ -1729,6 +1729,7 @@ dependencies = [
"futures-util",
"memchr",
"percent-encoding",
"sha-1",
"sqlx-core",
"string",
"url",

View File

@ -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(())
}

View File

@ -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
View 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];
}
}

View 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()
}

View File

@ -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);

View File

@ -25,6 +25,7 @@ mod io;
mod options;
mod protocol;
mod error;
mod auth;
#[cfg(feature = "blocking")]
mod blocking;

View File

@ -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]

View File

@ -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);