rs_password_utils/pwned/
mod.rs1use std::str::{from_utf8, FromStr};
15
16use hyper::{body, Client};
17use hyper_tls::HttpsConnector;
18use sha1::{Digest, Sha1};
19
20use crate::errors::{self, PasswordUtilsError};
21
22pub mod blocking;
23
24pub async fn is_pwned(pass: &str) -> errors::Result<bool> {
27 check_pwned(pass).await
28 .map(|api_resp| api_resp != PwnedResponse::Ok)
29}
30
31pub async fn check_pwned(pass: &str) -> errors::Result<PwnedResponse> {
34 let (hash_head, hash_tail) = calc_sha1_hash(pass);
35 let pwned_passwords_string = get_pwned_password_response_string(&hash_head).await?;
36 let pwned_resp = parse_pwned_password_api_response(&pwned_passwords_string)
37 .into_iter()
38 .find(|response_elem| response_elem.hash_tail == hash_tail)
39 .map(|found| PwnedResponse::Pwned(found.occurrences));
40 Ok(pwned_resp.unwrap_or_else(|| PwnedResponse::Ok))
41}
42
43async fn get_pwned_password_response_string(hash_head: &str) -> errors::Result<String> {
44 let https = HttpsConnector::new();
45 let client = Client::builder().build::<_, hyper::Body>(https);
46 let uri_string = format!("https://api.pwnedpasswords.com/range/{}", hash_head);
47 let uri = uri_string.parse()?;
48 let resp = client.get(uri).await?;
49 if resp.status().is_success() {
50 let body_bytes = body::to_bytes(resp.into_body()).await?;
51 Ok(from_utf8(body_bytes.as_ref())?.to_string())
52 } else {
53 Err(PasswordUtilsError::CommunicationWithThirdPartyApiError(format!("Error while invoking pwnedpasswords api: {}", resp.status())))
54 }
55}
56
57fn parse_pwned_password_api_response(resp_str: &str) -> Vec<PwnedPasswordsApiResponse> {
58 resp_str.lines()
59 .map(|line| {
60 let splitted: Vec<&str> = line.trim().split(':').collect();
61 if splitted.len() == 2 {
62 let hash_tail = splitted.first().unwrap().trim().to_lowercase();
63
64 let occurrences_str = *splitted.last().unwrap();
65 let occurrences = FromStr::from_str(occurrences_str.trim())?;
66 Ok((hash_tail, occurrences))
67 } else {
68 Err(PasswordUtilsError::ParseError(format!("Error while parsing pwnedpasswords api response line: {}", line)))
69 }
70 })
71 .map(|res: errors::Result<(String, isize)>| {
72 let (hash_tail, occurrences) = res.unwrap();
73 PwnedPasswordsApiResponse::new(hash_tail, occurrences)
74 })
75 .collect()
76}
77
78fn calc_sha1_hash(pass: &str) -> (String, String) {
79 let mut hasher = Sha1::new();
80 hasher.update(pass.as_bytes());
81 let hash_string = hex::encode(hasher.finalize());
82
83 let (hash_head, hash_tail) = hash_string.split_at(5);
84 (hash_head.to_string().to_lowercase(), hash_tail.to_string().to_lowercase())
85}
86
87#[derive(Debug, PartialEq, Eq, Clone)]
88pub enum PwnedResponse {
89 Pwned(isize),
90 Ok,
91}
92
93#[derive(Debug, PartialEq, Eq, Clone)]
94struct PwnedPasswordsApiResponse {
95 hash_tail: String,
96 occurrences: isize,
97}
98
99impl PwnedPasswordsApiResponse {
100 fn new(hash_tail: String, occurrences: isize) -> PwnedPasswordsApiResponse {
101 PwnedPasswordsApiResponse {
102 hash_tail,
103 occurrences,
104 }
105 }
106}
107
108#[cfg(test)]
109mod pwned_unit_tests {
110 use tokio::runtime::Runtime;
111
112 use super::*;
113
114 #[test]
115 #[ignore]
116 fn test_dummy() {
117 let f = check_pwned("test");
118 let res = Runtime::new().unwrap().block_on(f).unwrap();
119 assert!(res != PwnedResponse::Ok);
120 }
121
122 #[test]
123 fn test_calc_sha1_hash() {
124 let (hash_head, hash_tail) = calc_sha1_hash("test");
125 assert!(hash_head == "a94a8");
126 assert!(hash_tail == "fe5ccb19ba61c4c0873d391e987982fbbd3");
127 }
128
129 #[test]
130 fn test_parse_pwned_api_response() {
131 let resp_str = "00264A0EA456B57A3FC7258B13F3D29B3C0:6
132 00294015E5A8513C73396D18309F3FFF34A:6
133 005656C989B06C7846338A1473281F2A791:4
134 007279035BE63272C81B84BD8B07D25D7E5:2
135 00791B26EB0E2F2C108CC538F771A640A6F:2
136 FE5CCB19BA61C4C0873D391E987982FBBD3:76479
137 010B55A0CE243B3AA85FC808ACBEB97FFA3:1
138 011F0995FD72D213077D18CDCD4D08E00EA:2
139 02A4E38B06CD1DA522048DD15257A584578:2
140 03ECD7302EDC571D9F2D43848F045743D9E:5
141 04D07D84D6474B686D5DD5F5C72A729C43C:2
142 054555A079E6C52256D15651C2A6663DDB9:1
143 05A7177A60AB6D2D0889FD08B6DFA6029FC:1
144 05ED8B82BC639347C1509E9FAC64AA2D4FD:2
145 06A1A3683C2CF4C9E91415A1272857D216D:1
146 083536B05F8D77476B109A31B4FF50FC5E5:2
147 08597FCF86893DE61DFD7CA71D1F14D2391:6
148 08F8BCF21B908CBCF69053F5BF0A9B031AC:4
149 090840B696670B1EA84DFF706905FBDE59E:3
150 09F379E2E256538E9C98A20B6FDB020AEAE:3
151 0A6D8A3C5076E5286F0BCD5113E9AADFFE2:3
152 0AB7D91C1985FC5B703C1CD03FF63DEC533:2
153 0B6C4C0F0C06DB5C3BFDC2492020A0ABC59:1
154 0B918D4FD7045B4704DF52F9915C6B8F8D0:2
155 0C9EF2CD9CF0B7F5D5F9944439214C1D917:2
156 0FDC95106A7317D7050498F80F3AB967899:1
157 10651664D4736F4B78BFC747F25D62A4BC5:2
158 10B1F31D9E0F0248404EA1988CDC2CDF1D3:2
159 10D5F6C93918F47C196629F9FFF0304F4E5:1
160 115F3ECCB8BED60A41FF462427E40519777:1
161 11A2AC0DB7DF425B380482C68A507BEC0EC:1
162 11E22CC801506FC5E7F86E0924947C78935:9
163 11F97FB9ACCBB56C8DE320C7945054044CC:2";
164 let parsed = parse_pwned_password_api_response(resp_str);
165 assert!(parsed.len() == 33);
166 assert!(parsed.iter().find(|resp| resp.hash_tail == "fe5ccb19ba61c4c0873d391e987982fbbd3").is_some());
167 }
168}