use std::path::PathBuf;
use age::{x25519, Identity, IdentityFile, Recipient};
use clap::Parser;
use miette::{bail, miette, Context as _, IntoDiagnostic as _, Result};
use tokio::fs::read_to_string;
#[derive(Debug, Clone, Parser)]
pub struct KeyArgs {
#[cfg_attr(docsrs, doc("\n\n**Flag**: `-k, --key-path PATH`"))]
#[arg(short, long, verbatim_doc_comment)]
pub key_path: Option<PathBuf>,
#[cfg_attr(docsrs, doc("\n\n**Flag**: `-K, --key STRING`"))]
#[arg(short = 'K', verbatim_doc_comment, conflicts_with = "key_path")]
pub key: Option<String>,
#[command(flatten)]
#[allow(missing_docs, reason = "don't interfere with clap")]
pub pass: crate::passphrases::PassphraseArgs,
}
impl KeyArgs {
pub async fn get_secret_key(&self) -> Result<Option<Box<dyn Identity>>> {
self.secret_key(false).await
}
pub async fn require_secret_key(&self) -> Result<Box<dyn Identity>> {
self.secret_key(true)
.await
.transpose()
.expect("BUG: when required:true, Some must not be produced")
}
pub async fn get_public_key(&self) -> Result<Option<Box<dyn Recipient + Send>>> {
self.public_key(false).await
}
pub async fn require_public_key(&self) -> Result<Box<dyn Recipient + Send>> {
self.public_key(true)
.await
.transpose()
.expect("BUG: when required:true, Some must not be produced")
}
async fn secret_key(&self, required: bool) -> Result<Option<Box<dyn Identity>>> {
match self {
Self {
key_path: None,
key: None,
..
} => {
if required {
bail!("one of `--key-path` or `--key` must be provided");
} else {
Ok(None)
}
}
Self {
key_path: Some(_),
key: Some(_),
..
} => {
bail!("one of `--key-path` or `--key` must be provided, not both");
}
Self { key: Some(key), .. } => key
.parse::<x25519::Identity>()
.map(|sec| Some(Box::new(sec) as _))
.map_err(|err| miette!("{err}").wrap_err("parsing secret key")),
Self {
key_path: Some(path),
pass,
..
} if path.extension().unwrap_or_default() == "age" => {
let key = tokio::fs::read(path).await.into_diagnostic()?;
let pass = pass.require().await?;
let id = age::decrypt(&pass, &key)
.into_diagnostic()
.wrap_err("revealing identity file")?;
parse_id_as_identity(
&String::from_utf8(id)
.into_diagnostic()
.wrap_err("parsing identity file as UTF-8")?,
)
.map(Some)
}
Self {
key_path: Some(path),
..
} => {
let key = read_to_string(&path)
.await
.into_diagnostic()
.wrap_err("reading identity file")?;
parse_id_as_identity(&key).map(Some)
}
}
}
async fn public_key(&self, required: bool) -> Result<Option<Box<dyn Recipient + Send>>> {
match self {
Self {
key_path: None,
key: None,
..
} => {
if required {
bail!("one of `--key-path` or `--key` must be provided");
} else {
Ok(None)
}
}
Self {
key_path: Some(_),
key: Some(_),
..
} => {
bail!("one of `--key-path` or `--key` must be provided, not both");
}
Self { key: Some(key), .. } if key.starts_with("age") => key
.parse::<x25519::Recipient>()
.map(|key| Some(Box::new(key) as _))
.map_err(|err| miette!("{err}").wrap_err("parsing public key")),
Self { key: Some(key), .. } if key.starts_with("AGE-SECRET-KEY") => key
.parse::<x25519::Identity>()
.map(|sec| Some(Box::new(sec.to_public()) as _))
.map_err(|err| miette!("{err}").wrap_err("parsing key")),
Self { key: Some(_), .. } => {
bail!("value passed to `--key` is not a public or secret age key");
}
Self {
key_path: Some(path),
pass,
..
} if path.extension().unwrap_or_default() == "age" => {
let key = tokio::fs::read(path).await.into_diagnostic()?;
let pass = pass.require().await?;
let id = age::decrypt(&pass, &key)
.into_diagnostic()
.wrap_err("revealing identity file")?;
parse_id_as_recipient(
&String::from_utf8(id)
.into_diagnostic()
.wrap_err("parsing identity file as UTF-8")?,
)
.map(Some)
}
Self {
key_path: Some(path),
..
} => {
let key = read_to_string(path)
.await
.into_diagnostic()
.wrap_err("reading identity file")?;
parse_id_as_recipient(&key).map(Some)
}
}
}
}
fn parse_id_as_identity(id: &str) -> Result<Box<dyn Identity>> {
if id.starts_with("AGE-SECRET-KEY") {
id.parse::<x25519::Identity>()
.map(|sec| Box::new(sec) as _)
.map_err(|err| miette!("{err}").wrap_err("parsing secret key"))
} else {
IdentityFile::from_buffer(id.as_bytes())
.into_diagnostic()
.wrap_err("parsing identity")?
.into_identities()
.into_diagnostic()
.wrap_err("parsing keys from identity")?
.pop()
.ok_or_else(|| miette!("no identity available"))
}
}
fn parse_id_as_recipient(id: &str) -> Result<Box<dyn Recipient + Send>> {
if id.starts_with("age") {
id.parse::<x25519::Recipient>()
.map(|key| Box::new(key) as _)
.map_err(|err| miette!("{err}").wrap_err("parsing public key"))
} else if id.starts_with("AGE-SECRET-KEY") {
id.parse::<x25519::Identity>()
.map(|sec| Box::new(sec.to_public()) as _)
.map_err(|err| miette!("{err}").wrap_err("parsing secret key"))
} else {
IdentityFile::from_buffer(id.as_bytes())
.into_diagnostic()
.wrap_err("parsing identity")?
.to_recipients()
.into_diagnostic()
.wrap_err("parsing recipients from identity")?
.pop()
.ok_or_else(|| miette!("no recipient available in identity"))
}
}