From bb4a20f4763ed83c147216f5c7c2f2d4abb66b36 Mon Sep 17 00:00:00 2001 From: Marcelo Altmann Date: Wed, 22 Apr 2026 10:36:50 -0300 Subject: [PATCH] fix(mysql): repair caching_sha2_password fast-auth path The client-side scramble mixed the SHA-256 inputs in the wrong order, so no spec-compliant MySQL server could validate it. Every connection fell through to perform_full_authentication and the plugin's cache was never exercised. Two changes: 1. scramble_sha256 now hashes as SHA256(SHA256(SHA256(pw)) || nonce) to match the server's generate_sha2_scramble. Adds a unit test that simulates the server's XOR verification. 2. handle(..) returned true on fast_auth_success (0x01 0x03) without consuming the trailing OK_Packet, which then corrupted the next read. This was latent because 0x03 was never reached. It now yields back to the handshake loop so the OK is consumed by the existing 0x00 branch. fixes #4244 --- sqlx-mysql/src/connection/auth.rs | 50 ++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/sqlx-mysql/src/connection/auth.rs b/sqlx-mysql/src/connection/auth.rs index 0c6a4bf997..cd6d9324c3 100644 --- a/sqlx-mysql/src/connection/auth.rs +++ b/sqlx-mysql/src/connection/auth.rs @@ -44,10 +44,12 @@ impl AuthPlugin { match self { AuthPlugin::CachingSha2Password if packet[0] == 0x01 => { match packet[1] { - // AUTH_OK - 0x03 => Ok(true), + // fast_auth_success — the server still sends a trailing + // OK_Packet, so yield back to the handshake loop and let + // it consume the OK on the next iteration. + 0x03 => Ok(false), - // AUTH_CONTINUE + // perform_full_authentication 0x04 => { let payload = encrypt_rsa(stream, 0x02, password, nonce).await?; @@ -58,7 +60,7 @@ impl AuthPlugin { } v => { - Err(err_protocol!("unexpected result from fast authentication 0x{:x} when expecting 0x03 (AUTH_OK) or 0x04 (AUTH_CONTINUE)", v)) + Err(err_protocol!("unexpected result from fast authentication 0x{:x} when expecting 0x03 (fast_auth_success) or 0x04 (perform_full_authentication)", v)) } } } @@ -104,8 +106,9 @@ fn scramble_sha256( password: &str, nonce: &Chain, ) -> GenericArray::OutputSize> { - // XOR(SHA256(password), SHA256(seed, SHA256(SHA256(password)))) - // https://mariadb.com/kb/en/caching_sha2_password-authentication-plugin/#sha-2-encrypted-password + // XOR(SHA256(password), SHA256(SHA256(SHA256(password)), seed)) + // Order matches the server-side verification in MySQL's sha2_password + // (generate_sha2_scramble): stage2 digest first, then the nonce. let mut ctx = Sha256::new(); ctx.update(password); @@ -116,9 +119,9 @@ fn scramble_sha256( let pw_hash_hash = ctx.finalize_reset(); + ctx.update(pw_hash_hash); ctx.update(nonce.first_ref()); ctx.update(nonce.last_ref()); - ctx.update(pw_hash_hash); let pw_seed_hash_hash = ctx.finalize(); @@ -216,3 +219,36 @@ mod rsa_backend { )) } } + +#[cfg(test)] +mod tests { + use super::*; + use bytes::Buf; + use sha2::{Digest, Sha256}; + + // Regression test for https://github.com/launchbadge/sqlx/issues/4244: + // caching_sha2_password fast-auth requires the client scramble to be + // invertible by the server as XOR(scramble, SHA256(stage2 || nonce)) == stage1, + // where stage1 = SHA256(password) and stage2 = SHA256(stage1). + #[test] + fn scramble_sha256_is_invertible_by_server() { + let password = "my_pwd"; + let nonce_a = Bytes::from_static(b"0123456789"); + let nonce_b = Bytes::from_static(&[0xAB; 10]); + let nonce = nonce_a.clone().chain(nonce_b.clone()); + + let mut scramble = scramble_sha256(password, &nonce); + + let stage1 = Sha256::digest(password.as_bytes()); + let stage2 = Sha256::digest(stage1); + + let mut h = Sha256::new(); + h.update(stage2); + h.update(&nonce_a); + h.update(&nonce_b); + let xor_pad = h.finalize(); + + xor_eq(&mut scramble, &xor_pad); + assert_eq!(&scramble[..], &stage1[..]); + } +}