envseal 0.3.10

Write-only secret vault with process-level access control — post-agent secret management
Documentation
//! Configuration-administration operations: security tier presets,
//! granular field setters, detection→policy overrides, and TOTP
//! pairing.
//!
//! Lives as a sibling of [`super`] (the main `ops` module) so the
//! 70-function ops facade stays organized along clear domain
//! boundaries — anything an operator does to the *vault's
//! configuration* (vs. its secrets) is here.
//!
//! Re-exported from [`super`] for backwards compatibility, so
//! `envseal::ops::security_config_get`, `envseal::ops::totp_setup`
//! etc. continue to resolve unchanged.

use crate::error::Error;
use crate::vault::Vault;
use crate::{audit, gui, security_config, totp};

/// Load security config from the default vault. Opens the vault
/// (prompts for passphrase) — for callers that already hold an
/// unlocked [`Vault`] handle, prefer [`security_config_get_in`] to
/// avoid a redundant prompt.
pub fn security_config_get() -> Result<security_config::SecurityConfig, Error> {
    let vault = Vault::open_default()?;
    security_config_get_in(&vault)
}

/// Load security config using an already-unlocked [`Vault`] handle.
/// Used by the desktop GUI worker so loading the Settings tab
/// doesn't re-trigger a platform passphrase popup on every refresh.
pub fn security_config_get_in(vault: &Vault) -> Result<security_config::SecurityConfig, Error> {
    security_config::load_config(vault.root(), vault.master_key_bytes())
}

/// Persist security config to the default vault. Opens the vault
/// (prompts for passphrase). Prefer [`security_config_save_in`] when
/// you already hold an unlocked handle.
pub fn security_config_save(config: &security_config::SecurityConfig) -> Result<(), Error> {
    let vault = Vault::open_default()?;
    security_config_save_in(&vault, config)
}

/// Persist security config using an already-unlocked vault handle.
pub fn security_config_save_in(
    vault: &Vault,
    config: &security_config::SecurityConfig,
) -> Result<(), Error> {
    security_config::save_config(vault.root(), config, vault.master_key_bytes())
}

/// Apply a security tier preset in the default vault.
pub fn security_apply_preset(tier: security_config::SecurityTier) -> Result<(), Error> {
    let vault = Vault::open_default()?;
    let mut config = security_config::load_config(vault.root(), vault.master_key_bytes())?;
    let required = config.validate_tier_change(tier);
    if required != security_config::SecurityTier::Standard {
        return Err(Error::CryptoFailure(format!(
            "tier change requires re-authentication at the {required:?} tier"
        )));
    }
    let old_tier = format!("{:?}", config.tier);
    config.apply_preset(tier);
    security_config::save_config(vault.root(), &config, vault.master_key_bytes())?;
    audit::log_required(&audit::AuditEvent::TierChanged {
        from: old_tier,
        to: format!("{:?}", config.tier),
    })?;
    Ok(())
}

/// Apply a security tier preset by stable lowercase name.
pub fn security_apply_preset_by_name(preset: &str) -> Result<(), Error> {
    let tier = match preset {
        "standard" => security_config::SecurityTier::Standard,
        "hardened" => security_config::SecurityTier::Hardened,
        "lockdown" => security_config::SecurityTier::Lockdown,
        other => {
            return Err(Error::CryptoFailure(format!(
                "unknown security preset: {other} (expected standard|hardened|lockdown)"
            )))
        }
    };
    security_apply_preset(tier)
}

/// Set a single security-config field by name.
///
/// Accepted fields and their value semantics:
/// - `challenge_required`, `audit_logging`, `relay_required`,
///   `x11_auto_upgrade` — take a boolean
///   (`true|false|1|0|yes|no|on|off`).
/// - `approval_delay_secs` — takes a `u32`.
///
/// Hostile / degraded environment blocking moved to the policy
/// taxonomy; use `envseal security override` /
/// `envseal security override-tier` instead of legacy
/// `block_hostile` / `block_degraded` toggles.
///
/// # Errors
/// `Error::CryptoFailure` for unknown fields or unparseable values.
pub fn security_set_field(field: &str, value: &str) -> Result<(), Error> {
    let parse_bool = |s: &str| -> Result<bool, Error> {
        match s {
            "true" | "1" | "yes" | "on" => Ok(true),
            "false" | "0" | "no" | "off" => Ok(false),
            _ => Err(Error::CryptoFailure(format!(
                "invalid boolean: {s} (use true/false)"
            ))),
        }
    };

    let mut config = security_config_get()?;
    match field {
        "challenge_required" => config.challenge_required = parse_bool(value)?,
        "approval_delay_secs" => {
            config.approval_delay_secs = value
                .parse::<u32>()
                .map_err(|_| Error::CryptoFailure(format!("invalid u32: {value}")))?;
        }
        "audit_logging" => config.audit_logging = parse_bool(value)?,
        "relay_required" => config.relay_required = parse_bool(value)?,
        "x11_auto_upgrade" => config.x11_auto_upgrade = parse_bool(value)?,
        "block_hostile" | "block_degraded" => {
            return Err(Error::CryptoFailure(format!(
                "field '{field}' was removed in v0.2.0. The policy \
                 taxonomy now decides hostile/degraded handling per tier. \
                 Use `envseal security override-tier` to author overrides."
            )));
        }
        other => {
            return Err(Error::CryptoFailure(format!(
                "unknown security field '{other}'. valid: challenge_required, \
                 approval_delay_secs, audit_logging, relay_required, x11_auto_upgrade"
            )));
        }
    }
    security_config_save(&config)
}

