crates/nornir-vault/src/error.rs
//! The one error type for the vault.
//!
//! Every variant is deliberately *coarse* and carries **no secret material** and
//! no oracle: an unlock that fails because the PIN was wrong and an unlock that
//! fails because the ciphertext was corrupted both surface as
//! [`VaultError::Unlock`] — the caller learns "it did not open", never *why* in a
//! way that could be turned into a guessing oracle. The `Debug`/`Display` of this
//! type is safe to log.
use crate::provider::AuthMethod;
/// The result alias used across the crate.
pub type Result<T> = core::result::Result<T, VaultError>;
/// Everything that can go wrong opening, unlocking, or mutating a vault.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum VaultError {
/// The on-disk vault file could not be read/written, or its directory could
/// not be created / `chmod`ed.
#[error("vault i/o: {0}")]
Io(#[from] std::io::Error),
/// The header could not be (de)serialised — a truncated or foreign file. This
/// is a *format* failure, distinct from an authentication failure.
#[error("vault header is not valid JSON / not a vault file")]
Format(#[from] serde_json::Error),
/// A base64 blob in the header did not decode.
#[error("vault header contains a malformed binary field")]
Base64,
/// Unlock failed. Wrong credential OR tampered/corrupt ciphertext — the
/// variant is intentionally indistinguishable (no guessing oracle). The
/// attached counter is how many attempts remain before the vault locks; `0`
/// means this failure just tripped the lockout.
#[error("vault did not unlock ({remaining} attempt(s) remaining before lockout)")]
Unlock {
/// Attempts left before [`VaultError::LockedOut`]. Never leaks the reason.
remaining: u32,
},
/// The vault is locked out: too many failed unlock attempts. It will not
/// accept another attempt until the lockout is reset out-of-band (a fresh
/// vault / operator reset). This is the low-entropy-PIN backstop.
#[error("vault is locked out after too many failed attempts")]
LockedOut,
/// A read/write of secret entries was attempted while the vault is still
/// locked (no DEK in memory). Call `unlock` first.
#[error("vault is locked — unlock before reading or writing entries")]
Locked,
/// The provider handed to `unlock`/`create` does not match how this vault was
/// provisioned (e.g. a `PinProvider` against an `OsKeyring` header).
#[error("auth-method mismatch: vault is {vault:?}, provider offered {provider:?}")]
MethodMismatch {
/// The method the vault header was provisioned with.
vault: AuthMethod,
/// The method the supplied provider speaks.
provider: AuthMethod,
},
/// A design-only provider (OIDC→OpenBao, TPM, FIDO2) was invoked. These are
/// declared so they *slot in* to the trait, but their key material flow is
/// not implemented — see `provider.rs`.
#[error("key provider for {0:?} is not implemented yet (design-only stub)")]
ProviderUnimplemented(AuthMethod),
/// A key-provider-specific failure that is not itself a secret (e.g. the OS
/// keyring is unavailable, or Argon2 rejected the parameters).
#[error("key provider error: {0}")]
Provider(String),
}