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