1use std::path::PathBuf;
2
3use age::{
4 secrecy::{ExposeSecret, SecretString},
5 x25519,
6};
7use clap::Parser;
8use itertools::Itertools;
9use miette::{Context as _, IntoDiagnostic as _, Result};
10use tokio::{fs::File, io::AsyncWriteExt};
11
12use crate::passphrases::{Passphrase, PassphraseArgs};
13
14#[derive(Debug, Clone, Parser)]
46#[clap(verbatim_doc_comment)]
47pub struct KeygenArgs {
48 #[arg(short, long)]
52 pub output: Option<PathBuf>,
53
54 #[arg(long = "public", default_value = "identity.pub")]
59 pub public_path: PathBuf,
60
61 #[arg(long)]
63 pub plaintext: bool,
64
65 #[arg(short = 'R', long, conflicts_with = "plaintext")]
70 pub random_passphrase: bool,
71
72 #[command(flatten)]
73 #[allow(missing_docs, reason = "don't interfere with clap")]
74 pub key: PassphraseArgs,
75}
76
77pub async fn run(
79 KeygenArgs {
80 output,
81 public_path,
82 plaintext,
83 random_passphrase,
84 key,
85 }: KeygenArgs,
86) -> Result<()> {
87 let secret = x25519::Identity::generate();
88 let public = secret.to_public();
89
90 let output = output.unwrap_or_else(|| {
91 if plaintext {
92 "identity.txt"
93 } else {
94 "identity.txt.age"
95 }
96 .into()
97 });
98
99 let identity = SecretString::from(format!(
100 "# created: {}\n# public key: {public}\n{}\n",
101 jiff::Timestamp::now(),
102 secret.to_string().expose_secret()
103 ));
104
105 let identity = if plaintext {
106 identity.expose_secret().as_bytes().to_owned()
107 } else {
108 let key = if random_passphrase {
109 use rand::prelude::*;
110 let rng = &mut rand::rng();
111 let words: [_; 6] = diceware_wordlists::MINILOCK_WORDLIST
112 .choose_multiple_array(rng)
113 .unwrap();
114
115 let phrase: String = Itertools::intersperse(words.into_iter(), "-").collect();
116 println!("passphrase: {phrase}");
117
118 Passphrase::new(phrase.into())
119 } else {
120 key.require_with_confirmation().await?
121 };
122 age::encrypt(&key, identity.expose_secret().as_bytes()).into_diagnostic()?
123 };
124
125 File::create_new(&output)
126 .await
127 .into_diagnostic()
128 .wrap_err("opening the identity file")?
129 .write_all(&identity)
130 .await
131 .into_diagnostic()
132 .wrap_err("writing the identity")?;
133
134 println!("public key: {public}");
135 if public_path.to_string_lossy() != "-" {
136 File::create_new(&public_path)
137 .await
138 .into_diagnostic()
139 .wrap_err("opening the public key file")?
140 .write_all(public.to_string().as_bytes())
141 .await
142 .into_diagnostic()
143 .wrap_err("writing the public key")?;
144 }
145
146 Ok(())
147}