1use std::path::PathBuf;
2
3use age::{x25519, Identity, IdentityFile, Recipient};
4use clap::Parser;
5use miette::{bail, miette, Context as _, IntoDiagnostic as _, Result};
6use tokio::fs::read_to_string;
7
8#[derive(Debug, Clone, Parser)]
32pub struct KeyArgs {
33	#[cfg_attr(docsrs, doc("\n\n**Flag**: `-k, --key-path PATH`"))]
79	#[arg(short, long, verbatim_doc_comment)]
80	pub key_path: Option<PathBuf>,
81
82	#[cfg_attr(docsrs, doc("\n\n**Flag**: `-K, --key STRING`"))]
106	#[arg(short = 'K', verbatim_doc_comment, conflicts_with = "key_path")]
107	pub key: Option<String>,
108
109	#[command(flatten)]
110	#[allow(missing_docs, reason = "don't interfere with clap")]
111	pub pass: crate::passphrases::PassphraseArgs,
112}
113
114impl KeyArgs {
115	pub async fn get_secret_key(&self) -> Result<Option<Box<dyn Identity>>> {
122		self.secret_key(false).await
123	}
124
125	pub async fn require_secret_key(&self) -> Result<Box<dyn Identity>> {
129		self.secret_key(true)
130			.await
131			.transpose()
132			.expect("BUG: when required:true, Some must not be produced")
133	}
134
135	pub async fn get_public_key(&self) -> Result<Option<Box<dyn Recipient + Send>>> {
142		self.public_key(false).await
143	}
144
145	pub async fn require_public_key(&self) -> Result<Box<dyn Recipient + Send>> {
149		self.public_key(true)
150			.await
151			.transpose()
152			.expect("BUG: when required:true, Some must not be produced")
153	}
154
155	async fn secret_key(&self, required: bool) -> Result<Option<Box<dyn Identity>>> {
156		match self {
157			Self {
158				key_path: None,
159				key: None,
160				..
161			} => {
162				if required {
163					bail!("one of `--key-path` or `--key` must be provided");
164				} else {
165					Ok(None)
166				}
167			}
168			Self {
169				key_path: Some(_),
170				key: Some(_),
171				..
172			} => {
173				bail!("one of `--key-path` or `--key` must be provided, not both");
174			}
175			Self { key: Some(key), .. } => key
176				.parse::<x25519::Identity>()
177				.map(|sec| Some(Box::new(sec) as _))
178				.map_err(|err| miette!("{err}").wrap_err("parsing secret key")),
179			Self {
180				key_path: Some(path),
181				pass,
182				..
183			} if path.extension().unwrap_or_default() == "age" => {
184				let key = tokio::fs::read(path).await.into_diagnostic()?;
185				let pass = pass.require().await?;
186				let id = age::decrypt(&pass, &key)
187					.into_diagnostic()
188					.wrap_err("revealing identity file")?;
189
190				parse_id_as_identity(
191					&String::from_utf8(id)
192						.into_diagnostic()
193						.wrap_err("parsing identity file as UTF-8")?,
194				)
195				.map(Some)
196			}
197			Self {
198				key_path: Some(path),
199				..
200			} => {
201				let key = read_to_string(&path)
202					.await
203					.into_diagnostic()
204					.wrap_err("reading identity file")?;
205				parse_id_as_identity(&key).map(Some)
206			}
207		}
208	}
209
210	async fn public_key(&self, required: bool) -> Result<Option<Box<dyn Recipient + Send>>> {
211		match self {
212			Self {
213				key_path: None,
214				key: None,
215				..
216			} => {
217				if required {
218					bail!("one of `--key-path` or `--key` must be provided");
219				} else {
220					Ok(None)
221				}
222			}
223			Self {
224				key_path: Some(_),
225				key: Some(_),
226				..
227			} => {
228				bail!("one of `--key-path` or `--key` must be provided, not both");
229			}
230			Self { key: Some(key), .. } if key.starts_with("age") => key
231				.parse::<x25519::Recipient>()
232				.map(|key| Some(Box::new(key) as _))
233				.map_err(|err| miette!("{err}").wrap_err("parsing public key")),
234			Self { key: Some(key), .. } if key.starts_with("AGE-SECRET-KEY") => key
235				.parse::<x25519::Identity>()
236				.map(|sec| Some(Box::new(sec.to_public()) as _))
237				.map_err(|err| miette!("{err}").wrap_err("parsing key")),
238			Self { key: Some(_), .. } => {
239				bail!("value passed to `--key` is not a public or secret age key");
240			}
241			Self {
242				key_path: Some(path),
243				pass,
244				..
245			} if path.extension().unwrap_or_default() == "age" => {
246				let key = tokio::fs::read(path).await.into_diagnostic()?;
247				let pass = pass.require().await?;
248				let id = age::decrypt(&pass, &key)
249					.into_diagnostic()
250					.wrap_err("revealing identity file")?;
251
252				parse_id_as_recipient(
253					&String::from_utf8(id)
254						.into_diagnostic()
255						.wrap_err("parsing identity file as UTF-8")?,
256				)
257				.map(Some)
258			}
259			Self {
260				key_path: Some(path),
261				..
262			} => {
263				let key = read_to_string(path)
264					.await
265					.into_diagnostic()
266					.wrap_err("reading identity file")?;
267				parse_id_as_recipient(&key).map(Some)
268			}
269		}
270	}
271}
272
273fn parse_id_as_identity(id: &str) -> Result<Box<dyn Identity>> {
274	if id.starts_with("AGE-SECRET-KEY") {
275		id.parse::<x25519::Identity>()
276			.map(|sec| Box::new(sec) as _)
277			.map_err(|err| miette!("{err}").wrap_err("parsing secret key"))
278	} else {
279		IdentityFile::from_buffer(id.as_bytes())
280			.into_diagnostic()
281			.wrap_err("parsing identity")?
282			.into_identities()
283			.into_diagnostic()
284			.wrap_err("parsing keys from identity")?
285			.pop()
286			.ok_or_else(|| miette!("no identity available"))
287	}
288}
289
290fn parse_id_as_recipient(id: &str) -> Result<Box<dyn Recipient + Send>> {
291	if id.starts_with("age") {
292		id.parse::<x25519::Recipient>()
293			.map(|key| Box::new(key) as _)
294			.map_err(|err| miette!("{err}").wrap_err("parsing public key"))
295	} else if id.starts_with("AGE-SECRET-KEY") {
296		id.parse::<x25519::Identity>()
297			.map(|sec| Box::new(sec.to_public()) as _)
298			.map_err(|err| miette!("{err}").wrap_err("parsing secret key"))
299	} else {
300		IdentityFile::from_buffer(id.as_bytes())
301			.into_diagnostic()
302			.wrap_err("parsing identity")?
303			.to_recipients()
304			.into_diagnostic()
305			.wrap_err("parsing recipients from identity")?
306			.pop()
307			.ok_or_else(|| miette!("no recipient available in identity"))
308	}
309}