#![allow(dead_code)]
use hmac::{Hmac, Mac};
use serde_json::Value;
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
pub const SIGNED_VERSION: u64 = 2;
pub const CLOCK_SKEW_S: f64 = 30.0;
pub fn load() -> Option<Vec<u8>> {
if let Ok(path) = std::env::var("MENTAT_SECRET_FILE") {
let path = path.trim();
if !path.is_empty() {
let bytes = std::fs::read(path).ok()?;
let key = trim_ascii(&bytes);
return (!key.is_empty()).then(|| key.to_vec());
}
}
let v = std::env::var("MENTAT_SECRET").ok()?;
let key = trim_ascii(v.as_bytes());
(!key.is_empty()).then(|| key.to_vec())
}
fn trim_ascii(v: &[u8]) -> &[u8] {
let start = v.iter().position(|b| !b.is_ascii_whitespace()).unwrap_or(0);
let end = v
.iter()
.rposition(|b| !b.is_ascii_whitespace())
.map(|i| i + 1)
.unwrap_or(0);
if start >= end {
&[]
} else {
&v[start..end]
}
}
fn hex(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
use std::fmt::Write;
let _ = write!(s, "{b:02x}");
}
s
}
fn unhex(s: &str) -> Option<Vec<u8>> {
if !s.len().is_multiple_of(2) {
return None;
}
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
.collect()
}
fn canonical(payload: &Value) -> String {
payload.to_string()
}
pub fn sign(payload: &Value, key: &[u8]) -> String {
let mut mac = HmacSha256::new_from_slice(key).expect("hmac takes a key of any length");
mac.update(canonical(payload).as_bytes());
let sig = hex(&mac.finalize().into_bytes());
serde_json::json!({ "p": payload, "sig": sig }).to_string()
}
pub fn verify(raw: &[u8], key: &[u8]) -> Option<Value> {
let env: Value = serde_json::from_slice(raw).ok()?;
let payload = env.get("p")?;
let want = unhex(env.get("sig")?.as_str()?)?;
let mut mac = HmacSha256::new_from_slice(key).ok()?;
mac.update(canonical(payload).as_bytes());
mac.verify_slice(&want).ok()?;
Some(payload.clone())
}
pub fn universe() -> String {
std::env::var("MENTAT_UNIVERSE")
.ok()
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
.unwrap_or_else(|| "default".to_string())
}
pub fn peek_universe(raw: &[u8]) -> Option<String> {
let v: Value = serde_json::from_slice(raw).ok()?;
let payload = v.get("p").unwrap_or(&v);
Some(payload.get("universe")?.as_str()?.to_string())
}
pub fn fresh(t: f64, now: f64) -> bool {
(now - t).abs() <= CLOCK_SKEW_S
}
pub fn now_s() -> f64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs_f64())
.unwrap_or(0.0)
}
pub fn boot_id() -> String {
let mut buf = [0u8; 8];
if let Ok(mut f) = std::fs::File::open("/dev/urandom") {
use std::io::Read;
if f.read_exact(&mut buf).is_ok() {
return hex(&buf);
}
}
format!("{:x}{:x}", std::process::id(), now_s() as u64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip() {
let key = b"hunter2";
let payload = serde_json::json!({"b": 2, "a": 1});
let env = sign(&payload, key);
assert_eq!(verify(env.as_bytes(), key), Some(payload));
}
#[test]
fn wrong_key_fails() {
let payload = serde_json::json!({"a": 1});
let env = sign(&payload, b"one");
assert_eq!(verify(env.as_bytes(), b"two"), None);
}
#[test]
fn tampered_payload_fails() {
let env = sign(&serde_json::json!({"http": "10.0.0.1:6380"}), b"k");
let bad = env.replace("10.0.0.1", "10.0.0.9");
assert_eq!(verify(bad.as_bytes(), b"k"), None);
}
#[test]
fn unsigned_and_malformed_fail() {
assert_eq!(verify(b"{\"http\":\"x\"}", b"k"), None);
assert_eq!(verify(b"not json", b"k"), None);
assert_eq!(verify(b"{\"p\":{},\"sig\":\"zz\"}", b"k"), None);
}
#[test]
fn canonical_is_key_order_independent() {
let a: Value = serde_json::from_str(r#"{"a":1,"b":2}"#).unwrap();
let b: Value = serde_json::from_str(r#"{"b":2,"a":1}"#).unwrap();
assert_eq!(canonical(&a), canonical(&b));
}
#[test]
fn trailing_newline_in_key_is_ignored() {
assert_eq!(trim_ascii(b" hunter2\n"), b"hunter2");
assert_eq!(trim_ascii(b"\n\n"), b"");
}
#[test]
fn peek_reads_universe_from_both_shapes() {
let signed = sign(&serde_json::json!({"universe": "lab"}), b"k");
assert_eq!(peek_universe(signed.as_bytes()).as_deref(), Some("lab"));
assert_eq!(
peek_universe(br#"{"universe":"lab"}"#).as_deref(),
Some("lab")
);
assert_eq!(peek_universe(b"{}"), None);
}
#[test]
fn peek_does_not_need_the_key() {
let signed = sign(&serde_json::json!({"universe": "lab"}), b"secret");
assert_eq!(peek_universe(signed.as_bytes()).as_deref(), Some("lab"));
assert_eq!(verify(signed.as_bytes(), b"wrong"), None);
}
#[test]
fn announcement_survives_the_listener_path() {
let payload = serde_json::json!({
"mentat_announce": SIGNED_VERSION,
"node_id": "6d656e7461743a3137322e31382e302e33",
"control": "172.18.0.3:6379",
"http": "172.18.0.3:6380",
"universe": "lab",
"boot_id": "6d6474919bbe7beb",
"seq": 159u64,
"t": 1787862155u64,
});
let env = sign(&payload, b"k");
assert_eq!(verify(env.as_bytes(), b"k"), Some(payload));
}
#[test]
fn floats_do_not_survive_the_round_trip() {
let v = serde_json::json!({"t": 1787862155.6581013f64});
let once = v.to_string();
let twice = serde_json::from_str::<Value>(&once).unwrap().to_string();
assert_ne!(once, twice);
assert_eq!(verify(sign(&v, b"k").as_bytes(), b"k"), None);
}
#[test]
fn freshness_window() {
assert!(fresh(1000.0, 1000.0));
assert!(fresh(1000.0, 1029.0));
assert!(!fresh(1000.0, 1031.0));
assert!(!fresh(1000.0, 969.0));
}
}