use super::timestamp;
use hex::encode;
use hmac::{Hmac, KeyInit, Mac};
use nanoid::nanoid;
use sha2::{Digest, Sha256};
use subtle::ConstantTimeEq;
use tibba_error::Error;
use uuid::Uuid;
type Result<T> = std::result::Result<T, Error>;
type HmacSha256 = Hmac<Sha256>;
const SIGNATURE_TTL_SECS: i64 = 5 * 60;
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
a.ct_eq(b).into()
}
fn hmac_sha256(secret: &[u8], parts: &[&[u8]]) -> Result<String> {
let mut mac =
HmacSha256::new_from_slice(secret).map_err(|e| Error::new(e).with_category("sign_hash"))?;
for part in parts {
mac.update(part);
}
Ok(encode(mac.finalize().into_bytes()))
}
pub fn uuid() -> String {
Uuid::now_v7().to_string()
}
pub fn nanoid(size: usize) -> String {
nanoid!(size)
}
pub fn float_to_fixed(value: f64, precision: usize) -> String {
let p = precision.min(4);
format!("{value:.p$}")
}
fn sha256_multi(parts: &[&[u8]]) -> String {
let mut hasher = Sha256::new();
for part in parts {
hasher.update(part);
}
encode(hasher.finalize())
}
pub fn sha256(data: &[u8]) -> String {
sha256_multi(&[data])
}
pub fn sign_hash(value: &str, secret: &str) -> Result<String> {
hmac_sha256(secret.as_bytes(), &[value.as_bytes()])
}
pub fn timestamp_hash(value: &str, secret: &str) -> Result<(i64, String)> {
let ts = timestamp();
let ts_str = ts.to_string();
let hash = hmac_sha256(
secret.as_bytes(),
&[ts_str.as_bytes(), b":", value.as_bytes()],
)?;
Ok((ts, hash))
}
pub fn validate_sign_hash(value: &str, hash: &str, secret: &str) -> Result<()> {
let expected = sign_hash(value, secret)?;
if !constant_time_eq(expected.as_bytes(), hash.as_bytes()) {
return Err(Error::new("signature is invalid")
.with_category("sign_hash")
.with_status(401));
}
Ok(())
}
pub fn validate_timestamp_hash(ts: i64, value: &str, hash: &str, secret: &str) -> Result<()> {
let category = "timestamp_hash";
if timestamp().saturating_sub(ts).saturating_abs() > SIGNATURE_TTL_SECS {
return Err(Error::new("signature is expired")
.with_category(category)
.with_status(401));
}
let ts_str = ts.to_string();
let expected_hash = hmac_sha256(
secret.as_bytes(),
&[ts_str.as_bytes(), b":", value.as_bytes()],
)?;
if !constant_time_eq(expected_hash.as_bytes(), hash.as_bytes()) {
return Err(Error::new("signature is invalid")
.with_category(category)
.with_status(401));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn to_fixed() {
assert_eq!("1", float_to_fixed(1.123412, 0));
assert_eq!("1.1", float_to_fixed(1.123412, 1));
assert_eq!("1.12", float_to_fixed(1.123412, 2));
assert_eq!("1.123", float_to_fixed(1.123412, 3));
assert_eq!("1.1234", float_to_fixed(1.123412, 4));
assert_eq!("1.1234", float_to_fixed(1.123412, 10));
}
#[test]
fn uuid_v7_is_strictly_monotonic_within_process() {
let ids: Vec<String> = (0..2000).map(|_| uuid()).collect();
for pair in ids.windows(2) {
assert!(
pair[0] < pair[1],
"UUIDv7 出现逆序: {} >= {}",
pair[0],
pair[1]
);
}
assert!(ids.iter().all(|id| id.as_bytes()[14] == b'7'));
}
#[test]
fn sha256_is_stable_lowercase_hex() {
let h = sha256(b"hello");
assert_eq!(
h,
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
);
assert_eq!(h.len(), 64);
}
#[test]
fn sign_hash_round_trip() {
let sig = sign_hash("payload", "secret").unwrap();
assert_eq!(sig.len(), 64);
assert!(sig.bytes().all(|b| b.is_ascii_hexdigit()));
assert!(validate_sign_hash("payload", &sig, "secret").is_ok());
assert!(validate_sign_hash("payloadX", &sig, "secret").is_err());
assert!(validate_sign_hash("payload", &sig, "secretX").is_err());
assert!(validate_sign_hash("payload", "deadbeef", "secret").is_err());
}
#[test]
fn sign_hash_is_keyed() {
let a = sign_hash("payload", "secret-a").unwrap();
let b = sign_hash("payload", "secret-b").unwrap();
assert_ne!(a, b);
assert_ne!(a, sha256(b"payload"), "签名不得退化成无密钥哈希");
}
#[test]
fn sign_hash_is_not_secret_suffix_sha256() {
let legacy = sha256_multi(&[b"payload", b":", b"secret"]);
assert_ne!(sign_hash("payload", "secret").unwrap(), legacy);
}
#[test]
fn timestamp_hash_round_trip() {
let (ts, sig) = timestamp_hash("payload", "secret").unwrap();
assert!(validate_timestamp_hash(ts, "payload", &sig, "secret").is_ok());
let expired_ts = ts - (SIGNATURE_TTL_SECS + 1);
let expired_sig = hmac_sha256(
b"secret",
&[expired_ts.to_string().as_bytes(), b":", b"payload"],
)
.unwrap();
let err =
validate_timestamp_hash(expired_ts, "payload", &expired_sig, "secret").unwrap_err();
assert!(err.to_string().contains("expired"));
let future_ts = ts + (SIGNATURE_TTL_SECS + 1);
let future_sig = hmac_sha256(
b"secret",
&[future_ts.to_string().as_bytes(), b":", b"payload"],
)
.unwrap();
assert!(validate_timestamp_hash(future_ts, "payload", &future_sig, "secret").is_err());
assert!(validate_timestamp_hash(ts, "payloadX", &sig, "secret").is_err());
assert!(validate_timestamp_hash(ts + 1, "payload", &sig, "secret").is_err());
}
#[test]
fn signature_failures_are_unauthorized() {
let err = validate_sign_hash("payload", &"0".repeat(64), "secret").unwrap_err();
assert_eq!(err.status(), 401);
let err = validate_timestamp_hash(0, "payload", &"0".repeat(64), "secret").unwrap_err();
assert_eq!(err.status(), 401);
}
#[test]
fn constant_time_eq_basic() {
assert!(constant_time_eq(b"abc", b"abc"));
assert!(!constant_time_eq(b"abc", b"abd"));
assert!(!constant_time_eq(b"abc", b"abcd"));
assert!(!constant_time_eq(b"", b"x"));
assert!(constant_time_eq(b"", b""));
}
}