use super::timestamp;
use hex::encode;
use nanoid::nanoid;
use sha2::{Digest, Sha256};
use std::time::{SystemTime, UNIX_EPOCH};
use tibba_error::Error;
use uuid::{NoContext, Timestamp, Uuid};
type Result<T> = std::result::Result<T, Error>;
const SIGNATURE_TTL_SECS: i64 = 5 * 60;
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let acc = a
.iter()
.zip(b.iter())
.fold(0u8, |acc, (x, y)| acc | (x ^ y));
acc == 0
}
pub fn uuid() -> String {
let d = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let ts = Timestamp::from_unix(NoContext, d.as_secs(), d.subsec_nanos());
Uuid::new_v7(ts).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) -> String {
sha256_multi(&[value.as_bytes(), b":", secret.as_bytes()])
}
pub fn timestamp_hash(value: &str, secret: &str) -> (i64, String) {
let ts = timestamp();
let ts_str = ts.to_string();
let hash = sha256_multi(&[
ts_str.as_bytes(),
b":",
value.as_bytes(),
b":",
secret.as_bytes(),
]);
(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"));
}
Ok(())
}
pub fn validate_timestamp_hash(ts: i64, value: &str, hash: &str, secret: &str) -> Result<()> {
let category = "timestamp_hash";
if (timestamp() - ts).abs() > SIGNATURE_TTL_SECS {
return Err(Error::new("signature is expired").with_category(category));
}
let ts_str = ts.to_string();
let expected_hash = sha256_multi(&[
ts_str.as_bytes(),
b":",
value.as_bytes(),
b":",
secret.as_bytes(),
]);
if !constant_time_eq(expected_hash.as_bytes(), hash.as_bytes()) {
return Err(Error::new("signature is invalid").with_category(category));
}
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 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");
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 timestamp_hash_round_trip() {
let (ts, sig) = timestamp_hash("payload", "secret");
assert!(validate_timestamp_hash(ts, "payload", &sig, "secret").is_ok());
let expired_ts = ts - (SIGNATURE_TTL_SECS + 1);
let expired_sig = sha256_multi(&[
expired_ts.to_string().as_bytes(),
b":",
b"payload",
b":",
b"secret",
]);
let err =
validate_timestamp_hash(expired_ts, "payload", &expired_sig, "secret").unwrap_err();
assert!(err.to_string().contains("expired"));
assert!(validate_timestamp_hash(ts, "payloadX", &sig, "secret").is_err());
}
#[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""));
}
}