Skip to main content

envvault/cli/commands/
rotate.rs

1//! `envvault rotate-key` — change the vault master password.
2//!
3//! Decrypts all secrets with the old password, generates a new salt,
4//! re-derives the master key from the new password, re-encrypts all
5//! secrets, and writes the vault atomically.
6
7use zeroize::Zeroize;
8
9use crate::cli::output;
10use crate::cli::{load_keyfile, prompt_new_password, prompt_password_for_vault, vault_path, Cli};
11use crate::config::Settings;
12use crate::crypto::kdf::generate_salt;
13use crate::crypto::keys::MasterKey;
14use crate::errors::Result;
15use crate::vault::format::{StoredArgon2Params, VaultHeader, CURRENT_VERSION};
16use crate::vault::VaultStore;
17
18/// Execute the `rotate-key` command.
19pub fn execute(cli: &Cli) -> Result<()> {
20    let path = vault_path(cli)?;
21
22    // 1. Open the vault with the current password.
23    output::info("Enter your current vault password.");
24    let keyfile = load_keyfile(cli)?;
25    let vault_id = path.to_string_lossy();
26    let old_password = prompt_password_for_vault(Some(&vault_id))?;
27    let store = VaultStore::open(&path, old_password.as_bytes(), keyfile.as_deref())?;
28
29    // 2. Decrypt all secrets into memory.
30    let mut secrets = store.get_all_secrets()?;
31
32    // 3. Prompt for the new password.
33    output::info("Choose your new vault password.");
34    let new_password = prompt_new_password()?;
35
36    // 4. Load settings for Argon2 params.
37    let cwd = std::env::current_dir()?;
38    let settings = Settings::load(&cwd)?;
39    let params = settings.argon2_params();
40
41    // 5. Generate a new salt and derive a new master key.
42    //    If the vault uses a keyfile, combine it with the new password.
43    let new_salt = generate_salt();
44    let mut effective_password = match &keyfile {
45        Some(kf) => crate::crypto::keyfile::combine_password_keyfile(new_password.as_bytes(), kf)?,
46        None => new_password.as_bytes().to_vec(),
47    };
48    let mut master_bytes =
49        crate::crypto::kdf::derive_master_key_with_params(&effective_password, &new_salt, &params)?;
50    effective_password.zeroize();
51    let new_master_key = MasterKey::new(master_bytes);
52    master_bytes.zeroize();
53
54    // 6. Build a new header with the new salt and params.
55    let new_header = VaultHeader {
56        version: CURRENT_VERSION,
57        salt: new_salt.to_vec(),
58        created_at: store.created_at(),
59        environment: store.environment().to_string(),
60        argon2_params: Some(StoredArgon2Params {
61            memory_kib: params.memory_kib,
62            iterations: params.iterations,
63            parallelism: params.parallelism,
64        }),
65        keyfile_hash: store.header().keyfile_hash.clone(),
66    };
67
68    // 7. Create a new vault store with the new key and re-encrypt secrets.
69    let mut new_store = VaultStore::from_parts(path, new_header, new_master_key);
70
71    for (name, value) in &secrets {
72        new_store.set_secret(name, value)?;
73    }
74
75    // 8. Zeroize plaintext secrets from memory.
76    for value in secrets.values_mut() {
77        value.zeroize();
78    }
79
80    // 9. Save atomically.
81    new_store.save()?;
82
83    crate::audit::log_audit(
84        cli,
85        "rotate-key",
86        None,
87        Some(&format!(
88            "{} secrets re-encrypted",
89            new_store.secret_count()
90        )),
91    );
92
93    output::success(&format!(
94        "Password rotated for '{}' vault ({} secrets re-encrypted)",
95        new_store.environment(),
96        new_store.secret_count()
97    ));
98
99    Ok(())
100}