crates/nornir-vault/src/lib.rs
//! # nornir-vault
//!
//! A **client-side, at-rest-encrypted secrets vault** for the nordisk thin
//! clients (korp first) — the local locked store a thin client keeps its *own*
//! credentials in (an mTLS client cert+key, a Spark token). It is the sibling of
//! the `nornir-auth-*` leaves in edda, and it is **distinct from the server-side
//! OpenBao** (that is infra): this is the *client's* local store. OpenBao can
//! later be one of the key-providers (OIDC → OpenBao releases the key), but the
//! vault does not depend on it.
//!
//! ## Threat model (why this exists)
//!
//! The korp thin client runs inside an RDP-open container that an operator
//! watches from Windows. Anyone at that RDP session can read the container
//! filesystem. If korp's mTLS key / Spark token sat there in plaintext, the
//! session would be wide open. The vault's job is modest and concrete: keep those
//! credentials **encrypted at rest** so an idle or naive RDP viewer cannot lift
//! them off disk — "not too open", not a fortress.
//!
//! ## Envelope encryption
//!
//! ```text
//! PIN ──Argon2id(salt)──▶ KEK ──unwraps──▶ DEK ──AES-256-GCM──▶ entries
//! (or OS keyring ─fetch─▶ KEK) (random, per-vault) (cert/key, token)
//! ```
//!
//! * A random **DEK** seals each entry with AES-256-GCM (see [`crypto`] for the
//! cipher choice and nonce discipline).
//! * The DEK is wrapped by a **KEK** produced by a pluggable [`VaultKeyProvider`]
//! chosen by [`AuthMethod`]. [`PinProvider`] (Argon2id) and, behind the
//! `os-keyring` feature, [`provider::OsKeyringProvider`] (Secret Service) are
//! implemented; OIDC→OpenBao / TPM / FIDO2 are design-only stubs that slot into
//! the same trait.
//!
//! ## The invariants this crate keeps
//!
//! * **Unlock = file → memory.** [`Vault::unlock`] decrypts into RAM; plaintext
//! is *never* written back to disk.
//! * **Secrets are zeroized** ([`SecretString`], [`SecretEntry`]) on drop /
//! [`Vault::lock`].
//! * **Secrets never leak.** No secret type implements `Serialize`, a revealing
//! `Debug`, or `Display`. The only UI-facing view is [`VaultState`] — lock
//! flags, method, attempts-remaining, and entry *names + kinds*; **never** a
//! secret byte. Put only that in a `state_json`.
//! * **Low-entropy PIN backstop.** A persisted [`header::Lockout`] counter caps
//! failed unlock attempts.
//!
//! ## Quick start
//!
//! ```
//! use nornir_vault::{Vault, PinProvider, SecretEntry, SecretString};
//! # fn main() -> Result<(), nornir_vault::VaultError> {
//! # let dir = std::env::temp_dir().join(format!("nv-doctest-{}", std::process::id()));
//! # std::fs::create_dir_all(&dir).unwrap();
//! let path = dir.join("korp.vault");
//!
//! // Create (unlocked) and store a Spark token.
//! let mut v = Vault::create(&path, &PinProvider::new("4913"))?;
//! v.set("spark", SecretEntry::SparkToken(SecretString::new("s3cr3t-token")))?;
//! v.lock();
//!
//! // Re-open and unlock with the PIN.
//! let mut v = Vault::open(&path)?;
//! v.unlock(&PinProvider::new("4913"))?;
//! match v.get("spark")? {
//! Some(SecretEntry::SparkToken(t)) => assert_eq!(t.expose(), "s3cr3t-token"),
//! _ => panic!("expected the token"),
//! }
//! # std::fs::remove_dir_all(&dir).ok();
//! # Ok(())
//! # }
//! ```
#![forbid(unsafe_code)]
#![warn(missing_docs)]
pub mod crypto;
pub mod error;
pub mod header;
pub mod provider;
pub mod secret;
pub mod vault;
pub use error::{Result, VaultError};
pub use provider::{
AuthMethod, CachedKekProvider, Fido2Provider, HostKeyfileProvider, Kek, OidcOpenBaoProvider,
PinProvider, ProviderParams, VaultKeyProvider,
};
pub use secret::{EntryKind, SecretEntry, SecretString};
pub use vault::{EntryPresence, HolderPresence, Vault, VaultState};
/// The hardware slot, behind the `tpm` feature.
#[cfg(feature = "tpm")]
pub use provider::TpmProvider;
#[cfg(feature = "os-keyring")]
pub use provider::OsKeyringProvider;