1#![forbid(unsafe_code)]
36#![deny(clippy::mem_forget)]
37#![warn(
38 missing_docs,
39 rust_2018_idioms,
40 trivial_casts,
41 unused_qualifications,
42 overflowing_literals
43)]
44#![doc(html_root_url = "https://docs.rs/checkpwn_lib/0.2.1")]
45
46mod api;
47mod errors;
48
49pub use errors::CheckpwnError;
50use std::{thread, time};
51
52pub const CHECKPWN_USER_AGENT: &str = "checkpwn - cargo utility tool for hibp";
54
55pub fn check_account(account: &str, api_key: &str) -> Result<bool, CheckpwnError> {
60 if account.is_empty() || api_key.is_empty() {
61 return Err(CheckpwnError::EmptyInput);
62 }
63
64 thread::sleep(time::Duration::from_millis(1600));
66
67 let acc_db_api_route = api::arg_to_api_route(&api::CheckableChoices::Acc, account);
68 let paste_db_api_route = api::arg_to_api_route(&api::CheckableChoices::Paste, account);
69
70 let agent: ureq::Agent = ureq::AgentBuilder::new()
71 .timeout_connect(time::Duration::from_secs(10))
72 .build();
73
74 let acc_stat = agent
75 .get(&acc_db_api_route)
76 .set("User-Agent", CHECKPWN_USER_AGENT)
77 .set("hibp-api-key", api_key)
78 .call();
79
80 let paste_stat = agent
81 .get(&paste_db_api_route)
82 .set("User-Agent", CHECKPWN_USER_AGENT)
83 .set("hibp-api-key", api_key)
84 .call();
85
86 api::evaluate_acc_breach_statuscodes(
87 api::response_to_status_codes(&acc_stat)?,
88 api::response_to_status_codes(&paste_stat)?,
89 )
90}
91
92pub struct Password {
95 hash: String,
96}
97
98impl Password {
99 pub fn new(password: &str) -> Result<Self, CheckpwnError> {
101 if password.is_empty() {
102 return Err(CheckpwnError::EmptyInput);
103 }
104
105 Ok(Self {
106 hash: api::hash_password(password),
107 })
108 }
109}
110
111impl std::fmt::Debug for Password {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 write!(f, "Password {{ hash: ***OMITTED*** }}")
114 }
115}
116
117impl Drop for Password {
118 fn drop(&mut self) {
119 use zeroize::Zeroize;
120 self.hash.zeroize()
121 }
122}
123
124pub fn check_password(password: &Password) -> Result<bool, CheckpwnError> {
128 let pass_db_api_route = api::arg_to_api_route(&api::CheckableChoices::Pass, &password.hash);
129
130 let agent: ureq::Agent = ureq::AgentBuilder::new()
131 .timeout_connect(time::Duration::from_secs(10))
132 .build();
133
134 let pass_stat = agent
135 .get(&pass_db_api_route)
136 .set("User-Agent", CHECKPWN_USER_AGENT)
137 .set("Add-Padding", "true")
138 .call();
139
140 let request_status = api::response_to_status_codes(&pass_stat)?;
141 let pass_body: String = pass_stat.unwrap().into_string().unwrap();
144
145 if api::search_in_range(&pass_body, &password.hash) {
146 if request_status == 200 {
147 Ok(true)
148 } else if request_status == 404 {
149 Ok(false)
150 } else {
151 Err(CheckpwnError::StatusCode)
152 }
153 } else {
154 Ok(false)
155 }
156}
157#[test]
158fn test_empty_input_errors() {
159 assert!(check_account("", "Test").is_err());
160 assert!(check_account("Test", "").is_err());
161 assert!(Password::new("").is_err());
162}
163
164#[cfg(test)]
165#[cfg(feature = "ci_test")]
166fn get_env_api_key_from_ci() -> String {
167 std::env::var("API_KEY").unwrap()
170}
171
172#[cfg(feature = "ci_test")]
173#[test]
174fn test_check_account() {
175 use rand::prelude::*;
176
177 let mut rng = thread_rng();
178 let mut email_user: [char; 8] = ['a'; 8];
179 let mut email_domain: [char; 8] = ['a'; 8];
180 rng.fill(&mut email_user);
181 rng.fill(&mut email_domain);
182
183 let rnd_email = format!(
184 "{:?}@{:?}.com",
185 email_user.iter().collect::<String>(),
186 email_domain.iter().collect::<String>()
187 );
188
189 let api_key = get_env_api_key_from_ci();
190
191 assert!(check_account("test@example.com", &api_key).unwrap());
192 assert!(!check_account(&rnd_email, &api_key).unwrap());
193}
194
195#[test]
196fn test_check_password() {
197 let breached_password = Password::new("qwerty").unwrap();
198 let non_breached_password = Password::new("dHRUKbDaKgIobOtX").unwrap();
199
200 assert!(check_password(&breached_password).unwrap());
201 assert!(!check_password(&non_breached_password).unwrap());
202}