1use age_core::secrecy::{ExposeSecret, SecretString};
4use pinentry::{ConfirmationDialog, PassphraseInput};
5use rand::{
6 distributions::{Distribution, Uniform},
7 rngs::OsRng,
8 CryptoRng, RngCore,
9};
10use rpassword::prompt_password;
11
12use std::io;
13use subtle::ConstantTimeEq;
14
15use crate::{fl, Callbacks};
16
17mod error;
18pub use error::ReadError;
19
20pub mod file_io;
21
22mod identities;
23pub use identities::read_identities;
24
25mod recipients;
26pub use recipients::read_recipients;
27
28const BIP39_WORDLIST: &str = include_str!("../assets/bip39-english.txt");
29
30pub struct StdinGuard {
32 stdin_used: bool,
33}
34
35impl StdinGuard {
36 pub fn new(input_is_stdin: bool) -> Self {
41 Self {
42 stdin_used: input_is_stdin,
43 }
44 }
45
46 fn open(&mut self, filename: String) -> Result<file_io::InputReader, ReadError> {
47 let input = file_io::InputReader::new(Some(filename))?;
48 if matches!(input, file_io::InputReader::Stdin(_)) {
49 if self.stdin_used {
50 return Err(ReadError::MultipleStdin);
51 }
52 self.stdin_used = true;
53 }
54 Ok(input)
55 }
56}
57
58fn confirm(query: &str, ok: &str, cancel: Option<&str>) -> pinentry::Result<bool> {
59 if let Some(mut input) = ConfirmationDialog::with_default_binary() {
60 input.with_ok(ok).with_timeout(30);
62 if let Some(cancel) = cancel {
63 input.with_cancel(cancel);
64 }
65 input.confirm(query)
66 } else {
67 let term = console::Term::stderr();
69 let initial = format!("{}: (y/n) ", query);
70 loop {
71 term.write_str(&initial)?;
72 let response = term.read_line()?.to_lowercase();
73 if ["y", "yes"].contains(&response.as_str()) {
74 break Ok(true);
75 } else if ["n", "no"].contains(&response.as_str()) {
76 break Ok(false);
77 }
78 }
79 }
80}
81
82pub fn read_secret(
100 description: &str,
101 prompt: &str,
102 confirm: Option<&str>,
103) -> pinentry::Result<SecretString> {
104 let input = if let Ok(pinentry) = std::env::var("PINENTRY_PROGRAM") {
107 PassphraseInput::with_binary(pinentry)
108 } else {
109 PassphraseInput::with_default_binary()
110 };
111
112 if let Some(mut input) = input {
113 let mismatch_error = fl!("cli-secret-input-mismatch");
115 let empty_error = fl!("cli-secret-input-required");
116 input
117 .with_description(description)
118 .with_prompt(prompt)
119 .with_timeout(30);
120 if let Some(confirm_prompt) = confirm {
121 input.with_confirmation(confirm_prompt, &mismatch_error);
122 } else {
123 input.required(&empty_error);
124 }
125 input.interact()
126 } else {
127 let passphrase = prompt_password(format!("{}: ", description)).map(SecretString::from)?;
129 if let Some(confirm_prompt) = confirm {
130 let confirm_passphrase =
131 prompt_password(format!("{}: ", confirm_prompt)).map(SecretString::from)?;
132
133 if !bool::from(
134 passphrase
135 .expose_secret()
136 .as_bytes()
137 .ct_eq(confirm_passphrase.expose_secret().as_bytes()),
138 ) {
139 return Err(pinentry::Error::Io(io::Error::new(
140 io::ErrorKind::InvalidInput,
141 fl!("cli-secret-input-mismatch"),
142 )));
143 }
144 } else if passphrase.expose_secret().is_empty() {
145 return Err(pinentry::Error::Cancelled);
146 }
147
148 Ok(passphrase)
149 }
150}
151
152#[derive(Clone, Copy)]
154pub struct UiCallbacks;
155
156impl Callbacks for UiCallbacks {
157 fn display_message(&self, message: &str) {
158 eprintln!("{}", message);
159 }
160
161 fn confirm(&self, message: &str, yes_string: &str, no_string: Option<&str>) -> Option<bool> {
162 confirm(message, yes_string, no_string).ok()
163 }
164
165 fn request_public_string(&self, description: &str) -> Option<String> {
166 let term = console::Term::stderr();
167 term.write_str(description).ok()?;
168 term.read_line().ok().filter(|s| !s.is_empty())
169 }
170
171 fn request_passphrase(&self, description: &str) -> Option<SecretString> {
172 read_secret(description, &fl!("cli-passphrase-prompt"), None).ok()
173 }
174}
175
176pub enum Passphrase {
178 Typed(SecretString),
180 Generated(SecretString),
182}
183
184impl Passphrase {
185 pub fn random<R: RngCore + CryptoRng>(mut rng: R) -> Self {
187 let between = Uniform::from(0..2048);
188 let new_passphrase = (0..10)
189 .map(|_| {
190 BIP39_WORDLIST
191 .lines()
192 .nth(between.sample(&mut rng))
193 .expect("index is in range")
194 })
195 .fold(String::new(), |acc, s| {
196 if acc.is_empty() {
197 acc + s
198 } else {
199 acc + "-" + s
200 }
201 });
202 Passphrase::Generated(SecretString::from(new_passphrase))
203 }
204}
205
206pub fn read_or_generate_passphrase() -> pinentry::Result<Passphrase> {
208 let res = read_secret(
209 &fl!("cli-passphrase-desc"),
210 &fl!("cli-passphrase-prompt"),
211 Some(&fl!("cli-passphrase-confirm")),
212 )?;
213
214 if res.expose_secret().is_empty() {
215 Ok(Passphrase::random(OsRng))
216 } else {
217 Ok(Passphrase::Typed(res))
218 }
219}