age-vault 0.1.0

A secure vault for managing age-encrypted accounts and data.
Documentation
//! Main vault implementation.
//!
//! The [`Vault`] struct provides the core functionality: creating/opening a vault,
//! managing accounts, and encrypting/decrypting data using age recipients.

use crate::account::{Account, Role};
use crate::crypto;
use crate::error::{Error, Result};
use crate::store;
use neuxdb::Database;
use rand::RngCore;
use secrecy::{ExposeSecret, SecretString};
use std::path::PathBuf;
use uuid::Uuid;

/// The main vault structure.
///
/// A vault holds an encrypted database containing accounts and metadata.
/// It manages a Key Encryption Key (KEK) derived from the master password,
/// which is used to protect the age secret keys stored inside accounts.
///
/// # Thread Safety
///
/// `Vault` is `Send` and `Sync` if the underlying `Database` is `Send` and `Sync`.
/// However, note that mutable operations require exclusive `&mut self` access.
///
/// # Examples
///
/// ```rust
/// use age_vault::{Vault, Role};
/// # use std::path::PathBuf;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let path = PathBuf::from("./my_vault.ndbx");
/// let master = "super_secret";
///
/// // Create a new vault
/// let mut vault = Vault::create(&path, master)?;
///
/// // Add an account
/// let alice = vault.add_account("alice", Role::Admin)?;
///
/// // Encrypt a message for Alice
/// let msg = b"hello, world!";
/// let cipher = vault.encrypt_for(&["alice"], msg)?;
///
/// // Decrypt the message using Alice's key
/// let plain = vault.decrypt_with("alice", &cipher)?;
/// assert_eq!(msg, &plain[..]);
///
/// // Clean up
/// std::fs::remove_file(path)?;
/// # Ok(())
/// # }
/// ```
pub struct Vault {
    db: Database,
    kek: SecretString,
    salt: Vec<u8>,
}

impl Vault {
    /// Creates a new vault at the specified path with the given master password.
    ///
    /// This function initializes a new database, generates a random salt,
    /// derives a KEK from the password and salt, and creates the necessary tables.
    ///
    /// # Parameters
    ///
    /// * `db_path` - Filesystem path where the database will be stored. Must have `.ndbx` extension.
    /// * `master_password` - The master password used to derive the KEK. Must be at least 8 characters.
    ///
    /// # Returns
    ///
    /// A new `Vault` instance.
    ///
    /// # Errors
    ///
    /// Returns [`Error::Db`] if database creation fails (e.g., password too weak).
    /// Returns [`Error::Kdf`] if key derivation fails.
    /// Returns [`Error::Io`] or other errors from underlying stores.
    ///
    /// # Panics
    ///
    /// This function does not panic.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use age_vault::Vault;
    /// # use std::path::PathBuf;
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let vault = Vault::create("./vault.ndbx", "my_password")?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn create(db_path: impl Into<PathBuf>, master_password: &str) -> Result<Self> {
        let db = Database::create(db_path.into(), master_password)?;
        let mut salt = vec![0u8; 16];
        rand::thread_rng().fill_bytes(&mut salt);
        let kek = crypto::derive_kek(master_password, &salt)?;
        let mut vault = Self { db, kek, salt };
        store::init_accounts_table(&mut vault.db)?;
        store::init_metadata_table(&mut vault.db)?;
        store::save_metadata_salt(&mut vault.db, &vault.salt)?;
        vault.db.commit()?;
        Ok(vault)
    }

    /// Opens an existing vault at the specified path.
    ///
    /// This function loads the salt from the metadata table, derives the KEK from
    /// the provided master password, and initializes the accounts table.
    ///
    /// # Parameters
    ///
    /// * `db_path` - Path to the existing database file. Must have `.ndbx` extension.
    /// * `master_password` - The master password (must match the one used during creation).
    ///
    /// # Returns
    ///
    /// A `Vault` instance ready for operations.
    ///
    /// # Errors
    ///
    /// Returns [`Error::Db`] if opening the database fails.
    /// Returns [`Error::DecryptionFailed`] if the salt cannot be loaded.
    /// Returns [`Error::Kdf`] if key derivation fails.
    /// Returns [`Error::InvalidMasterPassword`] if the KEK derivation yields
    /// incorrect results (detected indirectly by later decryption failures).
    ///
    /// # Panics
    ///
    /// This function does not panic.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use age_vault::Vault;
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let vault = Vault::open("./vault.ndbx", "my_password")?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn open(db_path: impl Into<PathBuf>, master_password: &str) -> Result<Self> {
        let mut db = Database::open(db_path.into(), master_password)?;
        store::init_metadata_table(&mut db)?;
        let salt = store::load_metadata_salt(&db)?;
        let kek = crypto::derive_kek(master_password, &salt)?;
        let mut vault = Self { db, kek, salt };
        store::init_accounts_table(&mut vault.db)?;
        Ok(vault)
    }

