use std::path::PathBuf;
use anyhow::Context;
#[cfg(feature = "cli")]
use clap::{Args, Subcommand};
use crate::transport_iroh::{
default_key_path, enforce_passphrase_strength, format_endpoint_id, load_or_create_secret_key,
write_identity_key,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyOp {
Passwd,
Info,
}
#[derive(Debug, Clone)]
pub struct KeyConfig {
pub op: KeyOp,
pub key_file: Option<PathBuf>,
}
#[cfg(feature = "cli")]
#[derive(Args, Debug)]
pub struct KeyArgs {
#[command(subcommand)]
cmd: KeyCmd,
#[arg(long, global = true)]
key_file: Option<PathBuf>,
}
#[cfg(feature = "cli")]
#[derive(Subcommand, Debug)]
enum KeyCmd {
Passwd,
Info,
}
#[cfg(feature = "cli")]
impl From<KeyArgs> for KeyConfig {
fn from(a: KeyArgs) -> Self {
Self {
op: match a.cmd {
KeyCmd::Passwd => KeyOp::Passwd,
KeyCmd::Info => KeyOp::Info,
},
key_file: a.key_file,
}
}
}
pub fn run(config: impl Into<KeyConfig>) -> anyhow::Result<()> {
let args: KeyConfig = config.into();
let key_file = match args.key_file {
Some(p) => p,
None => default_key_path("client")?,
};
anyhow::ensure!(
key_file.exists(),
"no identity key at {} — run `koh id` (or `koh connect`/`koh serve`) to create one first, \
or pass --key-file",
key_file.display()
);
let secret = load_or_create_secret_key(&key_file)
.with_context(|| format!("loading identity key from {}", key_file.display()))?;
match args.op {
KeyOp::Info => {
println!("key file : {}", key_file.display());
println!("encryption : koh-key-v1 (Argon2id + AES-256-GCM)");
println!("endpoint id : {}", format_endpoint_id(&secret.public()));
}
KeyOp::Passwd => {
let new = if let Ok(p) = std::env::var("KOH_KEY_NEW_PASSPHRASE") {
anyhow::ensure!(
!p.is_empty(),
"$KOH_KEY_NEW_PASSPHRASE is empty; identity keys are always encrypted"
);
p
} else {
let p1 = rpassword::prompt_password("New passphrase: ")
.context("reading new passphrase")?;
anyhow::ensure!(
!p1.is_empty(),
"an empty passphrase is not allowed; identity keys are always encrypted"
);
let p2 = rpassword::prompt_password("Confirm passphrase: ")
.context("reading confirmation")?;
anyhow::ensure!(p1 == p2, "passphrases did not match");
p1
};
enforce_passphrase_strength(&new)?;
write_identity_key(&key_file, &secret, &new)?;
eprintln!(
"koh: identity key re-encrypted at {} (koh-key-v1). Set $KOH_KEY_PASSPHRASE for \
unattended `koh serve`.",
key_file.display()
);
}
}
Ok(())
}