agentguard_core/
chain_secret.rs

1//! Chain secret decoding helpers shared across the CLI and any
2//! embedder that consumes the `AGENTGUARD_CHAIN_SECRET` file.
3//!
4//! The chain secret on disk can be in one of three formats:
5//!
6//! 1. **Hex**: 64 ASCII hex characters (32 raw bytes).
7//! 2. **Base64**: standard base64 (with padding) of 32+ bytes.
8//! 3. **Raw bytes**: any other byte sequence is passed through as-is.
9//!
10//! The CLI, the server, the doctor command, and the delegate command
11//! all used to inline this logic. The five copies drifted over time
12//! (one called `base64::engine::general_purpose::URL_SAFE_NO_PAD`,
13//! the others `STANDARD`); this module is the single source of truth.
14
15use base64::Engine as _;
16
17/// Decode a chain secret file. Tries hex (64 ASCII chars) and
18/// base64 (standard alphabet) in turn; falls back to raw bytes.
19///
20/// # Errors
21/// Returns `None` for an empty input — empty is never a valid
22/// HMAC key. Returns `Some(Vec<u8>)` for any non-empty input,
23/// decoded as best as possible.
24pub fn decode(bytes: &[u8]) -> Option<Vec<u8>> {
25    if bytes.is_empty() {
26        return None;
27    }
28    if let Ok(s) = std::str::from_utf8(bytes) {
29        let s = s.trim();
30        if s.len() == 64 && s.chars().all(|c| c.is_ascii_hexdigit()) {
31            if let Ok(b) = hex::decode(s) {
32                return Some(b);
33            }
34        }
35        if let Ok(b) = base64::engine::general_purpose::STANDARD.decode(s) {
36            return Some(b);
37        }
38    }
39    Some(bytes.to_vec())
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45
46    #[test]
47    fn empty_is_none() {
48        assert_eq!(decode(b""), None);
49    }
50
51    #[test]
52    fn hex_64_chars() {
53        let hex = "0".repeat(64);
54        let out = decode(hex.as_bytes()).unwrap();
55        assert_eq!(out.len(), 32);
56        assert!(out.iter().all(|&b| b == 0));
57    }
58
59    #[test]
60    fn base64_padded() {
61        let raw = [42u8; 32];
62        let b64 = base64::engine::general_purpose::STANDARD.encode(raw);
63        let out = decode(b64.as_bytes()).unwrap();
64        assert_eq!(out, raw);
65    }
66
67    #[test]
68    fn raw_bytes_passthrough() {
69        let raw = [1u8, 2, 3, 4];
70        let out = decode(&raw).unwrap();
71        assert_eq!(out, raw);
72    }
73
74    #[test]
75    fn trims_whitespace() {
76        let raw = [7u8; 32];
77        let b64 = format!(
78            "  {}  ",
79            base64::engine::general_purpose::STANDARD.encode(raw)
80        );
81        let out = decode(b64.as_bytes()).unwrap();
82        assert_eq!(out, raw);
83    }
84}