algae_cli/
passphrases.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
use std::path::PathBuf;

use age::{secrecy::SecretString, Identity, Recipient};
use clap::Parser;
use dialoguer::Password;
use miette::{miette, Context as _, IntoDiagnostic as _, Result};
use pinentry::PassphraseInput;
use tokio::fs::read_to_string;

/// [Clap][clap] arguments for passphrases.
///
/// ```no_run
/// use clap::Parser;
/// use miette::Result;
/// use algae_cli::passphrases::PassphraseArgs;
///
/// /// Your CLI tool
/// #[derive(Parser)]
/// struct Args {
///     #[command(flatten)]
///     pass: PassphraseArgs,
/// }
///
/// #[tokio::main]
/// async fn main() -> Result<()> {
///     let args = Args::parse();
///     let key = args.pass.require().await?;
///     // use key somehow...
/// # let _key = key;
///     Ok(())
/// }
/// ```
#[derive(Debug, Clone, Parser)]
pub struct PassphraseArgs {
	/// Path to a file containing a passphrase.
	///
	/// The contents of the file will be trimmed of whitespace.
	#[cfg_attr(docsrs, doc("\n\n**Flag**: `-P, --passphrase-path PATH`"))]
	#[arg(short = 'P', long)]
	pub passphrase_path: Option<PathBuf>,

	/// A passphrase as a string.
	///
	/// This is extremely insecure, only use when there is no other option. When on an interactive
	/// terminal, make sure to wipe this command line from your history, or better yet not record it
	/// in the first place (in Bash you often can do that by prepending a space to your command).
	#[cfg_attr(docsrs, doc("\n\n**Flag**: `--insecure-passphrase STRING`"))]
	#[arg(long, conflicts_with = "passphrase_path")]
	pub insecure_passphrase: Option<SecretString>,
}

impl PassphraseArgs {
	/// Retrieve a passphrase from the user.
	pub async fn require(&self) -> Result<Passphrase> {
		self.get(false).await
	}

	/// Retrieve a passphrase from the user, with confirmation when prompting.
	pub async fn require_with_confirmation(&self) -> Result<Passphrase> {
		self.get(true).await
	}

	/// Retrieve a passphrase from the user, as a [`SecretString`].
	pub async fn require_phrase(&self) -> Result<SecretString> {
		self.get_phrase(false).await
	}

	/// Retrieve a passphrase from the user, as a [`SecretString`], with confirmation when prompting.
	pub async fn require_phrase_with_confirmation(&self) -> Result<SecretString> {
		self.get_phrase(true).await
	}

	async fn get(&self, confirm: bool) -> Result<Passphrase> {
		self.get_phrase(confirm).await.map(Passphrase::new)
	}

	async fn get_phrase(&self, confirm: bool) -> Result<SecretString> {
		if let Some(ref phrase) = self.insecure_passphrase {
			Ok(phrase.clone())
		} else if let Some(ref path) = self.passphrase_path {
			Ok(read_to_string(path)
				.await
				.into_diagnostic()
				.wrap_err("reading keyfile")?
				.trim()
				.into())
		} else {
			if let Some(mut input) = PassphraseInput::with_default_binary() {
				input
					.with_prompt("Passphrase:")
					.required("Cannot use an empty passphrase");
				if confirm {
					input.with_confirmation("Confirm passphrase:", "Passphrases do not match");
				}
				input.interact().map_err(|err| miette!("{err}"))
			} else {
				let mut prompt = Password::new().with_prompt("Passphrase");
				if confirm {
					prompt =
						prompt.with_confirmation("Confirm passphrase", "Passphrases do not match");
				}
				let phrase = prompt.interact().into_diagnostic()?;
				Ok(phrase.into())
			}
		}
	}
}

/// A wrapper around [`age::scrypt::Recipient`] and [`age::scrypt::Identity`].
///
/// Such that a single struct implements both [`Recipient`] and [`Identity`]
/// traits for a single passphrase, simplifying usage.
pub struct Passphrase(age::scrypt::Recipient, age::scrypt::Identity);

impl Passphrase {
	/// Initialise from a string.
	pub fn new(secret: SecretString) -> Self {
		Self(
			age::scrypt::Recipient::new(secret.clone()),
			age::scrypt::Identity::new(secret),
		)
	}
}

impl Recipient for Passphrase {
	fn wrap_file_key(
		&self,
		file_key: &age_core::format::FileKey,
	) -> std::result::Result<
		(
			Vec<age_core::format::Stanza>,
			std::collections::HashSet<String>,
		),
		age::EncryptError,
	> {
		self.0.wrap_file_key(file_key)
	}
}

impl Identity for Passphrase {
	fn unwrap_stanza(
		&self,
		stanza: &age_core::format::Stanza,
	) -> Option<std::result::Result<age_core::format::FileKey, age::DecryptError>> {
		self.1.unwrap_stanza(stanza)
	}

	fn unwrap_stanzas(
		&self,
		stanzas: &[age_core::format::Stanza],
	) -> Option<std::result::Result<age_core::format::FileKey, age::DecryptError>> {
		self.1.unwrap_stanzas(stanzas)
	}
}