use crate::{
Application, RUSTIC_APP, helpers::table_with_titles, repository::OpenRepo, status_err,
};
use std::path::PathBuf;
use abscissa_core::{Command, Runnable, Shutdown};
use anyhow::{Result, bail};
use dialoguer::Password;
use log::{info, warn};
use qrcode::{QrCode, render::svg};
use rustic_core::{
CommandInput, CredentialOptions, Credentials, KeyOptions,
repofile::{KeyFile, MasterKey},
};
#[derive(clap::Parser, Command, Debug)]
pub(super) struct KeyCmd {
#[clap(subcommand)]
cmd: KeySubCmd,
}
impl Runnable for KeyCmd {
fn run(&self) {
self.cmd.run();
}
}
#[derive(clap::Subcommand, Debug, Runnable)]
enum KeySubCmd {
Add(AddCmd),
List(ListCmd),
Remove(RemoveCmd),
Password(PasswordCmd),
Export(ExportCmd),
Create(CreateCmd),
}
#[derive(clap::Parser, Debug)]
pub(crate) struct NewPasswordOptions {
#[clap(long)]
pub(crate) new_password: Option<String>,
#[clap(long)]
pub(crate) new_password_file: Option<PathBuf>,
#[clap(long)]
pub(crate) new_password_command: Option<CommandInput>,
}
impl NewPasswordOptions {
fn pass(&self, text: &str) -> Result<String> {
let mut pass_opts = CredentialOptions::default();
pass_opts.password = self.new_password.clone();
pass_opts.password_file = self.new_password_file.clone();
pass_opts.password_command = self.new_password_command.clone();
let pass = if let Some(Credentials::Password(pass)) = pass_opts.credentials()? {
pass
} else {
Password::new()
.with_prompt(text)
.allow_empty_password(true)
.with_confirmation("confirm password", "passwords do not match")
.interact()?
};
Ok(pass)
}
}
#[derive(clap::Parser, Debug)]
pub(crate) struct AddCmd {
#[clap(flatten)]
pub(crate) pass_opts: NewPasswordOptions,
#[clap(flatten)]
pub(crate) key_opts: KeyOptions,
}
impl Runnable for AddCmd {
fn run(&self) {
if let Err(err) = RUSTIC_APP
.config()
.repository
.run_open(|repo| self.inner_run(repo))
{
status_err!("{}", err);
RUSTIC_APP.shutdown(Shutdown::Crash);
};
}
}
impl AddCmd {
fn inner_run(&self, repo: OpenRepo) -> Result<()> {
if RUSTIC_APP.config().global.dry_run {
info!("adding no key in dry-run mode.");
return Ok(());
}
let pass = self.pass_opts.pass("enter password for new key")?;
let id = repo.add_key(&pass, &self.key_opts)?;
info!("key {id} successfully added.");
Ok(())
}
}
#[derive(clap::Parser, Debug)]
pub(crate) struct ListCmd;
impl Runnable for ListCmd {
fn run(&self) {
if let Err(err) = RUSTIC_APP
.config()
.repository
.run_open(|repo| self.inner_run(repo))
{
status_err!("{}", err);
RUSTIC_APP.shutdown(Shutdown::Crash);
};
}
}
impl ListCmd {
fn inner_run(&self, repo: OpenRepo) -> Result<()> {
let used_key = repo.key_id();
let keys = repo
.stream_files()?
.inspect(|f| {
if let Err(err) = f {
warn!("{err:?}");
}
})
.filter_map(Result::ok);
let mut table = table_with_titles(["ID", "User", "Host", "Created"]);
_ = table.add_rows(keys.map(|key: (_, KeyFile)| {
[
format!(
"{}{}",
if used_key == &Some(key.0) { "*" } else { "" },
key.0
),
key.1.username.unwrap_or_default(),
key.1.hostname.unwrap_or_default(),
key.1
.created
.map_or(String::new(), |time| format!("{time}")),
]
}));
println!("{table}");
Ok(())
}
}
#[derive(clap::Parser, Debug)]
pub(crate) struct RemoveCmd {
ids: Vec<String>,
}
impl Runnable for RemoveCmd {
fn run(&self) {
if let Err(err) = RUSTIC_APP
.config()
.repository
.run_open(|repo| self.inner_run(repo))
{
status_err!("{}", err);
RUSTIC_APP.shutdown(Shutdown::Crash);
};
}
}
impl RemoveCmd {
fn inner_run(&self, repo: OpenRepo) -> Result<()> {
let repo_key = repo.key_id();
let ids: Vec<_> = repo.find_ids(&self.ids)?.collect();
if ids.iter().any(|id| Some(id) == repo_key.as_ref()) {
bail!("Cannot remove currently used key!");
}
if !RUSTIC_APP.config().global.dry_run {
for id in ids {
repo.delete_key(&id)?;
info!("key {id} successfully removed.");
}
return Ok(());
}
let keys = repo
.stream_files_list(ids)?
.inspect(|f| {
if let Err(err) = f {
warn!("{err:?}");
}
})
.filter_map(Result::ok);
let mut table = table_with_titles(["ID", "User", "Host", "Created"]);
_ = table.add_rows(keys.map(|key: (_, KeyFile)| {
[
key.0.to_string(),
key.1.username.unwrap_or_default(),
key.1.hostname.unwrap_or_default(),
key.1
.created
.map_or(String::new(), |time| format!("{time}")),
]
}));
println!("would have removed the following keys:");
println!("{table}");
Ok(())
}
}
#[derive(clap::Parser, Debug)]
pub(crate) struct PasswordCmd {
#[clap(flatten)]
pub(crate) pass_opts: NewPasswordOptions,
}
impl Runnable for PasswordCmd {
fn run(&self) {
if let Err(err) = RUSTIC_APP
.config()
.repository
.run_open(|repo| self.inner_run(repo))
{
status_err!("{}", err);
RUSTIC_APP.shutdown(Shutdown::Crash);
};
}
}
impl PasswordCmd {
fn inner_run(&self, repo: OpenRepo) -> Result<()> {
let Some(key_id) = repo.key_id() else {
bail!("No keyfile used to open the repo. Cannot change the password.")
};
if RUSTIC_APP.config().global.dry_run {
info!("changing no password in dry-run mode.");
return Ok(());
}
let pass = self.pass_opts.pass("enter new password")?;
let old_key: KeyFile = repo.get_file(key_id)?;
let key_opts = KeyOptions::default()
.hostname(old_key.hostname)
.username(old_key.username)
.with_created(old_key.created.is_some());
let id = repo.add_key(&pass, &key_opts)?;
info!("key {id} successfully added.");
let old_key = *key_id; let repo = repo.open(&Credentials::Password(pass))?;
repo.delete_key(&old_key)?;
info!("key {old_key} successfully removed.");
Ok(())
}
}
#[derive(clap::Parser, Debug)]
pub(crate) struct ExportCmd {
pub(crate) file: Option<PathBuf>,
#[clap(long)]
pub(crate) qr: bool,
}
impl Runnable for ExportCmd {
fn run(&self) {
if let Err(err) = RUSTIC_APP.config().repository.run_open(|repo| {
let mut data = serde_json::to_string(&repo.key())?;
if self.qr {
let qr = QrCode::new(&data)?;
data = qr.render::<svg::Color<'_>>().build();
}
match &self.file {
None => println!("{}", data),
Some(file) => std::fs::write(file, data)?,
}
Ok(())
}) {
status_err!("{}", err);
RUSTIC_APP.shutdown(Shutdown::Crash);
};
}
}
#[derive(clap::Parser, Debug)]
pub(crate) struct CreateCmd {
pub(crate) file: Option<PathBuf>,
}
impl Runnable for CreateCmd {
fn run(&self) {
let inner = || -> Result<_> {
let data = serde_json::to_string(&MasterKey::new())?;
match &self.file {
None => println!("{}", data),
Some(file) => std::fs::write(file, data)?,
}
Ok(())
};
if let Err(err) = inner() {
status_err!("{}", err);
RUSTIC_APP.shutdown(Shutdown::Crash);
};
}
}