envseal 0.3.11

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),
    /// Vault is enrolled with a FIDO2 authenticator but no
    /// authenticator was supplied to the unlock path. The caller is
    /// expected to attach one (CLI: physical key prompt; tests: mock
    /// backend) and retry.
    Fido2Required,
    /// FIDO2 authenticator was supplied but the assertion failed —
    /// wrong credential, no user-presence touch, PIN required, or
    /// device communication error. Detail carries the cause from the
    /// authenticator backend.
    Fido2AssertionFailed(String),
    /// Caller attempted to disable / change FIDO2 enrollment on a
    /// vault that has no enrollment. Distinct from `CryptoFailure` so
    /// CLI surfaces a clean "vault has no FIDO2 enrollment" instead
    /// of a generic crypto error.
    Fido2NotEnrolled,
}

impl fmt::Display for Error {
    #[allow(clippy::too_many_lines)] // long match over a wide variant set, no logic
    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}"
                )
            }
            Self::Fido2Required => {
                write!(
                    f,
                    "vault is enrolled with a FIDO2 authenticator but none was supplied — \
                     attach your security key and retry"
                )
            }
            Self::Fido2AssertionFailed(detail) => {
                write!(
                    f,
                    "FIDO2 authenticator assertion failed (wrong key, missing touch, \
                     or device error): {detail}"
                )
            }
            Self::Fido2NotEnrolled => {
                write!(
                    f,
                    "vault has no FIDO2 authenticator enrolled — \
                     run `envseal security fido2-enroll` first"
                )
            }
        }
    }
}

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)
    }
}