diff --git a/src/plan.rs b/src/plan.rs index b5f04a3..01c0a72 100644 --- a/src/plan.rs +++ b/src/plan.rs @@ -219,11 +219,19 @@ pub fn classify_secret(s: &str) -> SecretKind { SecretKind::Plaintext(s.to_string()) } -/// SHA512-crypt hash (what `mkpasswd -m sha-512` produces). +/// SHA512-crypt hash compatible with glibc/libxcrypt (Dovecot, `mkpasswd -m sha-512`). +/// +/// The salt is 12 random bytes = exactly 16 salt characters. sha-crypt 0.6's +/// default (16 bytes -> 22 characters) hashes over all 22 characters, while +/// libxcrypt only uses the first 16, so such hashes never verify in Dovecot +/// (caught by the e2e test). pub fn sha512_crypt(password: &str) -> Result { + use rand::RngExt; use sha_crypt::{PasswordHasher, ShaCrypt}; - let h = - ShaCrypt::default().hash_password(password.as_bytes()).map_err(|e| anyhow::anyhow!("hashing password: {e}"))?; + let salt: [u8; 12] = rand::rng().random(); + let h = ShaCrypt::default() + .hash_password_with_salt(password.as_bytes(), &salt) + .map_err(|e| anyhow::anyhow!("hashing password: {e}"))?; Ok(h.as_str().to_string()) } @@ -487,6 +495,14 @@ mod tests { assert!(h.starts_with("$6$")); assert!(verify_sha_crypt("correct horse", &h)); assert!(!verify_sha_crypt("wrong", &h)); + // glibc/libxcrypt only use 16 salt characters; longer salts never verify there. + let salt = h.trim_start_matches("$6$").split('$').find(|f| !f.starts_with("rounds=")).unwrap(); + assert_eq!(salt.len(), 16, "{h}"); + // Algorithm matches glibc (Drepper's test vector). + assert!(verify_sha_crypt( + "Hello world!", + "$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJuesI68u4OTLiBFdcbYEdFCoEOfaS35inz1" + )); } #[test]