use std::str::{FromStr, from_utf8};
use hyper::{Client, body};
use hyper_tls::HttpsConnector;
use sha1::{Digest, Sha1};
use crate::errors::{self, PasswordUtilsError};
pub mod blocking;
pub async fn is_pwned(pass: &str) -> errors::Result<bool> {
check_pwned(pass)
.await
.map(|api_resp| api_resp != PwnedResponse::Ok)
}
pub async fn check_pwned(pass: &str) -> errors::Result<PwnedResponse> {
let (hash_head, hash_tail) = calc_sha1_hash(pass);
let pwned_passwords_string = get_pwned_password_response_string(&hash_head).await?;
let pwned_resp = parse_pwned_password_api_response(&pwned_passwords_string)
.into_iter()
.find(|response_elem| response_elem.hash_tail == hash_tail)
.map(|found| PwnedResponse::Pwned(found.occurrences));
Ok(pwned_resp.unwrap_or_else(|| PwnedResponse::Ok))
}
async fn get_pwned_password_response_string(hash_head: &str) -> errors::Result<String> {
let https = HttpsConnector::new();
let client = Client::builder().build::<_, hyper::Body>(https);
let uri_string = format!("https://api.pwnedpasswords.com/range/{}", hash_head);
let uri = uri_string.parse()?;
let resp = client.get(uri).await?;
if resp.status().is_success() {
let body_bytes = body::to_bytes(resp.into_body()).await?;
Ok(from_utf8(body_bytes.as_ref())?.to_string())
} else {
Err(PasswordUtilsError::CommunicationWithThirdPartyApiError(
format!("Error while invoking pwnedpasswords api: {}", resp.status()),
))
}
}
fn parse_pwned_password_api_response(resp_str: &str) -> Vec<PwnedPasswordsApiResponse> {
resp_str
.lines()
.map(|line| {
let splitted: Vec<&str> = line.trim().split(':').collect();
if splitted.len() == 2 {
let hash_tail = splitted.first().unwrap().trim().to_lowercase();
let occurrences_str = *splitted.last().unwrap();
let occurrences = FromStr::from_str(occurrences_str.trim())?;
Ok((hash_tail, occurrences))
} else {
Err(PasswordUtilsError::ParseError(format!(
"Error while parsing pwnedpasswords api response line: {}",
line
)))
}
})
.map(|res: errors::Result<(String, isize)>| {
let (hash_tail, occurrences) = res.unwrap();
PwnedPasswordsApiResponse::new(hash_tail, occurrences)
})
.collect()
}
fn calc_sha1_hash(pass: &str) -> (String, String) {
let mut hasher = Sha1::new();
hasher.update(pass.as_bytes());
let hash_string = hex::encode(hasher.finalize());
let (hash_head, hash_tail) = hash_string.split_at(5);
(
hash_head.to_string().to_lowercase(),
hash_tail.to_string().to_lowercase(),
)
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum PwnedResponse {
Pwned(isize),
Ok,
}
#[derive(Debug, PartialEq, Eq, Clone)]
struct PwnedPasswordsApiResponse {
hash_tail: String,
occurrences: isize,
}
impl PwnedPasswordsApiResponse {
fn new(hash_tail: String, occurrences: isize) -> PwnedPasswordsApiResponse {
PwnedPasswordsApiResponse {
hash_tail,
occurrences,
}
}
}
#[cfg(test)]
mod pwned_unit_tests {
use tokio::runtime::Runtime;
use super::*;
#[test]
#[ignore]
fn test_dummy() {
let f = check_pwned("test");
let res = Runtime::new().unwrap().block_on(f).unwrap();
assert!(res != PwnedResponse::Ok);
}
#[test]
fn test_calc_sha1_hash() {
let (hash_head, hash_tail) = calc_sha1_hash("test");
assert!(hash_head == "a94a8");
assert!(hash_tail == "fe5ccb19ba61c4c0873d391e987982fbbd3");
}
#[test]
fn test_parse_pwned_api_response() {
let resp_str = "00264A0EA456B57A3FC7258B13F3D29B3C0:6
00294015E5A8513C73396D18309F3FFF34A:6
005656C989B06C7846338A1473281F2A791:4
007279035BE63272C81B84BD8B07D25D7E5:2
00791B26EB0E2F2C108CC538F771A640A6F:2
FE5CCB19BA61C4C0873D391E987982FBBD3:76479
010B55A0CE243B3AA85FC808ACBEB97FFA3:1
011F0995FD72D213077D18CDCD4D08E00EA:2
02A4E38B06CD1DA522048DD15257A584578:2
03ECD7302EDC571D9F2D43848F045743D9E:5
04D07D84D6474B686D5DD5F5C72A729C43C:2
054555A079E6C52256D15651C2A6663DDB9:1
05A7177A60AB6D2D0889FD08B6DFA6029FC:1
05ED8B82BC639347C1509E9FAC64AA2D4FD:2
06A1A3683C2CF4C9E91415A1272857D216D:1
083536B05F8D77476B109A31B4FF50FC5E5:2
08597FCF86893DE61DFD7CA71D1F14D2391:6
08F8BCF21B908CBCF69053F5BF0A9B031AC:4
090840B696670B1EA84DFF706905FBDE59E:3
09F379E2E256538E9C98A20B6FDB020AEAE:3
0A6D8A3C5076E5286F0BCD5113E9AADFFE2:3
0AB7D91C1985FC5B703C1CD03FF63DEC533:2
0B6C4C0F0C06DB5C3BFDC2492020A0ABC59:1
0B918D4FD7045B4704DF52F9915C6B8F8D0:2
0C9EF2CD9CF0B7F5D5F9944439214C1D917:2
0FDC95106A7317D7050498F80F3AB967899:1
10651664D4736F4B78BFC747F25D62A4BC5:2
10B1F31D9E0F0248404EA1988CDC2CDF1D3:2
10D5F6C93918F47C196629F9FFF0304F4E5:1
115F3ECCB8BED60A41FF462427E40519777:1
11A2AC0DB7DF425B380482C68A507BEC0EC:1
11E22CC801506FC5E7F86E0924947C78935:9
11F97FB9ACCBB56C8DE320C7945054044CC:2";
let parsed = parse_pwned_password_api_response(resp_str);
assert!(parsed.len() == 33);
assert!(
parsed
.iter()
.find(|resp| resp.hash_tail == "fe5ccb19ba61c4c0873d391e987982fbbd3")
.is_some()
);
}
}