envseal 0.3.11

Write-only secret vault with process-level access control — post-agent secret management
Documentation
//! HMAC framing for the on-disk policy file.
//!
//! The signature is appended as a TOML comment: `# hmac = "<hex>"`. The
//! comment form means the file remains valid TOML, but [`split_hmac`]
//! extracts and re-attaches it on save/load.
//!
//! [`compute_hmac`] uses an HKDF-derived key (per [`crate::keychain::derive_hmac_key`])
//! so the master key is never reused as an HMAC key directly.

use std::fmt::Write as _;

use ::hmac::{Hmac, Mac};
use sha2::Sha256;

use crate::error::Error;

type HmacSha256 = Hmac<Sha256>;

/// Compute HMAC-SHA256 of `data` using the HKDF-derived policy HMAC key,
/// returned as a lowercase hex string.
///
/// # Errors
/// Returns [`Error::CryptoFailure`] if the HMAC engine fails to initialize
/// (effectively unreachable for valid 32-byte keys).
pub fn compute_hmac(data: &[u8], master_key: &[u8; 32]) -> Result<String, Error> {
    let hmac_key = crate::keychain::derive_hmac_key(master_key)?;
    let mut mac = HmacSha256::new_from_slice(&hmac_key)
        .map_err(|e| Error::CryptoFailure(format!("HMAC init failed (unexpected): {e}")))?;
    mac.update(data);
    let bytes = mac.finalize().into_bytes();

    let mut hex = String::with_capacity(bytes.len() * 2);
    for b in &bytes {
        let _ = write!(hex, "{b:02x}");
    }
    Ok(hex)
}

/// Split file content into (`toml_content`, `optional_hmac_hex`).
///
/// The HMAC line format is `# hmac = "hexstring"` and must be the last
/// non-empty line in the file.
#[must_use]
pub fn split_hmac(content: &str) -> (&str, Option<String>) {
    for line in content.lines().rev() {
        let trimmed = line.trim();
        if trimmed.starts_with("# hmac = \"") && trimmed.ends_with('"') {
            let hmac_hex = &trimmed["# hmac = \"".len()..trimmed.len() - 1];
            if let Some(pos) = content.rfind(line) {
                let toml_part = content[..pos].trim_end();
                return (toml_part, Some(hmac_hex.to_string()));
            }
        }
    }
    (content, None)
}