use age_core::secrecy::{ExposeSecret, SecretString};
use pinentry::{ConfirmationDialog, PassphraseInput};
use rand::{
distributions::{Distribution, Uniform},
rngs::OsRng,
CryptoRng, RngCore,
};
use rpassword::prompt_password;
use std::io;
use subtle::ConstantTimeEq;
use crate::{fl, Callbacks};
mod error;
pub use error::ReadError;
pub mod file_io;
mod identities;
pub use identities::read_identities;
mod recipients;
pub use recipients::read_recipients;
const BIP39_WORDLIST: &str = include_str!("../assets/bip39-english.txt");
pub struct StdinGuard {
stdin_used: bool,
}
impl StdinGuard {
pub fn new(input_is_stdin: bool) -> Self {
Self {
stdin_used: input_is_stdin,
}
}
fn open(&mut self, filename: String) -> Result<file_io::InputReader, ReadError> {
let input = file_io::InputReader::new(Some(filename))?;
if matches!(input, file_io::InputReader::Stdin(_)) {
if self.stdin_used {
return Err(ReadError::MultipleStdin);
}
self.stdin_used = true;
}
Ok(input)
}
}
fn confirm(query: &str, ok: &str, cancel: Option<&str>) -> pinentry::Result<bool> {
if let Some(mut input) = ConfirmationDialog::with_default_binary() {
input.with_ok(ok).with_timeout(30);
if let Some(cancel) = cancel {
input.with_cancel(cancel);
}
input.confirm(query)
} else {
let term = console::Term::stderr();
let initial = format!("{}: (y/n) ", query);
loop {
term.write_str(&initial)?;
let response = term.read_line()?.to_lowercase();
if ["y", "yes"].contains(&response.as_str()) {
break Ok(true);
} else if ["n", "no"].contains(&response.as_str()) {
break Ok(false);
}
}
}
}
pub fn read_secret(
description: &str,
prompt: &str,
confirm: Option<&str>,
) -> pinentry::Result<SecretString> {
let input = if let Ok(pinentry) = std::env::var("PINENTRY_PROGRAM") {
PassphraseInput::with_binary(pinentry)
} else {
PassphraseInput::with_default_binary()
};
if let Some(mut input) = input {
let mismatch_error = fl!("cli-secret-input-mismatch");
let empty_error = fl!("cli-secret-input-required");
input
.with_description(description)
.with_prompt(prompt)
.with_timeout(30);
if let Some(confirm_prompt) = confirm {
input.with_confirmation(confirm_prompt, &mismatch_error);
} else {
input.required(&empty_error);
}
input.interact()
} else {
let passphrase = prompt_password(format!("{}: ", description)).map(SecretString::from)?;
if let Some(confirm_prompt) = confirm {
let confirm_passphrase =
prompt_password(format!("{}: ", confirm_prompt)).map(SecretString::from)?;
if !bool::from(
passphrase
.expose_secret()
.as_bytes()
.ct_eq(confirm_passphrase.expose_secret().as_bytes()),
) {
return Err(pinentry::Error::Io(io::Error::new(
io::ErrorKind::InvalidInput,
fl!("cli-secret-input-mismatch"),
)));
}
} else if passphrase.expose_secret().is_empty() {
return Err(pinentry::Error::Cancelled);
}
Ok(passphrase)
}
}
#[derive(Clone, Copy)]
pub struct UiCallbacks;
impl Callbacks for UiCallbacks {
fn display_message(&self, message: &str) {
eprintln!("{}", message);
}
fn confirm(&self, message: &str, yes_string: &str, no_string: Option<&str>) -> Option<bool> {
confirm(message, yes_string, no_string).ok()
}
fn request_public_string(&self, description: &str) -> Option<String> {
let term = console::Term::stderr();
term.write_str(description).ok()?;
term.read_line().ok().filter(|s| !s.is_empty())
}
fn request_passphrase(&self, description: &str) -> Option<SecretString> {
read_secret(description, &fl!("cli-passphrase-prompt"), None).ok()
}
}
pub enum Passphrase {
Typed(SecretString),
Generated(SecretString),
}
impl Passphrase {
pub fn random<R: RngCore + CryptoRng>(mut rng: R) -> Self {
let between = Uniform::from(0..2048);
let new_passphrase = (0..10)
.map(|_| {
BIP39_WORDLIST
.lines()
.nth(between.sample(&mut rng))
.expect("index is in range")
})
.fold(String::new(), |acc, s| {
if acc.is_empty() {
acc + s
} else {
acc + "-" + s
}
});
Passphrase::Generated(SecretString::from(new_passphrase))
}
}
pub fn read_or_generate_passphrase() -> pinentry::Result<Passphrase> {
let res = read_secret(
&fl!("cli-passphrase-desc"),
&fl!("cli-passphrase-prompt"),
Some(&fl!("cli-passphrase-confirm")),
)?;
if res.expose_secret().is_empty() {
Ok(Passphrase::random(OsRng))
} else {
Ok(Passphrase::Typed(res))
}
}