use hmac::{Hmac, Mac};
use sha2::Sha256;
use std::sync::atomic::{AtomicU64, Ordering};
type HmacSha256 = Hmac<Sha256>;
static HEARTBEAT: AtomicU64 = AtomicU64::new(0);
#[derive(Debug, Clone)]
pub struct ValidationReceipt {
pub signature: [u8; 32],
pub heartbeat: u64,
pub timestamp: u64,
}
impl ValidationReceipt {
pub(crate) fn issue(license_key: &str, fingerprint: &str) -> Self {
let beat = HEARTBEAT.fetch_add(1, Ordering::SeqCst) + 1;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let mut mac = <HmacSha256 as Mac>::new_from_slice(RECEIPT_KEY)
.expect("HMAC accepts any key length"); mac.update(license_key.as_bytes());
mac.update(b"|");
mac.update(fingerprint.as_bytes());
mac.update(b"|");
mac.update(&now.to_le_bytes());
let result = mac.finalize().into_bytes();
let mut sig = [0u8; 32];
sig.copy_from_slice(&result);
Self {
signature: sig,
heartbeat: beat,
timestamp: now,
}
}
pub fn verify(&self, license_key: &str, fingerprint: &str, max_age_secs: u64) -> bool {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
if now.saturating_sub(self.timestamp) > max_age_secs {
return false;
}
let mut mac = <HmacSha256 as Mac>::new_from_slice(RECEIPT_KEY)
.expect("HMAC accepts any key length"); mac.update(license_key.as_bytes());
mac.update(b"|");
mac.update(fingerprint.as_bytes());
mac.update(b"|");
mac.update(&self.timestamp.to_le_bytes());
mac.verify_slice(&self.signature).is_ok()
}
}
pub fn heartbeat_count() -> u64 {
HEARTBEAT.load(Ordering::SeqCst)
}
pub(crate) fn sign_cache(data: &[u8]) -> [u8; 32] {
let mut mac = <HmacSha256 as Mac>::new_from_slice(CACHE_KEY)
.expect("HMAC accepts any key length"); mac.update(data);
let result = mac.finalize().into_bytes();
let mut sig = [0u8; 32];
sig.copy_from_slice(&result);
sig
}
pub(crate) fn verify_cache(data: &[u8], expected_sig: &[u8; 32]) -> bool {
let mut mac = <HmacSha256 as Mac>::new_from_slice(CACHE_KEY)
.expect("HMAC accepts any key length"); mac.update(data);
mac.verify_slice(expected_sig).is_ok()
}
static RECEIPT_KEY: &[u8] = b"ag_receipt_v1_do_not_extract";
static CACHE_KEY: &[u8] = b"ag_cache_sig_v1_do_not_extract";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn receipt_round_trip() {
let receipt = ValidationReceipt::issue("TEST-KEY", "abc123fp");
assert!(receipt.verify("TEST-KEY", "abc123fp", 60));
}
#[test]
fn receipt_wrong_key() {
let receipt = ValidationReceipt::issue("TEST-KEY", "abc123fp");
assert!(!receipt.verify("WRONG-KEY", "abc123fp", 60));
}
#[test]
fn receipt_wrong_fingerprint() {
let receipt = ValidationReceipt::issue("TEST-KEY", "abc123fp");
assert!(!receipt.verify("TEST-KEY", "wrong_fp", 60));
}
#[test]
fn receipt_expired() {
let mut receipt = ValidationReceipt::issue("TEST-KEY", "abc123fp");
receipt.timestamp = 0; assert!(!receipt.verify("TEST-KEY", "abc123fp", 60));
}
#[test]
fn heartbeat_increments() {
let before = heartbeat_count();
let _ = ValidationReceipt::issue("K", "F");
let _ = ValidationReceipt::issue("K", "F");
let after = heartbeat_count();
assert_eq!(after, before + 2);
}
#[test]
fn cache_signature_valid() {
let data = b"some cache data";
let sig = sign_cache(data);
assert!(verify_cache(data, &sig));
}
#[test]
fn cache_signature_detects_tamper() {
let data = b"some cache data";
let sig = sign_cache(data);
let tampered = b"some cache DATA";
assert!(!verify_cache(tampered, &sig));
}
}