Skip to content

Commit 5bd5531

Browse files
fix(mysql): repair caching_sha2_password fast-auth path (#4245)
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
1 parent bfb8ff6 commit 5bd5531

1 file changed

Lines changed: 43 additions & 7 deletions

File tree

sqlx-mysql/src/connection/auth.rs

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@ impl AuthPlugin {
4444
match self {
4545
AuthPlugin::CachingSha2Password if packet[0] == 0x01 => {
4646
match packet[1] {
47-
// AUTH_OK
48-
0x03 => Ok(true),
47+
// fast_auth_success — the server still sends a trailing
48+
// OK_Packet, so yield back to the handshake loop and let
49+
// it consume the OK on the next iteration.
50+
0x03 => Ok(false),
4951

50-
// AUTH_CONTINUE
52+
// perform_full_authentication
5153
0x04 => {
5254
let payload = encrypt_rsa(stream, 0x02, password, nonce).await?;
5355

@@ -58,7 +60,7 @@ impl AuthPlugin {
5860
}
5961

6062
v => {
61-
Err(err_protocol!("unexpected result from fast authentication 0x{:x} when expecting 0x03 (AUTH_OK) or 0x04 (AUTH_CONTINUE)", v))
63+
Err(err_protocol!("unexpected result from fast authentication 0x{:x} when expecting 0x03 (fast_auth_success) or 0x04 (perform_full_authentication)", v))
6264
}
6365
}
6466
}
@@ -104,8 +106,9 @@ fn scramble_sha256(
104106
password: &str,
105107
nonce: &Chain<Bytes, Bytes>,
106108
) -> GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize> {
107-
// XOR(SHA256(password), SHA256(seed, SHA256(SHA256(password))))
108-
// https://mariadb.com/kb/en/caching_sha2_password-authentication-plugin/#sha-2-encrypted-password
109+
// XOR(SHA256(password), SHA256(SHA256(SHA256(password)), seed))
110+
// Order matches the server-side verification in MySQL's sha2_password
111+
// (generate_sha2_scramble): stage2 digest first, then the nonce.
109112
let mut ctx = Sha256::new();
110113

111114
ctx.update(password);
@@ -116,9 +119,9 @@ fn scramble_sha256(
116119

117120
let pw_hash_hash = ctx.finalize_reset();
118121

122+
ctx.update(pw_hash_hash);
119123
ctx.update(nonce.first_ref());
120124
ctx.update(nonce.last_ref());
121-
ctx.update(pw_hash_hash);
122125

123126
let pw_seed_hash_hash = ctx.finalize();
124127

@@ -216,3 +219,36 @@ mod rsa_backend {
216219
))
217220
}
218221
}
222+
223+
#[cfg(test)]
224+
mod tests {
225+
use super::*;
226+
use bytes::Buf;
227+
use sha2::{Digest, Sha256};
228+
229+
// Regression test for https://github.com/launchbadge/sqlx/issues/4244:
230+
// caching_sha2_password fast-auth requires the client scramble to be
231+
// invertible by the server as XOR(scramble, SHA256(stage2 || nonce)) == stage1,
232+
// where stage1 = SHA256(password) and stage2 = SHA256(stage1).
233+
#[test]
234+
fn scramble_sha256_is_invertible_by_server() {
235+
let password = "my_pwd";
236+
let nonce_a = Bytes::from_static(b"0123456789");
237+
let nonce_b = Bytes::from_static(&[0xAB; 10]);
238+
let nonce = nonce_a.clone().chain(nonce_b.clone());
239+
240+
let mut scramble = scramble_sha256(password, &nonce);
241+
242+
let stage1 = Sha256::digest(password.as_bytes());
243+
let stage2 = Sha256::digest(stage1);
244+
245+
let mut h = Sha256::new();
246+
h.update(stage2);
247+
h.update(&nonce_a);
248+
h.update(&nonce_b);
249+
let xor_pad = h.finalize();
250+
251+
xor_eq(&mut scramble, &xor_pad);
252+
assert_eq!(&scramble[..], &stage1[..]);
253+
}
254+
}

0 commit comments

Comments
 (0)