use hmac::{Hmac, Mac};
use sha2::Sha256;
use subtle::ConstantTimeEq;
use tracing::{error, warn};
type HmacSha256 = Hmac<Sha256>;
pub fn store_key() -> Option<Vec<u8>> {
let raw = std::env::var("RUNBOUND_STORE_KEY").ok()?;
let raw = raw.trim();
if raw.is_empty() { return None; }
if raw.len() >= 64 && raw.chars().all(|c| c.is_ascii_hexdigit()) {
hex::decode(raw).ok()
} else {
Some(raw.as_bytes().to_vec())
}
}
pub fn compute_mac(content: &[u8], key: &[u8]) -> String {
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts any key length");
mac.update(content);
hex::encode(mac.finalize().into_bytes())
}
pub fn write_mac(path: &std::path::Path, content: &[u8], key: Option<&[u8]>) -> std::io::Result<()> {
let Some(k) = key else { return Ok(()); };
let mac_str = compute_mac(content, k);
let mac_path = path.with_extension("mac");
let tmp = mac_path.with_extension("mac.tmp");
std::fs::write(&tmp, mac_str.as_bytes())?;
std::fs::rename(&tmp, &mac_path)
}
pub fn verify_mac(path: &std::path::Path, content: &[u8], key: Option<&[u8]>) -> Result<(), String> {
let mac_path = path.with_extension("mac");
let mac_exists = mac_path.exists();
match (key, mac_exists) {
(None, false) => Ok(()),
(None, true) => {
warn!(
path = %path.display(),
"Store .mac file found but RUNBOUND_STORE_KEY is not set — integrity cannot be verified."
);
Ok(())
}
(Some(_), false) => {
warn!(
path = %path.display(),
"RUNBOUND_STORE_KEY is set but no .mac sidecar found — \
file was saved without integrity protection."
);
Ok(())
}
(Some(k), true) => {
let stored = std::fs::read_to_string(&mac_path)
.map_err(|e| format!("read .mac for {}: {e}", path.display()))?;
let stored = stored.trim();
let expected = compute_mac(content, k);
if stored.as_bytes().ct_eq(expected.as_bytes()).into() {
Ok(())
} else {
error!(
path = %path.display(),
"HMAC mismatch — store file may have been tampered with. Load refused."
);
Err(format!("HMAC mismatch: {}", path.display()))
}
}
}
}