use std::fmt;
use hmac::{Hmac, KeyInit, Mac};
use sha2::Sha256;
pub const SIGNATURE_TIMESTAMP_HEADER: &str = "x-fp-timestamp";
pub const SIGNATURE_HEADER: &str = "x-fp-signature";
#[derive(Clone)]
pub struct ResponseSigner {
key: Vec<u8>,
}
impl ResponseSigner {
pub fn new(key: &[u8]) -> Self {
Self { key: key.to_vec() }
}
pub fn sign(&self, issued_ms: u64, body: &[u8]) -> Option<String> {
let mut mac = Hmac::<Sha256>::new_from_slice(&self.key).ok()?;
mac.update(&issued_ms.to_be_bytes());
mac.update(body);
Some(hex::encode(mac.finalize().into_bytes()))
}
}
impl fmt::Debug for ResponseSigner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ResponseSigner").finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::ResponseSigner;
#[test]
fn sign_is_deterministic() {
let signer = ResponseSigner::new(b"signing-secret");
let body = br#"{"visitorId":"abc"}"#;
assert_eq!(
signer.sign(1_700_000_000_000, body),
signer.sign(1_700_000_000_000, body)
);
}
#[test]
fn signature_binds_timestamp_and_body() {
let signer = ResponseSigner::new(b"signing-secret");
let body = br#"{"visitorId":"abc"}"#;
let sig = signer.sign(1_700_000_000_000, body).unwrap();
assert_ne!(Some(&sig), signer.sign(1_700_000_000_001, body).as_ref());
assert_ne!(
Some(&sig),
signer
.sign(1_700_000_000_000, br#"{"visitorId":"xyz"}"#)
.as_ref()
);
assert_ne!(
Some(&sig),
ResponseSigner::new(b"other-secret")
.sign(1_700_000_000_000, body)
.as_ref()
);
}
#[test]
fn debug_redacts_key() {
let shown = format!("{:?}", ResponseSigner::new(b"top-secret-key"));
assert!(!shown.contains("top-secret-key"));
assert!(!shown.contains("116")); }
}