use std::{
ffi::OsString,
fs::{DirBuilder, OpenOptions},
path::{Path, PathBuf},
};
#[cfg(target_family = "unix")]
use std::os::unix::fs::DirBuilderExt;
use anyhow::Result;
use clap::Parser as _;
use dialoguer::{Confirm, Input, Password};
use libmoshpit::{KexMode, KeyPair, extract_public_key_bytes, fingerprint};
use crate::cli::{Cli, Commands};
pub(crate) fn run<I, T>(args: Option<I>) -> Result<()>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
let cli = if let Some(args) = args {
Cli::try_parse_from(args)?
} else {
Cli::try_parse()?
};
match cli.command() {
Commands::Generate => generate_keypair(),
Commands::Verify {
randomart: _,
signature: _,
} => Ok(()),
Commands::Fingerprint { public_key } => display_fingerprint(public_key),
}
}
fn generate_keypair() -> Result<()> {
println!("Generating public/private ed25519 key pair.");
let (priv_key_path, pub_key_path) = setup_paths()?;
if !check_paths(&priv_key_path, &pub_key_path)? {
return Ok(());
}
let passphrase_opt = setup_passphrase(&priv_key_path)?;
let keypair = KeyPair::generate_key_pair(passphrase_opt.as_ref())?;
let mut priv_key_file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&priv_key_path)?;
keypair.write_private_key(&mut priv_key_file)?;
let mut pub_key_file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&pub_key_path)?;
keypair.write_public_key(&mut pub_key_file)?;
println!(
"Your identification has been saved in {}",
priv_key_path.display()
);
println!(
"Your public key has been saved in {}",
pub_key_path.display()
);
println!("The key fingerprint is:");
println!("{}", keypair.fingerprint()?);
println!("The key's randomart image is:");
print!("{}", keypair.randomart());
Ok(())
}
fn setup_paths() -> Result<(PathBuf, PathBuf)> {
let (default_priv_key_path, default_pub_key_ext) =
KeyPair::default_key_path_ext(KexMode::Client)?;
let key_path_prompt = format!(
"Enter file in which to save the key ({})",
default_priv_key_path.display()
);
let priv_key_path_input: String = Input::new()
.with_prompt(key_path_prompt)
.allow_empty(true)
.interact_text()?;
let priv_key_path = if priv_key_path_input.is_empty() {
default_priv_key_path
} else {
PathBuf::from(priv_key_path_input)
};
let mut pub_key_path = priv_key_path.clone();
let _ = pub_key_path.set_extension(default_pub_key_ext);
if let Some(priv_parent) = priv_key_path.parent() {
#[cfg(target_family = "unix")]
{
DirBuilder::new()
.mode(0o700)
.recursive(true)
.create(priv_parent)?;
}
#[cfg(not(target_family = "unix"))]
{
DirBuilder::new().recursive(true).create(priv_parent)?;
}
}
Ok((priv_key_path, pub_key_path))
}
fn check_paths(priv_key_path: &Path, pub_key_path: &Path) -> Result<bool> {
if priv_key_path.try_exists()? || pub_key_path.try_exists()? {
println!("{} already exists.", priv_key_path.display());
Ok(Confirm::new()
.with_prompt("Overwrite?")
.default(false)
.wait_for_newline(true)
.interact()?)
} else {
Ok(true)
}
}
fn setup_passphrase(priv_key_path: &Path) -> Result<Option<String>> {
let mut passphrase_opt = None;
let passphrase_prompt = format!(
"Enter passphrase for \"{}\" (empty for no passphrase)",
priv_key_path.display()
);
let passphrase: String = Password::new()
.with_prompt(passphrase_prompt)
.with_confirmation(
"Enter same passphrase again",
"Passphrases do not match. Try again.",
)
.allow_empty_password(true)
.report(false)
.interact()?;
if !passphrase.is_empty() {
passphrase_opt = Some(passphrase);
}
Ok(passphrase_opt)
}
fn display_fingerprint(public_key_path: &str) -> Result<()> {
let public_key_file = OpenOptions::new().read(true).open(public_key_path)?;
let public_key_bytes = extract_public_key_bytes(public_key_file)?;
println!("{}", fingerprint(&public_key_bytes)?);
Ok(())
}