wacl 0.1.3

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 wbase::hex::hex_decode;

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> {
    if hash_string.len() != NUM_HASH_BYTES * 2 {
      return Err(AclError::Password(
        "Unable to parse input password hash. The input is of wrong length.".into(),
      ));
    }
    // 十六进制折值单点在 wbase::hex(一处定义)
    let password_hash = hex_decode::<NUM_HASH_BYTES>(hash_string.as_bytes()).ok_or_else(|| {
      AclError::Password(
        "Unable to parse input password hash. The input is not of the correct format.".into(),
      )
    })?;
    Ok(Self { password_hash })
  }
}

/// 常量时间比较(口令哈希相等性)
///
/// 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(_))
    ));
  }
}