envseal 0.3.6

Write-only secret vault with process-level access control — post-agent secret management
Documentation
//! Error types for envseal.

use std::fmt;

/// All errors that can occur in envseal operations.
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
    /// Secret with the given name was not found in the vault.
    SecretNotFound(String),
    /// Secret with the given name already exists.
    SecretAlreadyExists(String),
    /// Encryption or decryption failed.
    CryptoFailure(String),
    /// Failed to read or write vault files.
    StorageIo(std::io::Error),
    /// Policy file could not be parsed.
    PolicyParse(String),
    /// The target binary is not whitelisted for this secret.
    AccessDenied {
        /// Name of the secret that was requested.
        secret_name: String,
        /// Path to the binary that was denied.
        binary_path: String,
    },
    /// The user denied access via the GUI popup.
    UserDenied,
    /// No GUI display available (headless environment).
    NoDisplay,
    /// Failed to resolve the binary path.
    BinaryResolution(String),
    /// Failed to execute the child process.
    ExecFailed(std::io::Error),
    /// Hostile environment variables detected (`LD_PRELOAD`, etc.).
    EnvironmentCompromised(String),
    /// Binary hash does not match the stored hash (replaced or corrupted).
    BinaryTampered {
        /// Filesystem path to the binary.
        binary_path: String,
        /// Hash stored in policy at time of approval.
        expected_hash: String,
        /// Hash computed from the binary on disk right now.
        actual_hash: String,
    },
    /// Policy file HMAC signature is invalid (tampering detected).
    PolicyTampered(String),
    /// Append-only audit log could not be written — operation aborted to preserve integrity.
    AuditLogFailed(String),
    /// Device-bound hardware seal (DPAPI / Secure Enclave / TPM 2.0) could
    /// not wrap or unwrap a master-key envelope. The detail string carries
    /// the platform-specific cause (`GetLastError` value, NTSTATUS,
    /// `tpm2-tools` stderr, or Security.framework error string).
    ///
    /// Distinct from [`Error::CryptoFailure`] because the recovery path
    /// is different: hardware-seal errors are usually device-state
    /// problems (TPM owner cleared, keychain reset, user-logon
    /// changed), not corruption.
    HardwareSealFailed(String),
    /// A vault file is sealed by one hardware backend but the active
    /// device is using a different backend. This is the precise
    /// signal that someone has copied `master.key` to another machine.
    DeviceMismatch {
        /// Backend that produced the on-disk envelope (e.g. `Windows DPAPI`).
        sealed_by: String,
        /// Backend currently available on this device (e.g. `macOS Secure Enclave`).
        active: String,
    },
    /// Approval relay was configured as required but failed to deliver
    /// an Allow/Deny decision. Detail carries the underlying cause.
    /// Holding this distinct from [`Error::CryptoFailure`] makes
    /// fail-closed behavior auditable in logs.
    RelayRequiredButUnavailable(String),
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::SecretNotFound(name) => {
                write!(f, "secret not found in vault: {name}")
            }
            Self::SecretAlreadyExists(name) => {
                write!(
                    f,
                    "secret already exists: {name} (use --force to overwrite)"
                )
            }
            Self::CryptoFailure(detail) => {
                write!(f, "cryptographic operation failed: {detail}")
            }
            Self::StorageIo(err) => {
                write!(f, "vault storage i/o error: {err}")
            }
            Self::PolicyParse(detail) => {
                write!(f, "failed to parse policy file: {detail}")
            }
            Self::AccessDenied {
                secret_name,
                binary_path,
            } => {
                write!(
                    f,
                    "access denied: {binary_path} is not authorized to access secret '{secret_name}'"
                )
            }
            Self::UserDenied => {
                write!(f, "access denied: user rejected the approval request")
            }
            Self::NoDisplay => {
                write!(
                    f,
                    "no display server available — envseal requires a GUI session for approval"
                )
            }
            Self::BinaryResolution(detail) => {
                write!(f, "failed to resolve binary path: {detail}")
            }
            Self::ExecFailed(err) => {
                write!(f, "failed to execute child process: {err}")
            }
            Self::EnvironmentCompromised(detail) => {
                write!(f, "environment compromised — refusing to inject: {detail}")
            }
            Self::BinaryTampered {
                binary_path,
                expected_hash,
                actual_hash,
            } => {
                write!(
                    f,
                    "binary tampered: {binary_path} hash mismatch — \
                     expected {expected_hash}, got {actual_hash}. \
                     the binary may have been replaced since it was approved."
                )
            }
            Self::PolicyTampered(detail) => {
                write!(
                    f,
                    "policy file tampered: HMAC signature verification failed — {detail}. \
                     the policy file may have been modified by an unauthorized process."
                )
            }
            Self::AuditLogFailed(detail) => {
                write!(f, "audit log write failed — refusing to continue: {detail}")
            }
            Self::HardwareSealFailed(detail) => {
                write!(
                    f,
                    "hardware-bound key seal failed (DPAPI / Secure Enclave / TPM 2.0): {detail}"
                )
            }
            Self::DeviceMismatch { sealed_by, active } => {
                write!(
                    f,
                    "vault was sealed on a different device (backend: {sealed_by}); \
                     this machine uses {active}. master.key cannot move between machines — \
                     re-import the secrets here with `envseal import`."
                )
            }
            Self::RelayRequiredButUnavailable(detail) => {
                write!(
                    f,
                    "approval relay is required by policy but did not respond — \
                     refusing to fall back to local GUI: {detail}"
                )
            }
        }
    }
}

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::StorageIo(err) | Self::ExecFailed(err) => Some(err),
            _ => None,
        }
    }
}

impl From<std::io::Error> for Error {
    fn from(err: std::io::Error) -> Self {
        Self::StorageIo(err)
    }
}