use std::{
fmt,
hash::{Hash, Hasher},
};
use sha2::{Digest, Sha256};
use super::{acl_exception::AclError, secrets_utility::constant_equals};
pub const NUM_HASH_BYTES: usize = 32;
#[derive(Debug, Clone)]
pub struct AclPassword {
pub password_hash: [u8; NUM_HASH_BYTES],
}
impl AclPassword {
pub fn from_string(password: &str) -> Self {
Self {
password_hash: Sha256::digest(password.as_bytes()).into(),
}
}
pub fn from_hash(hash_string: &str) -> Result<Self, AclError> {
let bytes = hash_string.as_bytes();
if bytes.len() != NUM_HASH_BYTES * 2 {
return Err(AclError::Password(
"Unable to parse input password hash. The input is of wrong length.".into(),
));
}
let mut password_hash = [0u8; NUM_HASH_BYTES];
for (i, slot) in password_hash.iter_mut().enumerate() {
let hi = hex_val(bytes[i * 2]);
let lo = hex_val(bytes[i * 2 + 1]);
match (hi, lo) {
(Some(hi), Some(lo)) => *slot = hi << 4 | lo,
_ => {
return Err(AclError::Password(
"Unable to parse input password hash. The input is not of the correct format.".into(),
));
}
}
}
Ok(Self { password_hash })
}
}
#[inline]
const fn hex_val(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'a'..=b'f' => Some(c - b'a' + 10),
b'A'..=b'F' => Some(c - b'A' + 10),
_ => None,
}
}
impl PartialEq for AclPassword {
#[inline]
fn eq(&self, other: &Self) -> bool {
constant_equals(&self.password_hash, &other.password_hash)
}
}
impl Eq for AclPassword {}
impl Hash for AclPassword {
#[inline]
fn hash<H: Hasher>(&self, state: &mut H) {
state.write_u8(self.password_hash[0]);
}
}
impl fmt::Display for AclPassword {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for b in self.password_hash {
write!(f, "{b:02x}")?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
const DUMMY_PASSWORD_HASH: &str =
"8f0e2f76e22b43e2855189877e7dc1e1e7d98c226c95db247cd1d547928334a9";
#[test]
fn from_string_sha256_hex() {
let p = AclPassword::from_string("passw0rd");
assert_eq!(p.to_string(), DUMMY_PASSWORD_HASH);
}
#[test]
fn from_hash_roundtrip() {
let p = AclPassword::from_hash(DUMMY_PASSWORD_HASH).unwrap();
assert_eq!(p, AclPassword::from_string("passw0rd"));
let upper = AclPassword::from_hash(&DUMMY_PASSWORD_HASH.to_uppercase()).unwrap();
assert_eq!(upper, p);
}
#[test]
fn from_hash_rejects_bad_input() {
assert!(matches!(
AclPassword::from_hash("abcd"),
Err(AclError::Password(_))
));
assert!(matches!(
AclPassword::from_hash(&"z".repeat(64)),
Err(AclError::Password(_))
));
}
}