algae_cli/
passphrases.rs

1use std::path::PathBuf;
2
3use age::{secrecy::SecretString, Identity, Recipient};
4use clap::Parser;
5use dialoguer::Password;
6use miette::{miette, Context as _, IntoDiagnostic as _, Result};
7use pinentry::PassphraseInput;
8use tokio::fs::read_to_string;
9
10/// [Clap][clap] arguments for passphrases.
11///
12/// ```no_run
13/// use clap::Parser;
14/// use miette::Result;
15/// use algae_cli::passphrases::PassphraseArgs;
16///
17/// /// Your CLI tool
18/// #[derive(Parser)]
19/// struct Args {
20///     #[command(flatten)]
21///     pass: PassphraseArgs,
22/// }
23///
24/// #[tokio::main]
25/// async fn main() -> Result<()> {
26///     let args = Args::parse();
27///     let key = args.pass.require().await?;
28///     // use key somehow...
29/// # let _key = key;
30///     Ok(())
31/// }
32/// ```
33#[derive(Debug, Clone, Parser)]
34pub struct PassphraseArgs {
35	/// Path to a file containing a passphrase.
36	///
37	/// The contents of the file will be trimmed of whitespace.
38	#[cfg_attr(docsrs, doc("\n\n**Flag**: `-P, --passphrase-path PATH`"))]
39	#[arg(short = 'P', long)]
40	pub passphrase_path: Option<PathBuf>,
41
42	/// A passphrase as a string.
43	///
44	/// This is extremely insecure, only use when there is no other option. When on an interactive
45	/// terminal, make sure to wipe this command line from your history, or better yet not record it
46	/// in the first place (in Bash you often can do that by prepending a space to your command).
47	#[cfg_attr(docsrs, doc("\n\n**Flag**: `--insecure-passphrase STRING`"))]
48	#[arg(long, conflicts_with = "passphrase_path")]
49	pub insecure_passphrase: Option<SecretString>,
50}
51
52impl PassphraseArgs {
53	/// Retrieve a passphrase from the user.
54	pub async fn require(&self) -> Result<Passphrase> {
55		self.get(false).await
56	}
57
58	/// Retrieve a passphrase from the user, with confirmation when prompting.
59	pub async fn require_with_confirmation(&self) -> Result<Passphrase> {
60		self.get(true).await
61	}
62
63	/// Retrieve a passphrase from the user, as a [`SecretString`].
64	pub async fn require_phrase(&self) -> Result<SecretString> {
65		self.get_phrase(false).await
66	}
67
68	/// Retrieve a passphrase from the user, as a [`SecretString`], with confirmation when prompting.
69	pub async fn require_phrase_with_confirmation(&self) -> Result<SecretString> {
70		self.get_phrase(true).await
71	}
72
73	async fn get(&self, confirm: bool) -> Result<Passphrase> {
74		self.get_phrase(confirm).await.map(Passphrase::new)
75	}
76
77	async fn get_phrase(&self, confirm: bool) -> Result<SecretString> {
78		if let Some(ref phrase) = self.insecure_passphrase {
79			Ok(phrase.clone())
80		} else if let Some(ref path) = self.passphrase_path {
81			Ok(read_to_string(path)
82				.await
83				.into_diagnostic()
84				.wrap_err("reading keyfile")?
85				.trim()
86				.into())
87		} else if let Some(mut input) = PassphraseInput::with_default_binary() {
88			input
89				.with_prompt("Passphrase:")
90				.required("Cannot use an empty passphrase");
91			if confirm {
92				input.with_confirmation("Confirm passphrase:", "Passphrases do not match");
93			}
94			input.interact().map_err(|err| miette!("{err}"))
95		} else {
96			let mut prompt = Password::new().with_prompt("Passphrase");
97			if confirm {
98				prompt = prompt.with_confirmation("Confirm passphrase", "Passphrases do not match");
99			}
100			let phrase = prompt.interact().into_diagnostic()?;
101			Ok(phrase.into())
102		}
103	}
104}
105
106/// A wrapper around [`age::scrypt::Recipient`] and [`age::scrypt::Identity`].
107///
108/// Such that a single struct implements both [`Recipient`] and [`Identity`]
109/// traits for a single passphrase, simplifying usage.
110pub struct Passphrase(age::scrypt::Recipient, age::scrypt::Identity);
111
112impl Passphrase {
113	/// Initialise from a string.
114	pub fn new(secret: SecretString) -> Self {
115		Self(
116			age::scrypt::Recipient::new(secret.clone()),
117			age::scrypt::Identity::new(secret),
118		)
119	}
120}
121
122impl Recipient for Passphrase {
123	fn wrap_file_key(
124		&self,
125		file_key: &age_core::format::FileKey,
126	) -> std::result::Result<
127		(
128			Vec<age_core::format::Stanza>,
129			std::collections::HashSet<String>,
130		),
131		age::EncryptError,
132	> {
133		self.0.wrap_file_key(file_key)
134	}
135}
136
137impl Identity for Passphrase {
138	fn unwrap_stanza(
139		&self,
140		stanza: &age_core::format::Stanza,
141	) -> Option<std::result::Result<age_core::format::FileKey, age::DecryptError>> {
142		self.1.unwrap_stanza(stanza)
143	}
144
145	fn unwrap_stanzas(
146		&self,
147		stanzas: &[age_core::format::Stanza],
148	) -> Option<std::result::Result<age_core::format::FileKey, age::DecryptError>> {
149		self.1.unwrap_stanzas(stanzas)
150	}
151}