auth_framework/utils/
breach_check.rs1use crate::errors::{AuthError, Result};
8use sha1::{Digest, Sha1};
9
10pub async fn is_password_breached(password: &str) -> Result<bool> {
19 let hash = hex::encode(Sha1::digest(password.as_bytes())).to_uppercase();
20 let (prefix, suffix) = hash.split_at(5);
21
22 let url = format!("https://api.pwnedpasswords.com/range/{prefix}");
23
24 let response = reqwest::Client::builder()
25 .timeout(std::time::Duration::from_secs(5))
26 .build()
27 .map_err(|e| AuthError::internal(format!("HTTP client error: {e}")))?
28 .get(&url)
29 .header("Add-Padding", "true")
30 .send()
31 .await
32 .map_err(|e| AuthError::internal(format!("HIBP API request failed: {e}")))?;
33
34 if !response.status().is_success() {
35 return Err(AuthError::internal(format!(
36 "HIBP API returned status {}",
37 response.status()
38 )));
39 }
40
41 let body = response
42 .text()
43 .await
44 .map_err(|e| AuthError::internal(format!("Failed to read HIBP response: {e}")))?;
45
46 let breached = body.lines().any(|line| line.trim().starts_with(suffix));
48
49 Ok(breached)
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55
56 #[test]
57 fn sha1_prefix_suffix_split() {
58 let hash = hex::encode(Sha1::digest(b"password")).to_uppercase();
60 assert_eq!(&hash[..5], "5BAA6");
61 assert_eq!(hash.len(), 40);
62 }
63}