use clap::Args;
use std::collections::BTreeMap;
use std::fs;
use std::fs::File;
use std::io::BufReader;
use std::path::PathBuf;
use crate::error::{Error, Result};
use crate::etree::{self, ParseOps, TextNode, TextTree};
use super::{CommonArgs, apply_common, resolve_policy};
#[derive(Args)]
pub struct RotateSubcmd {
#[arg(long = "key-file", value_name = "PRIV.pem")]
pub key_file: Option<PathBuf>,
#[arg(long, value_name = "PASSWORD")]
pub new_password: String,
#[arg(long = "recovery-key", value_name = "PUB.pem")]
pub recovery_key: Vec<PathBuf>,
#[arg(value_name = "FILE")]
pub files: Vec<String>,
}
pub(super) fn run(common: CommonArgs, a: RotateSubcmd) -> Result<()> {
if a.files.is_empty() {
return Err(Error::InvalidArg {
arg: "FILE",
reason: "rotate needs at least one FILE".to_string(),
});
}
if let Some(f) = a.files.iter().find(|f| f.as_str() == "-") {
return Err(Error::InvalidArg {
arg: "FILE",
reason: format!(
"stdin ('{f}') not supported: the rewrite must be written back in place"
),
});
}
if common.password.is_empty() && a.key_file.is_none() {
return Err(Error::InvalidArg {
arg: "-k",
reason: "rotate: supply the current WORD password (-k WORD=PASSWORD) \\
or a current recovery privkey (--key-file)"
.to_string(),
});
}
let policy = resolve_policy(&common)?;
let mut paops = ParseOps::new(policy)?;
apply_common(&common, &mut paops);
let old_passwords: BTreeMap<String, String> = common.password.iter().cloned().collect();
let old_priv = a.key_file.as_ref().map(fs::read_to_string).transpose()?;
let new_pubs: Vec<String> = a
.recovery_key
.iter()
.map(fs::read_to_string)
.collect::<std::result::Result<_, _>>()?;
if new_pubs.is_empty() {
return Err(Error::InvalidArg {
arg: "--recovery-key",
reason: "rotate needs at least one --recovery-key for the new wrap".to_string(),
});
}
for path in &a.files {
rotate_one_file(
path,
&old_passwords,
old_priv.as_deref(),
&a.new_password,
&new_pubs,
&mut paops,
)?;
}
Ok(())
}
fn rotate_one_file(
path: &str,
old_passwords: &BTreeMap<String, String>,
old_priv: Option<&str>,
new_password: &str,
new_pubs: &[String],
paops: &mut ParseOps,
) -> Result<()> {
paops.runtime.fname = path.into();
let reader = File::open(path)
.map_err(|e| Error::Io(std::io::Error::other(format!("Failed to open {path}: {e}"))))?;
let mut tree = etree::parse(BufReader::new(reader), paops)?;
let count = rotate_tree(
&mut tree,
old_passwords,
old_priv,
new_password,
new_pubs,
paops,
)?;
if count == 0 {
println!("{path}: no escrow-mode blocks found; nothing to rotate");
return Ok(());
}
let mut out = File::create(path)?;
etree::tree_write(&mut out, &tree, paops)?;
println!("{path}: rotated {count} escrow block(s)");
Ok(())
}
fn rotate_tree(
tree: &mut TextTree,
old_passwords: &BTreeMap<String, String>,
old_priv: Option<&str>,
new_password: &str,
new_pubs: &[String],
paops: &mut ParseOps,
) -> Result<usize> {
let mut count = 0;
let mut bad: Option<Error> = None;
etree::visitor::visit_mut(tree, &mut |node| {
let TextNode::Encrypted {
keyw, extfields, ..
} = node
else {
return etree::visitor::Control::Continue;
};
if !crate::escrow::is_escrow_block(extfields) {
return etree::visitor::Control::Prune;
}
let old_pw = old_passwords.get(keyw).map(|s| s.as_str());
match crate::escrow::rotate(
extfields,
old_pw,
old_priv,
new_password,
new_pubs,
&mut paops.crypto.rng,
&paops.crypto.pbkdfopts,
&mut paops.crypto.pbkdf_cache,
&*paops.crypto.policy,
) {
Ok(new_ext) => {
*extfields = new_ext;
count += 1;
}
Err(e) => {
bad.get_or_insert(e);
}
}
etree::visitor::Control::Prune
});
if let Some(e) = bad {
return Err(e);
}
Ok(count)
}