#![forbid(unsafe_code)]
#![deny(clippy::mem_forget)]
#![warn(
missing_docs,
rust_2018_idioms,
trivial_casts,
unused_qualifications,
overflowing_literals
)]
mod api;
mod errors;
pub use errors::CheckpwnError;
use std::{thread, time};
pub const CHECKPWN_USER_AGENT: &str = "checkpwn - cargo utility tool for hibp";
pub fn check_account(account: &str, api_key: &str) -> Result<bool, CheckpwnError> {
if account.is_empty() || api_key.is_empty() {
return Err(CheckpwnError::EmptyInput);
}
thread::sleep(time::Duration::from_millis(1600));
let acc_db_api_route = api::arg_to_api_route(&api::CheckableChoices::Acc, account);
let paste_db_api_route = api::arg_to_api_route(&api::CheckableChoices::Paste, account);
let agent_config = ureq::Agent::config_builder()
.timeout_global(Some(time::Duration::from_secs(10)))
.build();
let agent: ureq::Agent = agent_config.into();
let acc_stat = agent
.get(&acc_db_api_route)
.header("User-Agent", CHECKPWN_USER_AGENT)
.header("hibp-api-key", api_key)
.call();
let paste_stat = agent
.get(&paste_db_api_route)
.header("User-Agent", CHECKPWN_USER_AGENT)
.header("hibp-api-key", api_key)
.call();
api::evaluate_acc_breach_statuscodes(
api::response_to_status_codes(&acc_stat)?,
api::response_to_status_codes(&paste_stat)?,
)
}
pub struct Password {
hash: String,
}
impl Password {
pub fn new(password: &str) -> Result<Self, CheckpwnError> {
if password.is_empty() {
return Err(CheckpwnError::EmptyInput);
}
Ok(Self {
hash: api::hash_password(password),
})
}
}
impl std::fmt::Debug for Password {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Password {{ hash: ***OMITTED*** }}")
}
}
impl Drop for Password {
fn drop(&mut self) {
use zeroize::Zeroize;
self.hash.zeroize()
}
}
pub fn check_password(password: &Password) -> Result<bool, CheckpwnError> {
let pass_db_api_route = api::arg_to_api_route(&api::CheckableChoices::Pass, &password.hash);
let agent_config = ureq::Agent::config_builder()
.timeout_global(Some(time::Duration::from_secs(10)))
.build();
let agent: ureq::Agent = agent_config.into();
let pass_stat = agent
.get(&pass_db_api_route)
.header("User-Agent", CHECKPWN_USER_AGENT)
.header("Add-Padding", "true")
.call();
let request_status = api::response_to_status_codes(&pass_stat)?;
let pass_body: String = pass_stat.unwrap().into_body().read_to_string().unwrap();
if api::search_in_range(&pass_body, &password.hash) {
if request_status == 200 {
Ok(true)
} else if request_status == 404 {
Ok(false)
} else {
Err(CheckpwnError::StatusCode)
}
} else {
Ok(false)
}
}
#[test]
fn test_empty_input_errors() {
assert!(check_account("", "Test").is_err());
assert!(check_account("Test", "").is_err());
assert!(Password::new("").is_err());
}
#[cfg(test)]
#[cfg(feature = "ci_test")]
fn get_env_api_key_from_ci() -> String {
std::env::var("API_KEY").unwrap()
}
#[cfg(feature = "ci_test")]
#[test]
fn test_check_account() {
use rand::RngExt;
use rand::distr::Alphanumeric;
let mut rng = rand::rng();
let email_user: String = (0..8).map(|_| rng.sample(Alphanumeric) as char).collect();
let email_domain: String = (0..8).map(|_| rng.sample(Alphanumeric) as char).collect();
let rnd_email = format!("{:?}@{:?}.com", email_user, email_domain);
let api_key = get_env_api_key_from_ci();
assert!(check_account("test@example.com", &api_key).unwrap());
assert!(!check_account(&rnd_email, &api_key).unwrap());
}
#[test]
fn test_check_password() {
let breached_password = Password::new("qwerty").unwrap();
let non_breached_password = Password::new("dHRUKbDaKgIobOtX").unwrap();
assert!(check_password(&breached_password).unwrap());
assert!(!check_password(&non_breached_password).unwrap());
}