1use sha1::Sha1;
24use sha2::{Digest, Sha256};
25
26use rand::rand_core::UnwrapErr;
27use rand::rngs::SysRng;
28
29use rsa::RsaPublicKey;
30use rsa::pkcs1::DecodeRsaPublicKey;
31use rsa::pkcs8::DecodePublicKey;
32
33pub mod plugins {
35 pub const MYSQL_NATIVE_PASSWORD: &str = "mysql_native_password";
37 pub const CACHING_SHA2_PASSWORD: &str = "caching_sha2_password";
39 pub const SHA256_PASSWORD: &str = "sha256_password";
41 pub const MYSQL_CLEAR_PASSWORD: &str = "mysql_clear_password";
43}
44
45pub mod caching_sha2 {
47 pub const REQUEST_PUBLIC_KEY: u8 = 0x02;
49 pub const FAST_AUTH_SUCCESS: u8 = 0x03;
51 pub const PERFORM_FULL_AUTH: u8 = 0x04;
53}
54
55pub fn mysql_native_password(password: &str, auth_data: &[u8]) -> Vec<u8> {
66 if password.is_empty() {
67 return vec![];
68 }
69
70 let seed = if auth_data.len() > 20 {
72 &auth_data[..20]
73 } else {
74 auth_data
75 };
76
77 let mut hasher = Sha1::new();
79 hasher.update(password.as_bytes());
80 let stage1: [u8; 20] = hasher.finalize().into();
81
82 let mut hasher = Sha1::new();
84 hasher.update(stage1);
85 let stage2: [u8; 20] = hasher.finalize().into();
86
87 let mut hasher = Sha1::new();
89 hasher.update(seed);
90 hasher.update(stage2);
91 let stage3: [u8; 20] = hasher.finalize().into();
92
93 stage1
95 .iter()
96 .zip(stage3.iter())
97 .map(|(a, b)| a ^ b)
98 .collect()
99}
100
101pub fn caching_sha2_password(password: &str, auth_data: &[u8]) -> Vec<u8> {
112 if password.is_empty() {
113 return vec![];
114 }
115
116 let seed = if auth_data.len() == 21 && auth_data.last() == Some(&0) {
119 &auth_data[..20]
120 } else {
121 auth_data
122 };
123
124 let mut hasher = Sha256::new();
126 hasher.update(password.as_bytes());
127 let password_hash: [u8; 32] = hasher.finalize().into();
128
129 let mut hasher = Sha256::new();
131 hasher.update(password_hash);
132 let password_hash_hash: [u8; 32] = hasher.finalize().into();
133
134 let mut hasher = Sha256::new();
136 hasher.update(password_hash_hash);
137 hasher.update(seed);
138 let scramble: [u8; 32] = hasher.finalize().into();
139
140 password_hash
142 .iter()
143 .zip(scramble.iter())
144 .map(|(a, b)| a ^ b)
145 .collect()
146}
147
148pub fn generate_nonce(length: usize) -> Vec<u8> {
155 use rand::Rng;
156 let mut bytes = vec![0u8; length];
157 UnwrapErr(SysRng).fill_bytes(&mut bytes);
158 bytes
159}
160
161pub fn sha256_password_rsa(
177 password: &str,
178 seed: &[u8],
179 public_key_pem: &[u8],
180 use_oaep: bool,
181) -> Result<Vec<u8>, String> {
182 let mut pw = password.as_bytes().to_vec();
184 pw.push(0); if seed.is_empty() {
187 return Err("Seed is empty".to_string());
188 }
189
190 for (i, b) in pw.iter_mut().enumerate() {
191 *b ^= seed[i % seed.len()];
192 }
193
194 let pem = std::str::from_utf8(public_key_pem)
196 .map_err(|e| format!("Public key is not valid UTF-8 PEM: {e}"))?;
197
198 let pub_key = RsaPublicKey::from_public_key_pem(pem)
200 .or_else(|_| RsaPublicKey::from_pkcs1_pem(pem))
201 .map_err(|e| format!("Failed to parse RSA public key PEM: {e}"))?;
202
203 let encrypted = if use_oaep {
204 let padding = rsa::Oaep::<Sha1>::new();
206 pub_key
207 .encrypt(&mut UnwrapErr(SysRng), padding, &pw)
208 .map_err(|e| format!("RSA OAEP encryption failed: {e}"))?
209 } else {
210 let padding = rsa::Pkcs1v15Encrypt;
211 pub_key
212 .encrypt(&mut UnwrapErr(SysRng), padding, &pw)
213 .map_err(|e| format!("RSA PKCS1v1.5 encryption failed: {e}"))?
214 };
215
216 Ok(encrypted)
217}
218
219pub fn xor_password_with_seed(password: &str, seed: &[u8]) -> Vec<u8> {
224 let password_bytes = password.as_bytes();
225 let mut result = Vec::with_capacity(password_bytes.len() + 1);
226
227 for (i, &byte) in password_bytes.iter().enumerate() {
228 let seed_byte = seed.get(i % seed.len()).copied().unwrap_or(0);
229 result.push(byte ^ seed_byte);
230 }
231
232 result.push(0);
234
235 result
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn test_mysql_native_password_empty() {
244 let result = mysql_native_password("", &[0; 20]);
245 assert!(result.is_empty());
246 }
247
248 #[test]
249 fn test_mysql_native_password() {
250 let seed = [0u8; 20];
254 let result = mysql_native_password("secret", &seed);
255
256 assert_eq!(result.len(), 20);
258
259 let result2 = mysql_native_password("secret", &seed);
261 assert_eq!(result, result2);
262 }
263
264 #[test]
265 fn test_mysql_native_password_real_seed() {
266 let seed = [
268 0x3d, 0x4c, 0x5e, 0x2f, 0x1a, 0x0b, 0x7c, 0x8d, 0x9e, 0xaf, 0x10, 0x21, 0x32, 0x43,
269 0x54, 0x65, 0x76, 0x87, 0x98, 0xa9,
270 ];
271
272 let result = mysql_native_password("mypassword", &seed);
273 assert_eq!(result.len(), 20);
274
275 let result2 = mysql_native_password("otherpassword", &seed);
277 assert_ne!(result, result2);
278 }
279
280 #[test]
281 fn test_caching_sha2_password_empty() {
282 let result = caching_sha2_password("", &[0; 20]);
283 assert!(result.is_empty());
284 }
285
286 #[test]
287 fn test_caching_sha2_password() {
288 let seed = [0u8; 20];
289 let result = caching_sha2_password("secret", &seed);
290
291 assert_eq!(result.len(), 32);
293
294 let result2 = caching_sha2_password("secret", &seed);
296 assert_eq!(result, result2);
297 }
298
299 #[test]
300 fn test_caching_sha2_password_with_nul() {
301 let mut seed = vec![0u8; 20];
303 seed.push(0); let result = caching_sha2_password("secret", &seed);
306 assert_eq!(result.len(), 32);
307
308 let result2 = caching_sha2_password("secret", &seed[..20]);
310 assert_eq!(result, result2);
311 }
312
313 #[test]
314 fn test_generate_nonce() {
315 let nonce1 = generate_nonce(20);
316 let nonce2 = generate_nonce(20);
317
318 assert_eq!(nonce1.len(), 20);
319 assert_eq!(nonce2.len(), 20);
320
321 assert_ne!(nonce1, nonce2);
323 }
324
325 #[test]
326 fn test_xor_password_with_seed() {
327 let password = "test";
328 let seed = [1, 2, 3, 4, 5, 6, 7, 8];
329
330 let result = xor_password_with_seed(password, &seed);
331
332 assert_eq!(result.len(), 5);
334
335 assert_eq!(result[4], 0);
337
338 let recovered: Vec<u8> = result[..4]
340 .iter()
341 .enumerate()
342 .map(|(i, &b)| b ^ seed[i % seed.len()])
343 .collect();
344 assert_eq!(recovered, password.as_bytes());
345 }
346
347 const SPKI_PUBLIC_KEY: &str = "-----BEGIN PUBLIC KEY-----\n\
350MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArJ59U1RtRan3BEJqsItb\n\
351tD3nDU7ThwlNaJ42vRSEt/UzFO5/yemxUz3ogTUcsDMgXeLVjQjwwV0+lh+s9IWc\n\
3529fU6nu4Q8yW7Pc/SDpDkFBdEtLAOIjfSMv0CzoPQB0A0njVFe7l7SuyrMWQ/19N5\n\
353iEtqZQmP2y7h5a23XgPGogXHm0XnKpueZn9KFXhK2lNhZj9IUuQLmhzrH0pov8Ae\n\
354FknazXZxL5aoAG+cJIoHKsf9NcGTzj0Hewb36YlgVi+yZ5NRkhgjklQ8E6IL+aaW\n\
355yEOsCBtS8kCd/nHP4t6ZeyExdpggPGQ2nJq18jG+sttI+3AnzQhXtR2Adq9LKVdN\n\
356RQIDAQAB\n\
357-----END PUBLIC KEY-----\n";
358
359 const PKCS1_PUBLIC_KEY: &str = "-----BEGIN RSA PUBLIC KEY-----\n\
362MIIBCgKCAQEArJ59U1RtRan3BEJqsItbtD3nDU7ThwlNaJ42vRSEt/UzFO5/yemx\n\
363Uz3ogTUcsDMgXeLVjQjwwV0+lh+s9IWc9fU6nu4Q8yW7Pc/SDpDkFBdEtLAOIjfS\n\
364Mv0CzoPQB0A0njVFe7l7SuyrMWQ/19N5iEtqZQmP2y7h5a23XgPGogXHm0XnKpue\n\
365Zn9KFXhK2lNhZj9IUuQLmhzrH0pov8AeFknazXZxL5aoAG+cJIoHKsf9NcGTzj0H\n\
366ewb36YlgVi+yZ5NRkhgjklQ8E6IL+aaWyEOsCBtS8kCd/nHP4t6ZeyExdpggPGQ2\n\
367nJq18jG+sttI+3AnzQhXtR2Adq9LKVdNRQIDAQAB\n\
368-----END RSA PUBLIC KEY-----\n";
369
370 #[test]
375 fn test_sha256_password_rsa_accepts_both_pem_encodings() {
376 let seed = [
377 0x3d, 0x4c, 0x5e, 0x2f, 0x1a, 0x0b, 0x7c, 0x8d, 0x9e, 0xaf, 0x10, 0x21, 0x32, 0x43,
378 0x54, 0x65, 0x76, 0x87, 0x98, 0xa9,
379 ];
380
381 for pem in [SPKI_PUBLIC_KEY, PKCS1_PUBLIC_KEY] {
382 for use_oaep in [true, false] {
383 let out = sha256_password_rsa("hunter2", &seed, pem.as_bytes(), use_oaep)
384 .expect("RSA encryption with a 2048-bit MySQL-style key");
385 assert_eq!(out.len(), 256, "pem={pem} oaep={use_oaep}");
387 let again = sha256_password_rsa("hunter2", &seed, pem.as_bytes(), use_oaep)
389 .expect("second encryption");
390 assert_ne!(out, again, "padding must be randomized");
391 }
392 }
393 }
394
395 #[test]
396 fn test_sha256_password_rsa_rejects_bad_input() {
397 assert!(sha256_password_rsa("pw", &[], SPKI_PUBLIC_KEY.as_bytes(), true).is_err());
398 assert!(sha256_password_rsa("pw", &[1, 2, 3], b"not a pem", true).is_err());
399 }
400
401 #[test]
402 fn test_plugin_names() {
403 assert_eq!(plugins::MYSQL_NATIVE_PASSWORD, "mysql_native_password");
404 assert_eq!(plugins::CACHING_SHA2_PASSWORD, "caching_sha2_password");
405 assert_eq!(plugins::SHA256_PASSWORD, "sha256_password");
406 }
407}