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}