Skip to main content

algae_cli/
passphrases.rs

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