use tracing::info;
use crate::cli::output;
use crate::core::vault::Vault;
use crate::core::{cipher, config, store};
use crate::error::Result;
use std::fs;
use std::path::PathBuf;
fn archive_dir(project_id: &str) -> Result<PathBuf> {
use crate::core::domain::Identity;
let key_dir = Identity::project_dir(project_id)?;
Ok(key_dir.join("archive"))
}
fn archive_old_key(project_id: &str) -> Result<()> {
use crate::core::domain::Identity;
let old_key_path = Identity::project_dir(project_id)?.join("identity.key");
if !old_key_path.exists() {
return Ok(());
}
let archive_path = archive_dir(project_id)?;
fs::create_dir_all(&archive_path)?;
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
let archive_file = archive_path.join(format!("identity.key.{}", timestamp));
fs::rename(&old_key_path, &archive_file)?;
Ok(())
}
pub fn execute() -> Result<()> {
info!("Starting key rotation");
let vault = Vault::open()?;
let old_public_key = vault.identity().public_key();
let mut cfg = config::Config::load()?;
let project_id = cfg.project_id();
let backend = cipher::CipherBackend::from_config(&cfg)?;
if !store::has_key(&project_id) {
return Err(crate::error::StoreError::NoPrivateKey(project_id.clone()).into());
}
let mut decrypted_secrets = Vec::new();
for (key, ciphertext) in &cfg.secrets {
let plaintext = backend.decrypt(ciphertext, vault.identity().as_age())?;
decrypted_secrets.push((key.clone(), plaintext));
}
archive_old_key(&project_id)?;
let new_public_key = store::generate_keypair(&project_id)?;
let mut recipients: Vec<String> = cfg
.recipients
.values()
.map(|key| {
if key == &old_public_key {
new_public_key.clone()
} else {
key.clone()
}
})
.collect();
if !recipients.iter().any(|key| key == &new_public_key) {
recipients.push(new_public_key.clone());
}
recipients.sort();
recipients.dedup();
let secret_count = decrypted_secrets.len();
cfg.secrets.clear();
for (key, plaintext) in decrypted_secrets {
let ciphertext = backend.encrypt(&plaintext, &recipients)?;
cfg.secrets.insert(key, ciphertext);
}
let owner_name = cfg
.recipients
.iter()
.find(|(_, key)| *key == &old_public_key)
.map(|(name, _)| name.clone())
.or_else(|| cfg.recipients.keys().next().cloned());
if let Some(name) = owner_name {
cfg.recipients.insert(name, new_public_key.clone());
}
cfg.save()?;
output::success(&format!("rotated ({} secrets re-encrypted)", secret_count));
Ok(())
}