use std::collections::HashSet;
use sha1::Digest;
use crate::{Error, Result};
#[derive(Default, Serialize, Deserialize, Clone)]
pub enum PasswordScanning {
#[cfg_attr(not(feature = "pwned100k"), default)]
None,
Custom { passwords: HashSet<String> },
#[cfg(feature = "pwned100k")]
#[default]
Top100k,
#[cfg(feature = "easypwned")]
EasyPwned { endpoint: String },
#[cfg(feature = "have_i_been_pwned")]
HIBP { api_key: String },
}
#[cfg(feature = "pwned100k")]
lazy_static! {
static ref TOP_100K_COMPROMISED: HashSet<String> = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/pwned100k.txt"))
.split('\n')
.map(|x| x.into())
.collect();
}
impl PasswordScanning {
pub async fn assert_safe(&self, password: &str) -> Result<()> {
if password.len() < 8 {
return Err(Error::ShortPassword);
}
match self {
PasswordScanning::None => Ok(()),
PasswordScanning::Custom { passwords } => {
if passwords.contains(password) {
Err(Error::CompromisedPassword)
} else {
Ok(())
}
}
#[cfg(feature = "easypwned")]
PasswordScanning::EasyPwned { endpoint } => {
let mut hasher = sha1::Sha1::new();
hasher.update(password);
let pwd_hash = hasher.finalize();
#[derive(Deserialize)]
struct EasyPwnedResult {
secure: bool,
}
let result = match reqwest::get(format!("{endpoint}/hash/{pwd_hash:#02x}")).await {
Ok(response) => match response.json::<EasyPwnedResult>().await {
Ok(result) => Ok(result.secure),
Err(_) => Err(Error::InternalError),
},
Err(_) => Err(Error::InternalError),
};
match result {
Ok(true) => Ok(()),
_ => Err(Error::CompromisedPassword),
}
}
#[cfg(feature = "pwned100k")]
PasswordScanning::Top100k => {
if TOP_100K_COMPROMISED.contains(password) {
Err(Error::CompromisedPassword)
} else {
Ok(())
}
}
#[cfg(feature = "have_i_been_pwned")]
PasswordScanning::HIBP { .. } => {
unimplemented!("Have I Been Pwned? API is not supported yet.")
}
}
}
}
#[cfg(test)]
mod tests {
use crate::Error;
use super::PasswordScanning;
use std::collections::HashSet;
#[async_std::test]
async fn it_accepts_any_passwords() {
let passwords = PasswordScanning::None;
assert_eq!(passwords.assert_safe("example123").await, Ok(()));
}
#[async_std::test]
async fn it_accepts_some_passwords() {
let passwords = PasswordScanning::Custom {
passwords: HashSet::from(["abc".to_string()]),
};
assert_eq!(passwords.assert_safe("example123").await, Ok(()));
}
#[async_std::test]
async fn it_rejects_some_passwords() {
let passwords = PasswordScanning::Custom {
passwords: HashSet::from(["example123".to_string()]),
};
assert_eq!(
passwords.assert_safe("example123").await,
Err(Error::CompromisedPassword)
);
assert_eq!(
passwords.assert_safe("short").await,
Err(Error::ShortPassword)
);
}
}