use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Key, Nonce,
};
use rand::RngCore;
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::types::LicenseResponse;
pub const GRACE_PERIOD_HOURS: u64 = 24;
#[derive(Serialize, Deserialize)]
struct CacheEntry {
license_key: String,
hwid: String,
product_id: String,
validated_at: u64,
expires_at: Option<u64>,
response_json: String,
version: u8,
}
fn cache_path(product_id: &str, hwid: &str) -> Option<std::path::PathBuf> {
use sha2::Digest;
let input = format!("{}:{}", product_id, hwid);
let hash = sha2::Sha256::digest(input.as_bytes());
let hex = hex::encode(&hash[..8]);
let base = dirs::data_local_dir()?;
let dir = base.join("AstraGuard").join("cache");
std::fs::create_dir_all(&dir).ok()?;
Some(dir.join(format!("{}.ag", hex)))
}
fn derive_key(hwid: &str, product_id: &str) -> [u8; 32] {
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
let mut mac = <HmacSha256 as Mac>::new_from_slice(b"astraguard-cache-v2")
.expect("HMAC accepts any key length");
mac.update(hwid.as_bytes());
mac.update(b":");
mac.update(product_id.as_bytes());
mac.finalize().into_bytes().into()
}
pub fn save(license_key: &str, hwid: &str, product_id: &str, response: &LicenseResponse) {
let Ok(json) = serde_json::to_string(response) else {
return;
};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let expires_ts = response
.expires_at
.as_deref()
.and_then(chrono_like_parse_pub);
let entry = CacheEntry {
license_key: license_key.to_string(),
hwid: hwid.to_string(),
product_id: product_id.to_string(),
validated_at: now,
expires_at: expires_ts,
response_json: json,
version: 1,
};
let Ok(plaintext) = serde_json::to_vec(&entry) else {
return;
};
let key_bytes = derive_key(hwid, product_id);
let key = Key::<Aes256Gcm>::from_slice(&key_bytes);
let cipher = Aes256Gcm::new(key);
let mut nonce_bytes = [0u8; 12];
rand::thread_rng().fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
let Ok(ciphertext) = cipher.encrypt(nonce, plaintext.as_ref()) else {
return;
};
let mut blob = Vec::with_capacity(12 + ciphertext.len());
blob.extend_from_slice(&nonce_bytes);
blob.extend_from_slice(&ciphertext);
if let Some(path) = cache_path(product_id, hwid) {
let _ = std::fs::write(path, blob);
}
}
pub fn load(license_key: &str, hwid: &str, product_id: &str) -> Option<LicenseResponse> {
let path = cache_path(product_id, hwid)?;
let blob = std::fs::read(&path).ok()?;
if blob.len() < 13 {
return None;
}
let nonce = Nonce::from_slice(&blob[..12]);
let ciphertext = &blob[12..];
let key_bytes = derive_key(hwid, product_id);
let key = Key::<Aes256Gcm>::from_slice(&key_bytes);
let cipher = Aes256Gcm::new(key);
let plaintext = cipher.decrypt(nonce, ciphertext).ok()?;
let entry: CacheEntry = serde_json::from_slice(&plaintext).ok()?;
if entry.hwid != hwid || entry.product_id != product_id || entry.license_key != license_key {
return None;
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
if let Some(exp) = entry.expires_at {
if exp < now {
return None;
}
}
let age_secs = now.saturating_sub(entry.validated_at);
if age_secs > GRACE_PERIOD_HOURS * 3600 {
return None;
}
let mut resp: LicenseResponse = serde_json::from_str(&entry.response_json).ok()?;
resp.is_offline_cache = Some(true);
Some(resp)
}
#[allow(dead_code)]
pub fn invalidate(product_id: &str, hwid: &str) {
if let Some(path) = cache_path(product_id, hwid) {
let _ = std::fs::remove_file(path);
}
}
pub fn chrono_like_parse_pub(s: &str) -> Option<u64> {
let s = s.trim_end_matches('Z');
let s = s.split('.').next().unwrap_or(s);
let parts: Vec<&str> = s.splitn(2, 'T').collect();
if parts.len() != 2 {
return None;
}
let date_parts: Vec<u32> = parts[0].split('-').filter_map(|p| p.parse().ok()).collect();
let time_parts: Vec<u32> = parts[1]
.split(':')
.filter_map(|p| p.parse::<f64>().ok().map(|v| v as u32))
.collect();
if date_parts.len() < 3 || time_parts.len() < 3 {
return None;
}
let (y, m, d) = (
date_parts[0] as i64,
date_parts[1] as i64,
date_parts[2] as i64,
);
let (h, min, sec) = (
time_parts[0] as i64,
time_parts[1] as i64,
time_parts[2] as i64,
);
let days = days_from_ymd(y, m, d)?;
let ts = days * 86400 + h * 3600 + min * 60 + sec;
if ts < 0 {
return None;
}
Some(ts as u64)
}
fn days_from_ymd(y: i64, m: i64, d: i64) -> Option<i64> {
if !(1..=12).contains(&m) || !(1..=31).contains(&d) {
return None;
}
let era = if m <= 2 { y - 1 } else { y };
let yoe = era.rem_euclid(400);
let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
Some(era / 400 * 146097 + doe - 719468)
}