envseal 0.3.14

Write-only secret vault with process-level access control — post-agent secret management
//! Shared hex encoding utility.
//!
//! Single shared implementation to avoid duplicate `mod hex` blocks
//! across modules (`guard::binary`, `config::tiers`, etc.).

use std::fmt::Write;

/// Encode bytes as a lowercase hex string.
pub fn encode(bytes: impl AsRef<[u8]>) -> String {
    let data = bytes.as_ref();
    let mut s = String::with_capacity(data.len() * 2);
    for b in data {
        let _ = write!(s, "{b:02x}");
    }
    s
}

/// Hard cap on inputs accepted by [`decode`]. 8 MiB of hex decodes
/// to 4 MiB of bytes — comfortably larger than any legitimate
/// envseal blob (sealed events, FIDO2 envelopes) while preventing
/// a hostile log line from forcing the reader into a multi-GiB
/// allocation. Callers that need higher limits should validate the
/// length themselves and pre-shrink before invoking the decoder.
const DECODE_MAX_LEN: usize = 8 * 1024 * 1024;

/// Decode a lowercase or mixed-case hex string into bytes. Returns
/// `None` on odd length, on non-hex digits, or on inputs larger
/// than [`DECODE_MAX_LEN`].
#[must_use]
pub fn decode(s: &str) -> Option<Vec<u8>> {
    if s.len() % 2 != 0 || s.len() > DECODE_MAX_LEN {
        return None;
    }
    let bytes = s.as_bytes();
    let mut out = Vec::with_capacity(s.len() / 2);
    for i in (0..bytes.len()).step_by(2) {
        let hi = nibble(bytes[i])?;
        let lo = nibble(bytes[i + 1])?;
        out.push((hi << 4) | lo);
    }
    Some(out)
}

fn nibble(b: u8) -> Option<u8> {
    match b {
        b'0'..=b'9' => Some(b - b'0'),
        b'a'..=b'f' => Some(b - b'a' + 10),
        b'A'..=b'F' => Some(b - b'A' + 10),
        _ => None,
    }
}