use crate::config::Config;
use anyhow::{bail, Context, Result};
use cargo_component_core::{
command::CommonOptions,
keyring::{self, delete_signing_key, get_signing_key, get_signing_key_entry, set_signing_key},
terminal::Colors,
};
use clap::{Args, Subcommand};
use p256::ecdsa::SigningKey;
use rand_core::OsRng;
use std::io::{self, Write};
use warg_client::RegistryUrl;
use warg_crypto::signing::PrivateKey;
#[derive(Args)]
#[clap(disable_version_flag = true)]
pub struct KeyCommand {
#[clap(flatten)]
pub common: CommonOptions,
#[clap(subcommand)]
pub command: KeySubcommand,
}
impl KeyCommand {
pub async fn exec(self) -> Result<()> {
log::debug!("executing key command");
let config = Config::new(self.common.new_terminal())?;
match self.command {
KeySubcommand::Id(cmd) => cmd.exec().await,
KeySubcommand::New(cmd) => cmd.exec(&config).await,
KeySubcommand::Set(cmd) => cmd.exec(&config).await,
KeySubcommand::Delete(cmd) => cmd.exec(&config).await,
}
}
}
#[derive(Subcommand)]
pub enum KeySubcommand {
Id(KeyIdCommand),
New(KeyNewCommand),
Set(KeySetCommand),
Delete(KeyDeleteCommand),
}
#[derive(Args)]
pub struct KeyIdCommand {
#[clap(long, short, value_name = "NAME", default_value = "default")]
pub key_name: String,
#[clap(value_name = "URL")]
pub url: RegistryUrl,
}
impl KeyIdCommand {
pub async fn exec(self) -> Result<()> {
let key = get_signing_key(&self.url, &self.key_name)?;
println!(
"{fingerprint}",
fingerprint = key.public_key().fingerprint()
);
Ok(())
}
}
#[derive(Args)]
#[clap(disable_version_flag = true)]
pub struct KeyNewCommand {
#[clap(long, short, value_name = "NAME", default_value = "default")]
pub key_name: String,
#[clap(value_name = "URL")]
pub url: RegistryUrl,
}
impl KeyNewCommand {
pub async fn exec(self, config: &Config) -> Result<()> {
let entry = get_signing_key_entry(&self.url, &self.key_name)?;
match entry.get_password() {
Err(keyring::Error::NoEntry) => {
}
Ok(_) | Err(keyring::Error::Ambiguous(_)) => {
bail!(
"signing key `{name}` already exists for registry `{url}`",
name = self.key_name,
url = self.url
);
}
Err(e) => {
bail!(
"failed to get signing key `{name}` for registry `{url}`: {e}",
name = self.key_name,
url = self.url
);
}
}
let key = SigningKey::random(&mut OsRng).into();
set_signing_key(&self.url, &self.key_name, &key)?;
config.terminal().status(
"Created",
format!(
"signing key `{name}` ({fingerprint}) for registry `{url}`",
name = self.key_name,
fingerprint = key.public_key().fingerprint(),
url = self.url,
),
)?;
Ok(())
}
}
#[derive(Args)]
#[clap(disable_version_flag = true)]
pub struct KeySetCommand {
#[clap(long, short, value_name = "NAME", default_value = "default")]
pub key_name: String,
#[clap(value_name = "URL")]
pub url: RegistryUrl,
}
impl KeySetCommand {
pub async fn exec(self, config: &Config) -> Result<()> {
let key = PrivateKey::decode(
rpassword::prompt_password("input signing key (expected format is `<alg>:<base64>`): ")
.context("failed to read signing key")?,
)
.context("signing key is not in the correct format")?;
set_signing_key(&self.url, &self.key_name, &key)?;
config.terminal().status(
"Set",
format!(
"signing key `{name}` ({fingerprint}) for registry `{url}`",
name = self.key_name,
fingerprint = key.public_key().fingerprint(),
url = self.url,
),
)?;
Ok(())
}
}
#[derive(Args)]
#[clap(disable_version_flag = true)]
pub struct KeyDeleteCommand {
#[clap(long, short, value_name = "NAME", default_value = "default")]
pub key_name: String,
#[clap(value_name = "URL")]
pub url: RegistryUrl,
}
impl KeyDeleteCommand {
pub async fn exec(self, config: &Config) -> Result<()> {
config.terminal().write_stdout(
"⚠️ WARNING: this operation cannot be undone and the key will be permanently deleted ⚠️",
Some(Colors::Yellow),
)?;
config.terminal().write_stdout(
format!(
"\nare you sure you want to delete signing key `{name}` for registry `{url}`? [type `yes` to confirm] ",
name = self.key_name,
url = self.url
),
None,
)?;
io::stdout().flush().ok();
let mut line = String::new();
io::stdin().read_line(&mut line).ok();
line.make_ascii_lowercase();
if line.trim() != "yes" {
config.terminal().note(format!(
"skipping deletion of signing key for registry `{url}`",
url = self.url,
))?;
return Ok(());
}
delete_signing_key(&self.url, &self.key_name)?;
config.terminal().status(
"Deleted",
format!(
"signing key `{name}` for registry `{url}`",
name = self.key_name,
url = self.url,
),
)?;
Ok(())
}
}