wacl 0.1.2

Access Control List and authentication with SHA-256 password hashing
//! ACL 口令(对标 libs/server/ACL/ACLPassword.cs)
//!
//! 口令以 SHA-256 哈希存储(32 字节),比较走常量时间路径。

use std::{
  fmt,
  hash::{Hash, Hasher},
  str::from_utf8_unchecked,
};

use sha2::{Digest, Sha256};

use super::{acl_exception::AclError, secrets_utility::constant_equals};

/// 每条哈希的字节数(SHA-256)
pub const NUM_HASH_BYTES: usize = 32;

/// ACL 口令(SHA-256 哈希值)
#[derive(Debug, Clone)]
pub struct AclPassword {
  /// 口令哈希
  pub password_hash: [u8; NUM_HASH_BYTES],
}

impl AclPassword {
  /// 由明文口令构造(SHA-256(UTF-8(明文)))
  ///
  /// libs/server/ACL/ACLPassword.cs:ACLPasswordFromString
  pub fn from_string(password: &str) -> Self {
    Self {
      password_hash: Sha256::digest(password.as_bytes()).into(),
    }
  }

  /// 由十六进制哈希串构造(64 个十六进制字符)
  ///
  /// libs/server/ACL/ACLPassword.cs:ACLPasswordFromHash
  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 (slot, pair) in password_hash.iter_mut().zip(bytes.as_chunks::<2>().0) {
      let hi = hex_val(pair[0]);
      let lo = hex_val(pair[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,
  }
}

/// 常量时间比较(口令哈希相等性)
///
/// libs/server/ACL/ACLPassword.cs:Equals
impl PartialEq for AclPassword {
  #[inline]
  fn eq(&self, other: &Self) -> bool {
    constant_equals(&self.password_hash, &other.password_hash)
  }
}

impl Eq for AclPassword {}

/// 加速索引的短哈希(仅取首字节)
///
/// libs/server/ACL/ACLPassword.cs:GetHashCode
impl Hash for AclPassword {
  #[inline]
  fn hash<H: Hasher>(&self, state: &mut H) {
    state.write_u8(self.password_hash[0]);
  }
}

const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";

/// 小写十六进制输出
///
/// libs/server/ACL/ACLPassword.cs:ToString
impl fmt::Display for AclPassword {
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    let mut buf = [0u8; NUM_HASH_BYTES * 2];
    for (i, &b) in self.password_hash.iter().enumerate() {
      buf[i * 2] = HEX_CHARS[(b >> 4) as usize];
      buf[i * 2 + 1] = HEX_CHARS[(b & 0xf) as usize];
    }
    // 缓冲区全为 ASCII 16 进制码表字符,保证为有效 UTF-8
    let s = unsafe { from_utf8_unchecked(&buf) };
    f.write_str(s)
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  /// garnet/test/standalone/Garnet.test.acl/Resp/ACL/AclTest.cs:DummyPasswordHash
  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(_))
    ));
  }
}