1use crate::{
4 Application, RUSTIC_APP, helpers::table_with_titles, repository::OpenRepo, status_err,
5};
6
7use std::path::PathBuf;
8
9use abscissa_core::{Command, Runnable, Shutdown};
10use anyhow::{Result, bail};
11use dialoguer::Password;
12use log::{info, warn};
13
14use qrcode::{QrCode, render::svg};
15use rustic_core::{
16 CommandInput, CredentialOptions, Credentials, KeyOptions,
17 repofile::{KeyFile, MasterKey},
18};
19
20#[derive(clap::Parser, Command, Debug)]
22pub(super) struct KeyCmd {
23 #[clap(subcommand)]
25 cmd: KeySubCmd,
26}
27
28impl Runnable for KeyCmd {
29 fn run(&self) {
30 self.cmd.run();
31 }
32}
33
34#[derive(clap::Subcommand, Debug, Runnable)]
35enum KeySubCmd {
36 Add(AddCmd),
38 List(ListCmd),
40 Remove(RemoveCmd),
42 Password(PasswordCmd),
44 Export(ExportCmd),
46 Create(CreateCmd),
48}
49
50#[derive(clap::Parser, Debug)]
51pub(crate) struct NewPasswordOptions {
52 #[clap(long)]
54 pub(crate) new_password: Option<String>,
55
56 #[clap(long)]
58 pub(crate) new_password_file: Option<PathBuf>,
59
60 #[clap(long)]
62 pub(crate) new_password_command: Option<CommandInput>,
63}
64
65impl NewPasswordOptions {
66 fn pass(&self, text: &str) -> Result<String> {
67 let mut pass_opts = CredentialOptions::default();
69 pass_opts.password = self.new_password.clone();
70 pass_opts.password_file = self.new_password_file.clone();
71 pass_opts.password_command = self.new_password_command.clone();
72
73 let pass = if let Some(Credentials::Password(pass)) = pass_opts.credentials()? {
74 pass
75 } else {
76 Password::new()
77 .with_prompt(text)
78 .allow_empty_password(true)
79 .with_confirmation("confirm password", "passwords do not match")
80 .interact()?
81 };
82 Ok(pass)
83 }
84}
85
86#[derive(clap::Parser, Debug)]
87pub(crate) struct AddCmd {
88 #[clap(flatten)]
90 pub(crate) pass_opts: NewPasswordOptions,
91
92 #[clap(flatten)]
94 pub(crate) key_opts: KeyOptions,
95}
96
97impl Runnable for AddCmd {
98 fn run(&self) {
99 if let Err(err) = RUSTIC_APP
100 .config()
101 .repository
102 .run_open(|repo| self.inner_run(repo))
103 {
104 status_err!("{}", err);
105 RUSTIC_APP.shutdown(Shutdown::Crash);
106 };
107 }
108}
109
110impl AddCmd {
111 fn inner_run(&self, repo: OpenRepo) -> Result<()> {
112 if RUSTIC_APP.config().global.dry_run {
113 info!("adding no key in dry-run mode.");
114 return Ok(());
115 }
116 let pass = self.pass_opts.pass("enter password for new key")?;
117 let id = repo.add_key(&pass, &self.key_opts)?;
118 info!("key {id} successfully added.");
119
120 Ok(())
121 }
122}
123
124#[derive(clap::Parser, Debug)]
125pub(crate) struct ListCmd;
126
127impl Runnable for ListCmd {
128 fn run(&self) {
129 if let Err(err) = RUSTIC_APP
130 .config()
131 .repository
132 .run_open(|repo| self.inner_run(repo))
133 {
134 status_err!("{}", err);
135 RUSTIC_APP.shutdown(Shutdown::Crash);
136 };
137 }
138}
139
140impl ListCmd {
141 fn inner_run(&self, repo: OpenRepo) -> Result<()> {
142 let used_key = repo.key_id();
143 let keys = repo
144 .stream_files()?
145 .inspect(|f| {
146 if let Err(err) = f {
147 warn!("{err:?}");
148 }
149 })
150 .filter_map(Result::ok);
151
152 let mut table = table_with_titles(["ID", "User", "Host", "Created"]);
153 _ = table.add_rows(keys.map(|key: (_, KeyFile)| {
154 [
155 format!(
156 "{}{}",
157 if used_key == &Some(key.0) { "*" } else { "" },
158 key.0
159 ),
160 key.1.username.unwrap_or_default(),
161 key.1.hostname.unwrap_or_default(),
162 key.1
163 .created
164 .map_or(String::new(), |time| format!("{time}")),
165 ]
166 }));
167 println!("{table}");
168 Ok(())
169 }
170}
171
172#[derive(clap::Parser, Debug)]
173pub(crate) struct RemoveCmd {
174 ids: Vec<String>,
176}
177
178impl Runnable for RemoveCmd {
179 fn run(&self) {
180 if let Err(err) = RUSTIC_APP
181 .config()
182 .repository
183 .run_open(|repo| self.inner_run(repo))
184 {
185 status_err!("{}", err);
186 RUSTIC_APP.shutdown(Shutdown::Crash);
187 };
188 }
189}
190
191impl RemoveCmd {
192 fn inner_run(&self, repo: OpenRepo) -> Result<()> {
193 let repo_key = repo.key_id();
194 let ids: Vec<_> = repo.find_ids(&self.ids)?.collect();
195 if ids.iter().any(|id| Some(id) == repo_key.as_ref()) {
196 bail!("Cannot remove currently used key!");
197 }
198 if !RUSTIC_APP.config().global.dry_run {
199 for id in ids {
200 repo.delete_key(&id)?;
201 info!("key {id} successfully removed.");
202 }
203 return Ok(());
204 }
205
206 let keys = repo
207 .stream_files_list(&ids)?
208 .inspect(|f| {
209 if let Err(err) = f {
210 warn!("{err:?}");
211 }
212 })
213 .filter_map(Result::ok);
214
215 let mut table = table_with_titles(["ID", "User", "Host", "Created"]);
216 _ = table.add_rows(keys.map(|key: (_, KeyFile)| {
217 [
218 key.0.to_string(),
219 key.1.username.unwrap_or_default(),
220 key.1.hostname.unwrap_or_default(),
221 key.1
222 .created
223 .map_or(String::new(), |time| format!("{time}")),
224 ]
225 }));
226 println!("would have removed the following keys:");
227 println!("{table}");
228 Ok(())
229 }
230}
231
232#[derive(clap::Parser, Debug)]
233pub(crate) struct PasswordCmd {
234 #[clap(flatten)]
236 pub(crate) pass_opts: NewPasswordOptions,
237}
238
239impl Runnable for PasswordCmd {
240 fn run(&self) {
241 if let Err(err) = RUSTIC_APP
242 .config()
243 .repository
244 .run_open(|repo| self.inner_run(repo))
245 {
246 status_err!("{}", err);
247 RUSTIC_APP.shutdown(Shutdown::Crash);
248 };
249 }
250}
251
252impl PasswordCmd {
253 fn inner_run(&self, repo: OpenRepo) -> Result<()> {
254 let Some(key_id) = repo.key_id() else {
255 bail!("No keyfile used to open the repo. Cannot change the password.")
256 };
257 if RUSTIC_APP.config().global.dry_run {
258 info!("changing no password in dry-run mode.");
259 return Ok(());
260 }
261 let pass = self.pass_opts.pass("enter new password")?;
262 let old_key: KeyFile = repo.get_file(key_id)?;
263 let key_opts = KeyOptions::default()
264 .hostname(old_key.hostname)
265 .username(old_key.username)
266 .with_created(old_key.created.is_some());
267 let id = repo.add_key(&pass, &key_opts)?;
268 info!("key {id} successfully added.");
269
270 let old_key = *key_id; let repo = repo.open(&Credentials::Password(pass))?;
273 repo.delete_key(&old_key)?;
274 info!("key {old_key} successfully removed.");
275
276 Ok(())
277 }
278}
279
280#[derive(clap::Parser, Debug)]
281pub(crate) struct ExportCmd {
282 pub(crate) file: Option<PathBuf>,
284
285 #[clap(long)]
287 pub(crate) qr: bool,
288}
289
290impl Runnable for ExportCmd {
291 fn run(&self) {
292 if let Err(err) = RUSTIC_APP.config().repository.run_open(|repo| {
293 let mut data = serde_json::to_string(&repo.key())?;
294 if self.qr {
295 let qr = QrCode::new(&data)?;
296 data = qr.render::<svg::Color<'_>>().build();
297 }
298 match &self.file {
299 None => println!("{}", data),
300 Some(file) => std::fs::write(file, data)?,
301 }
302 Ok(())
303 }) {
304 status_err!("{}", err);
305 RUSTIC_APP.shutdown(Shutdown::Crash);
306 };
307 }
308}
309
310#[derive(clap::Parser, Debug)]
311pub(crate) struct CreateCmd {
312 pub(crate) file: Option<PathBuf>,
314}
315
316impl Runnable for CreateCmd {
317 fn run(&self) {
318 let inner = || -> Result<_> {
319 let data = serde_json::to_string(&MasterKey::new())?;
320 match &self.file {
321 None => println!("{}", data),
322 Some(file) => std::fs::write(file, data)?,
323 }
324 Ok(())
325 };
326 if let Err(err) = inner() {
327 status_err!("{}", err);
328 RUSTIC_APP.shutdown(Shutdown::Crash);
329 };
330 }
331}