/// Set or replace a per-signal policy override and persist.
///
/// Thin wrapper that loads, mutates, and re-saves the security config
/// (HMAC re-signed with the current master key).
///
/// # Errors
/// `Error::CryptoFailure` for invalid signal id or action; vault/IO
/// errors propagated from load/save.
pub fn security_override_signal(signal_id: &str, action: &str) -> Result<(), Error> {
    let mut config = security_config_get()?;
    config.set_signal_override(signal_id, action)?;
    security_config_save(&config)
}

/// Remove a per-signal policy override. Returns whether one was present.
///
/// # Errors
/// Vault/IO errors from load/save.
pub fn security_clear_signal_override(signal_id: &str) -> Result<bool, Error> {
    let mut config = security_config_get()?;
    let removed = config.clear_signal_override(signal_id);
    if removed {
        security_config_save(&config)?;
    }
    Ok(removed)
}

/// Set or replace a per-(severity, tier) policy override and persist.
///
/// # Errors
/// `Error::CryptoFailure` for invalid severity/tier/action; vault/IO
/// errors propagated from load/save.
pub fn security_override_tier(severity: &str, tier: &str, action: &str) -> Result<(), Error> {
    let mut config = security_config_get()?;
    config.set_tier_override(severity, tier, action)?;
    security_config_save(&config)
}

/// Remove a per-(severity, tier) policy override. Returns whether one was present.
///
/// # Errors
/// `Error::CryptoFailure` for invalid severity/tier; vault/IO errors
/// from load/save.
pub fn security_clear_tier_override(severity: &str, tier: &str) -> Result<bool, Error> {
    let mut config = security_config_get()?;
    let removed = config.clear_tier_override(severity, tier)?;
    if removed {
        security_config_save(&config)?;
    }
    Ok(removed)
}

/// Snapshot of policy overrides — `(signal_overrides, tier_overrides)`.
pub type OverrideSnapshot = (
    std::collections::BTreeMap<String, String>,
    std::collections::BTreeMap<String, String>,
);

/// Snapshot of all currently authored policy overrides.
///
/// Both maps are stable `BTreeMap<String, String>` so iteration order
/// is deterministic for rendering.
///
/// # Errors
/// Vault/IO errors from load.
pub fn security_list_overrides() -> Result<OverrideSnapshot, Error> {
    let config = security_config_get()?;
    Ok((config.signal_overrides, config.tier_overrides))
}

/// Set up TOTP two-factor authentication.
///
/// Returns the `otpauth` URI on success so the caller can render it to
/// the user (CLI prints it; desktop renders a QR; MCP returns it).
///
/// The user-supplied verification code is read via the platform GUI
/// prompt (one attempt). On valid pairing the encrypted secret is
/// persisted into `security.toml` and `totp_required` is set.
///
/// # Errors
/// `Error::CryptoFailure` when TOTP is already configured, when the
/// verification code is wrong, plus the underlying vault/store/audit errors.
pub fn totp_setup() -> Result<String, Error> {
    let mut config = security_config_get()?;
    if config.totp_required && config.totp_secret_encrypted.is_some() {
        return Err(Error::CryptoFailure(
            "TOTP is already configured. Disable it first.".to_string(),
        ));
    }

    let secret_b32 = totp::generate_secret();
    let uri = totp::otpauth_uri(&secret_b32, "vault");

    let user_code = gui::request_totp_code(1)?;
    if !totp::verify_code(&secret_b32, &user_code)? {
        return Err(Error::CryptoFailure(
            "invalid TOTP verification code — setup cancelled".to_string(),
        ));
    }

    let vault = Vault::open_default()?;
    let encrypted = totp::encrypt_secret(&secret_b32, vault.master_key_bytes())?;
    config.totp_secret_encrypted = Some(encrypted);
    config.totp_required = true;
    security_config::save_config(vault.root(), &config, vault.master_key_bytes())?;

    audit::log_required(&audit::AuditEvent::TierChanged {
        from: "totp_disabled".to_string(),
        to: "totp_enabled".to_string(),
    })?;

    Ok(uri)
}

/// Disable TOTP. Requires verification of the current TOTP code first
/// (so a thief who only has the master passphrase can't trivially turn
/// off the second factor).
///
/// # Errors
/// `Error::CryptoFailure` when TOTP isn't enabled or the verification
/// code is wrong.
pub fn totp_disable() -> Result<(), Error> {
    let vault = Vault::open_default()?;
    let mut config = security_config::load_config(vault.root(), vault.master_key_bytes())?;
    if !config.totp_required {
        return Err(Error::CryptoFailure("TOTP is not enabled.".to_string()));
    }
    if let Some(ref encrypted) = config.totp_secret_encrypted {
        let secret_b32 = totp::decrypt_secret(encrypted, vault.master_key_bytes())?;
        let user_code = gui::request_totp_code(1)?;
        if !totp::verify_code(&secret_b32, &user_code)? {
            return Err(Error::CryptoFailure(
                "invalid TOTP code — disable cancelled".to_string(),
            ));
        }
    }
    config.totp_required = false;
    config.totp_secret_encrypted = None;
    security_config::save_config(vault.root(), &config, vault.master_key_bytes())?;
    audit::log_required(&audit::AuditEvent::TierChanged {
        from: "totp_enabled".to_string(),
        to: "totp_disabled".to_string(),
    })?;
    Ok(())
}