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}