use std::path::Path;
use zeroize::Zeroize;
use crate::cli::output;
use crate::cli::{load_keyfile, prompt_new_password, prompt_password_for_vault, vault_path, Cli};
use crate::config::Settings;
use crate::crypto::kdf::generate_salt;
use crate::crypto::keyfile;
use crate::crypto::keys::MasterKey;
use crate::errors::Result;
use crate::vault::format::{StoredArgon2Params, VaultHeader, CURRENT_VERSION};
use crate::vault::VaultStore;
pub fn execute(cli: &Cli, new_keyfile_arg: Option<&str>) -> Result<()> {
let path = vault_path(cli)?;
output::info("Enter your current vault password.");
let keyfile_data = load_keyfile(cli)?;
let vault_id = path.to_string_lossy();
let old_password = prompt_password_for_vault(Some(&vault_id))?;
let store = VaultStore::open(&path, old_password.as_bytes(), keyfile_data.as_deref())?;
let mut secrets = store.get_all_secrets()?;
output::info("Choose your new vault password.");
let new_password = prompt_new_password()?;
let cwd = std::env::current_dir()?;
let settings = Settings::load(&cwd)?;
let params = settings.argon2_params();
let (new_keyfile_bytes, new_keyfile_hash) =
resolve_new_keyfile(new_keyfile_arg, keyfile_data.as_deref(), &store)?;
let new_salt = generate_salt();
let mut effective_password = match &new_keyfile_bytes {
Some(kf) => keyfile::combine_password_keyfile(new_password.as_bytes(), kf)?,
None => new_password.as_bytes().to_vec(),
};
let mut master_bytes =
crate::crypto::kdf::derive_master_key_with_params(&effective_password, &new_salt, ¶ms)?;
effective_password.zeroize();
let new_master_key = MasterKey::new(master_bytes);
master_bytes.zeroize();
let new_header = VaultHeader {
version: CURRENT_VERSION,
salt: new_salt.to_vec(),
created_at: store.created_at(),
environment: store.environment().to_string(),
argon2_params: Some(StoredArgon2Params {
memory_kib: params.memory_kib,
iterations: params.iterations,
parallelism: params.parallelism,
}),
keyfile_hash: new_keyfile_hash,
};
let mut new_store = VaultStore::from_parts(path, new_header, new_master_key);
for (name, value) in &secrets {
new_store.set_secret(name, value)?;
}
for value in secrets.values_mut() {
value.zeroize();
}
new_store.save()?;
crate::audit::log_audit(
cli,
"rotate-key",
None,
Some(&format!(
"{} secrets re-encrypted",
new_store.secret_count()
)),
);
let keyfile_msg = match new_keyfile_arg {
Some("none") => " (keyfile requirement removed)",
Some(_) => " (keyfile changed)",
None => "",
};
output::success(&format!(
"Password rotated for '{}' vault ({} secrets re-encrypted){}",
new_store.environment(),
new_store.secret_count(),
keyfile_msg,
));
Ok(())
}
fn resolve_new_keyfile(
new_keyfile_arg: Option<&str>,
existing_keyfile: Option<&[u8]>,
store: &VaultStore,
) -> Result<(Option<Vec<u8>>, Option<String>)> {
match new_keyfile_arg {
Some("none") => {
output::info("Removing keyfile requirement from vault.");
Ok((None, None))
}
Some(path) => {
output::info(&format!("Switching to new keyfile: {path}"));
let bytes = keyfile::load_keyfile(Path::new(path))?;
let hash = keyfile::hash_keyfile(&bytes);
Ok((Some(bytes), Some(hash)))
}
None => Ok((
existing_keyfile.map(|b| b.to_vec()),
store.header().keyfile_hash.clone(),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_new_keyfile_none_removes_requirement() {
let tmp = tempfile::TempDir::new().unwrap();
let vault_path = tmp.path().join(".envvault").join("dev.vault");
std::fs::create_dir_all(vault_path.parent().unwrap()).unwrap();
let kf_bytes = [0xABu8; 32];
let store = VaultStore::create(
&vault_path,
b"test-password-long",
"dev",
None,
Some(&kf_bytes),
)
.unwrap();
let (bytes, hash) = resolve_new_keyfile(Some("none"), Some(&kf_bytes), &store).unwrap();
assert!(bytes.is_none());
assert!(hash.is_none());
}
#[test]
fn resolve_new_keyfile_with_path_changes_hash() {
let tmp = tempfile::TempDir::new().unwrap();
let vault_path = tmp.path().join(".envvault").join("dev.vault");
std::fs::create_dir_all(vault_path.parent().unwrap()).unwrap();
let store =
VaultStore::create(&vault_path, b"test-password-long", "dev", None, None).unwrap();
let kf_path = tmp.path().join("new.keyfile");
let kf_bytes = crate::crypto::keyfile::generate_keyfile(&kf_path).unwrap();
let (bytes, hash) =
resolve_new_keyfile(Some(kf_path.to_str().unwrap()), None, &store).unwrap();
assert!(bytes.is_some());
assert!(hash.is_some());
assert_eq!(bytes.unwrap(), kf_bytes);
}
#[test]
fn resolve_new_keyfile_preserves_existing() {
let tmp = tempfile::TempDir::new().unwrap();
let vault_path = tmp.path().join(".envvault").join("dev.vault");
std::fs::create_dir_all(vault_path.parent().unwrap()).unwrap();
let kf_bytes = [0xCDu8; 32];
let store = VaultStore::create(
&vault_path,
b"test-password-long",
"dev",
None,
Some(&kf_bytes),
)
.unwrap();
let original_hash = store.header().keyfile_hash.clone();
let (bytes, hash) = resolve_new_keyfile(None, Some(&kf_bytes), &store).unwrap();
assert_eq!(bytes.unwrap(), kf_bytes);
assert_eq!(hash, original_hash);
}
}