haveibeenrusted/
lib.rs

1use regex::Regex;
2use reqwest::Client;
3use sha1::{Digest, Sha1};
4
5pub struct Hibr {
6    client: Client,
7    pw_response_match: Regex,
8}
9
10impl Hibr {
11    pub fn new(client: Client) -> Self {
12        Self {
13            client,
14            pw_response_match: Regex::new(r"([0-9a-fA-F]+):([0-9]+)").unwrap(),
15        }
16    }
17
18    fn get_hash(password: &str) -> String {
19        let hash = Sha1::digest(password.as_bytes());
20        base16ct::upper::encode_string(&hash)
21    }
22
23    pub async fn get_password_count(&self, password: &str) -> reqwest::Result<u32> {
24        let hex_hash = Hibr::get_hash(password);
25
26        let hash_prefix = &hex_hash[0..5];
27        let hash_suffix = &hex_hash[5..];
28
29        let resp = self.pw_range_search(hash_prefix).await?;
30
31        for m in self.pw_response_match.captures_iter(&resp) {
32            if m[1] == *hash_suffix {
33                return Ok(m[2].parse().unwrap());
34            }
35        }
36
37        Ok(0)
38    }
39
40    pub async fn is_password_breached(&self, password: &str) -> reqwest::Result<bool> {
41        let count = self.get_password_count(password).await?;
42        Ok(count > 0)
43    }
44
45    pub async fn pw_range_search(&self, hash_prefix: &str) -> reqwest::Result<String> {
46        let url = format!("https://api.pwnedpasswords.com/range/{}", hash_prefix);
47
48        let resp = self.client.get(&url).send().await?;
49        let body = resp.text().await?;
50
51        Ok(body)
52    }
53}