#![allow(clippy::disallowed_methods)]
use argon2::password_hash::rand_core::OsRng;
use super::error::TenancyError;
use crate::passwords::PasswordError;
pub fn hash(plaintext: &str) -> Result<String, TenancyError> {
if plaintext.is_empty() {
return Err(TenancyError::Validation(
"password must not be empty".into(),
));
}
crate::passwords::hash(plaintext).map_err(|e| match e {
PasswordError::Hash(m) => TenancyError::Validation(format!("argon2 hash failed: {m}")),
other => TenancyError::Validation(other.to_string()),
})
}
#[must_use]
pub fn generate(length: usize) -> String {
assert!(length > 0, "password length must be > 0");
use argon2::password_hash::rand_core::RngCore;
const ALPHABET: &[u8] = b"abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789";
let mut rng = OsRng;
let mut out = String::with_capacity(length);
let mut buf = [0u8; 64];
let mut filled = 0;
while out.len() < length {
if filled == 0 {
rng.fill_bytes(&mut buf);
filled = buf.len();
}
let byte = buf[buf.len() - filled];
filled -= 1;
let max = u8::try_from(ALPHABET.len() - 1).expect("alphabet < 256 chars");
if byte <= max.saturating_mul(255 / max) {
out.push(ALPHABET[(byte as usize) % ALPHABET.len()] as char);
}
}
out
}
pub fn verify(plaintext: &str, phc_hash: &str) -> Result<bool, TenancyError> {
crate::passwords::verify(plaintext, phc_hash).map_err(|e| match e {
PasswordError::Verify(m) => {
TenancyError::Validation(format!("malformed password hash: {m}"))
}
other => TenancyError::Validation(other.to_string()),
})
}
pub fn verify_dummy(plaintext: &str) {
crate::passwords::verify_dummy(plaintext);
}
pub async fn hash_async(plaintext: &str) -> Result<String, TenancyError> {
let plaintext = plaintext.to_owned();
crate::passwords::off_runtime(move || hash(&plaintext))
.await
.map_err(|_| TenancyError::Busy)?
}
pub async fn verify_async(plaintext: &str, phc_hash: &str) -> Result<bool, TenancyError> {
verify_async_in(HashLane::Login, plaintext, phc_hash).await
}
pub async fn verify_dummy_async(plaintext: &str) -> Result<(), TenancyError> {
verify_dummy_async_in(HashLane::Login, plaintext).await
}
pub(crate) use crate::passwords::HashLane;
pub(crate) async fn verify_async_in(
lane: HashLane,
plaintext: &str,
phc_hash: &str,
) -> Result<bool, TenancyError> {
let (plaintext, phc_hash) = (plaintext.to_owned(), phc_hash.to_owned());
crate::passwords::off_runtime_in(lane, move || verify(&plaintext, &phc_hash))
.await
.map_err(|_| TenancyError::Busy)?
}
pub(crate) async fn verify_dummy_async_in(
lane: HashLane,
plaintext: &str,
) -> Result<(), TenancyError> {
let plaintext = plaintext.to_owned();
crate::passwords::off_runtime_in(lane, move || verify_dummy(&plaintext))
.await
.map_err(|_| TenancyError::Busy)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::passwords::ticks_while;
#[tokio::test(flavor = "current_thread")]
async fn async_variants_do_not_block_the_runtime() {
let (h, n) = ticks_while(hash_async("hunter2")).await;
assert!(n >= 2, "hash_async stalled the runtime ({n} ticks)");
let (ok, n) = ticks_while(verify_async("hunter2", &h.unwrap())).await;
assert!(ok.unwrap());
assert!(n >= 2, "verify_async stalled the runtime ({n} ticks)");
let (r, n) = ticks_while(verify_dummy_async("hunter2")).await;
assert!(r.is_ok());
assert!(n >= 2, "verify_dummy_async stalled the runtime ({n} ticks)");
}
#[test]
fn hash_and_verify_round_trip() {
let h = hash("hunter2").unwrap();
assert!(h.starts_with("$argon2id$"));
assert!(verify("hunter2", &h).unwrap());
assert!(!verify("wrong", &h).unwrap());
}
#[test]
fn hash_rejects_empty() {
let err = hash("").unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("must not be empty"), "got: {msg}");
}
#[test]
fn verify_rejects_malformed_hash() {
let err = verify("hunter2", "not-a-phc-string").unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("malformed"), "got: {msg}");
}
#[test]
fn generate_produces_correct_length_and_charset() {
let p = generate(24);
assert_eq!(p.len(), 24);
for c in p.chars() {
assert!(
c.is_ascii_alphanumeric(),
"generated char outside expected alphabet: {c:?}"
);
assert!(
!"0O1lI".contains(c),
"ambiguous char in generated password: {c:?}"
);
}
}
#[test]
fn generate_round_trips_through_hash_and_verify() {
let p = generate(20);
let h = hash(&p).unwrap();
assert!(verify(&p, &h).unwrap());
}
#[test]
fn two_calls_to_generate_differ() {
assert_ne!(generate(16), generate(16));
}
#[test]
fn two_hashes_of_same_password_differ() {
let h1 = hash("same").unwrap();
let h2 = hash("same").unwrap();
assert_ne!(h1, h2);
assert!(verify("same", &h1).unwrap());
assert!(verify("same", &h2).unwrap());
}
}