ai-model-vault 4.6.1

DEPRECATED - renamed to `ironvault`. This crate is no longer maintained; 4.6.1 is identical to 4.6.0 apart from this notice.
Documentation
//! CLI helper utilities.

use ai_model_vault::{kms, Result, Vault, VaultBuilder, VaultConfig};
use std::io::{self, BufRead, IsTerminal, Write};
use zeroize::Zeroize;

/// Environment variable holding the vault passphrase for unattended use.
///
/// The value is either the passphrase itself or a KMS URI
/// (`env://`, `file://`, `aws-sm://`, `azure-kv://`, `vault://`) — see
/// [`ai_model_vault::kms`].
pub const PASSPHRASE_ENV: &str = "aimodelvault_PASSPHRASE";

/// Obtain the vault passphrase, in descending order of precedence:
///
/// 1. `$aimodelvault_PASSPHRASE` — a literal value or a KMS URI to resolve.
/// 2. A line piped on stdin, when stdin is not a terminal.
/// 3. An interactive masked prompt.
///
/// Steps 1 and 2 make every passphrase-gated command usable from CI and from
/// agents; step 3 preserves the interactive behaviour for humans.
///
/// Every intermediate buffer that holds the plaintext is zeroized before it is
/// dropped. Each of the three paths used to leave a copy behind: `kms::resolve`
/// returns an owned `String`, `read_line` fills one, and `trim_end_matches` +
/// `to_vec` copies out of it — so the secret was freed to the allocator intact
/// and could resurface in a later allocation or a core dump (MITRE ATT&CK
/// T1552, credentials in process memory). The returned buffer is the caller's
/// responsibility; it is consumed by `derive_key`, which zeroizes it.
pub fn prompt_passphrase(prompt: &str) -> Result<Vec<u8>> {
    if let Ok(mut value) = std::env::var(PASSPHRASE_ENV) {
        if !value.is_empty() {
            let resolved = kms::resolve(&value);
            value.zeroize();
            let mut resolved = resolved?;
            let bytes = resolved.as_bytes().to_vec();
            resolved.zeroize();
            return Ok(bytes);
        }
    }

    let stdin = io::stdin();
    if !stdin.is_terminal() {
        let mut line = String::new();
        // A closed/empty stdin is not a passphrase — fall through to the prompt
        // rather than silently unlocking with "".
        let read = stdin.lock().read_line(&mut line);
        match read {
            Ok(n) if n > 0 => {
                let trimmed = line.trim_end_matches(['\n', '\r']);
                if !trimmed.is_empty() {
                    let bytes = trimmed.as_bytes().to_vec();
                    line.zeroize();
                    return Ok(bytes);
                }
                line.zeroize();
            }
            Ok(_) => line.zeroize(),
            Err(err) => {
                line.zeroize();
                return Err(err.into());
            }
        }
    }

    print!("{}", prompt);
    io::stdout().flush()?;

    let passphrase = rpassword::read_password()?;
    if passphrase.is_empty() {
        // A closed or non-interactive stdin reads as "" here. Deriving a key
        // from an empty passphrase would silently unlock the vault with no
        // secret at all, so refuse it.
        return Err(ai_model_vault::VaultError::InvalidInput(format!(
            "No passphrase provided. Set ${PASSPHRASE_ENV} (a literal value or a \
             KMS URI), pipe it on stdin, or run interactively."
        )));
    }
    // `String::into_bytes` hands over the same allocation rather than copying,
    // so there is no second buffer to clear on this path.
    Ok(passphrase.into_bytes())
}

/// Build a [`Vault`] using the standard builder, optionally enabling SQLite
/// version storage when `use_sqlite` is true.
///
/// All CLI handlers should use this instead of `Vault::new()` directly.
pub fn build_vault(config: VaultConfig, use_sqlite: bool) -> Result<Vault> {
    let mut builder = VaultBuilder::new().config(config);

    if use_sqlite {
        #[cfg(feature = "sqlite")]
        {
            builder = builder.sqlite_versions();
        }
        #[cfg(not(feature = "sqlite"))]
        {
            return Err(ai_model_vault::VaultError::ConfigError(
                "SQLite version backend requires the `sqlite` feature".to_string(),
            ));
        }
    }

    builder.build()
}