    /// Adds a new account to the vault.
    ///
    /// Generates a new age keypair, encrypts the secret key with the vault's KEK,
    /// and stores the account in the database.
    ///
    /// # Parameters
    ///
    /// * `name` - Unique account name.
    /// * `role` - Role assigned to the account.
    ///
    /// # Returns
    ///
    /// The newly created `Account`.
    ///
    /// # Errors
    ///
    /// Returns [`Error::AccountExists`] if an account with the same name already exists.
    /// Returns [`Error::KeyGen`] if keypair generation fails.
    /// Returns [`Error::Crypto`] if encryption of the secret key fails.
    /// Returns [`Error::Db`] or serialization errors from storage.
    ///
    /// # Panics
    ///
    /// This function does not panic.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use age_vault::{Vault, Role};
    /// # use std::path::PathBuf;
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let path = PathBuf::from("./test_vault_add.ndbx");
    /// let mut vault = Vault::create(&path, "master123")?;
    /// let account = vault.add_account("bob", Role::User)?;
    /// assert_eq!(account.name, "bob");
    /// std::fs::remove_file(path)?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn add_account(&mut self, name: &str, role: Role) -> Result<Account> {
        if self.get_account_by_name(name).is_ok() {
            return Err(Error::AccountExists(name.into()));
        }
        let keypair = age_setup::build_keypair()?;
        let public_key = keypair.public.expose().to_string();
        let secret_key = keypair.secret.expose_secret().to_string();
        let encrypted_secret_key =
            crypto::encrypt_secret_key(&secret_key, self.kek.expose_secret())?;
        let account = Account {
            id: Uuid::new_v4().to_string(),
            name: name.to_string(),
            role,
            public_key,
            encrypted_secret_key,
            enabled: true,
        };
        store::save_account(&mut self.db, &account)?;
        Ok(account)
    }

    /// Removes an account by name.
    ///
    /// Deletes the account from the database.
    ///
    /// # Parameters
    ///
    /// * `name` - Name of the account to remove.
    ///
    /// # Errors
    ///
    /// Returns [`Error::AccountNotFound`] if the account does not exist.
    /// Returns [`Error::Db`] if deletion or commit fails.
    ///
    /// # Panics
    ///
    /// This function may panic if the database row does not contain an ID column
    /// at index 0 (should not happen with a consistent database).
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use age_vault::{Vault, Role};
    /// # use std::path::PathBuf;
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let path = PathBuf::from("./test_vault_remove.ndbx");
    /// let mut vault = Vault::create(&path, "master123")?;
    /// vault.add_account("charlie", Role::User)?;
    /// vault.remove_account("charlie")?;
    /// let accounts = vault.list_accounts()?;
    /// assert!(accounts.iter().all(|a| a.name != "charlie"));
    /// std::fs::remove_file(path)?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn remove_account(&mut self, name: &str) -> Result<()> {
        let account = self.get_account_by_name(name)?;
        self.db
            .delete("accounts", &|row| row[0].to_string() == account.id)?;
        self.db.commit()?;
        Ok(())
    }

    /// Lists all accounts in the vault.
    ///
    /// # Returns
    ///
    /// A vector of all `Account` structs currently stored.
    ///
    /// # Errors
    ///
    /// Returns [`Error::Db`] or deserialization errors from storage.
    ///
    /// # Panics
    ///
    /// This function does not panic.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use age_vault::{Vault, Role};
    /// # use std::path::PathBuf;
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let path = PathBuf::from("./test_vault_list.ndbx");
    /// let mut vault = Vault::create(&path, "master123")?;
    /// vault.add_account("alice", Role::Admin)?;
    /// vault.add_account("bob", Role::User)?;
    /// let accounts = vault.list_accounts()?;
    /// assert_eq!(accounts.len(), 2);
    /// std::fs::remove_file(path)?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn list_accounts(&self) -> Result<Vec<Account>> {
        store::load_accounts(&self.db)
    }

    /// Encrypts plaintext for a set of recipient accounts.
    ///
    /// Each recipient is identified by account name; the function collects their
    /// public keys and encrypts the plaintext using age's multi-recipient encryption.
    ///
    /// # Parameters
    ///
    /// * `account_names` - Slice of account names that should be able to decrypt.
    /// * `plaintext` - Bytes to encrypt.
    ///
    /// # Returns
    ///
    /// Ciphertext as `Vec<u8>`.
    ///
    /// # Errors
    ///
    /// Returns [`Error::AccountNotFound`] if any of the named accounts does not exist.
    /// Returns [`Error::Crypto`] if the age encryption fails.
    ///
    /// # Panics
    ///
    /// This function does not panic.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use age_vault::{Vault, Role};
    /// # use std::path::PathBuf;
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let path = PathBuf::from("./test_vault_encrypt.ndbx");
    /// let mut vault = Vault::create(&path, "master123")?;
    /// vault.add_account("alice", Role::Admin)?;
    /// let data = b"top secret";
    /// let cipher = vault.encrypt_for(&["alice"], data)?;
    /// std::fs::remove_file(path)?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn encrypt_for(&self, account_names: &[&str], plaintext: &[u8]) -> Result<Vec<u8>> {
        let recipients: Vec<String> = account_names
            .iter()
            .map(|n| {
                let acc = self.get_account_by_name(n)?;
                Ok(acc.public_key)
            })
            .collect::<Result<Vec<_>>>()?;
        let recipient_strs: Vec<&str> = recipients.iter().map(|s| s.as_str()).collect();
        Ok(age_crypto::encrypt(plaintext, &recipient_strs)?.to_vec())
    }

    /// Decrypts ciphertext using the specified account's secret key.
    ///
    /// The account's encrypted secret key is decrypted using the vault's KEK,
    /// then used to decrypt the ciphertext.
    ///
    /// # Parameters
    ///
    /// * `account_name` - Name of the account whose secret key will be used.
    /// * `ciphertext` - Encrypted bytes (produced by `encrypt_for` targeting this account).
    ///
    /// # Returns
    ///
    /// Decrypted plaintext as `Vec<u8>`.
    ///
    /// # Errors
    ///
    /// Returns [`Error::AccountNotFound`] if the account does not exist.
    /// Returns [`Error::DecryptionFailed`] if the secret key decryption or
    /// age decryption fails (e.g., wrong account or corrupted ciphertext).
    ///
    /// # Panics
    ///
    /// This function does not panic.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use age_vault::{Vault, Role};
    /// # use std::path::PathBuf;
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let path = PathBuf::from("./test_vault_decrypt.ndbx");
    /// let mut vault = Vault::create(&path, "master123")?;
    /// vault.add_account("alice", Role::Admin)?;
    /// let original = b"secret message";
    /// let cipher = vault.encrypt_for(&["alice"], original)?;
    /// let decrypted = vault.decrypt_with("alice", &cipher)?;
    /// assert_eq!(original, &decrypted[..]);
    /// std::fs::remove_file(path)?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn decrypt_with(&self, account_name: &str, ciphertext: &[u8]) -> Result<Vec<u8>> {
        let acc = self.get_account_by_name(account_name)?;
        let secret_key =
            crypto::decrypt_secret_key(&acc.encrypted_secret_key, self.kek.expose_secret())?;
        age_crypto::decrypt(ciphertext, &secret_key)
            .map_err(|e| Error::DecryptionFailed(e.to_string()))
    }

    /// Retrieves an account by its name (internal helper).
    fn get_account_by_name(&self, name: &str) -> Result<Account> {
        let accounts = store::load_accounts(&self.db)?;
        accounts
            .into_iter()
            .find(|a| a.name == name)
            .ok_or_else(|| Error::AccountNotFound(name.to_string()))
    }
}