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::fmt;
use std::fs::File;
use std::io::{self, BufReader};
use subtle::ConstantTimeEq;
use crate::{fl, identity::IdentityFile, wfl, Callbacks, Identity};
#[cfg(feature = "armor")]
use crate::armor::ArmoredReader;
pub mod file_io;
const BIP39_WORDLIST: &str = include_str!("../assets/bip39-english.txt");
#[derive(Debug)]
pub enum ReadError {
IdentityEncryptedWithoutPassphrase(String),
IdentityNotFound(String),
Io(io::Error),
#[cfg(feature = "plugin")]
#[cfg_attr(docsrs, doc(cfg(feature = "plugin")))]
MissingPlugin {
binary_name: String,
},
#[cfg(feature = "ssh")]
#[cfg_attr(docsrs, doc(cfg(feature = "ssh")))]
UnsupportedKey(String, crate::ssh::UnsupportedKey),
}
impl From<io::Error> for ReadError {
fn from(e: io::Error) -> Self {
ReadError::Io(e)
}
}
impl fmt::Display for ReadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ReadError::IdentityEncryptedWithoutPassphrase(filename) => {
write!(
f,
"{}",
i18n_embed_fl::fl!(
crate::i18n::LANGUAGE_LOADER,
"err-read-identity-encrypted-without-passphrase",
filename = filename.as_str()
)
)
}
ReadError::IdentityNotFound(filename) => write!(
f,
"{}",
i18n_embed_fl::fl!(
crate::i18n::LANGUAGE_LOADER,
"err-read-identity-not-found",
filename = filename.as_str()
)
),
ReadError::Io(e) => write!(f, "{}", e),
#[cfg(feature = "plugin")]
ReadError::MissingPlugin { binary_name } => {
writeln!(
f,
"{}",
i18n_embed_fl::fl!(
crate::i18n::LANGUAGE_LOADER,
"err-missing-plugin",
plugin_name = binary_name.as_str()
)
)?;
wfl!(f, "rec-missing-plugin")
}
#[cfg(feature = "ssh")]
ReadError::UnsupportedKey(filename, k) => k.display(f, Some(filename.as_str())),
}
}
}
impl std::error::Error for ReadError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(inner) => Some(inner),
_ => None,
}
}
}
pub fn read_identities(
filenames: Vec<String>,
max_work_factor: Option<u8>,
) -> Result<Vec<Box<dyn Identity>>, ReadError> {
let mut identities: Vec<Box<dyn Identity>> = vec![];
for filename in filenames {
#[cfg(feature = "armor")]
if let Ok(identity) = crate::encrypted::Identity::from_buffer(
ArmoredReader::new(BufReader::new(File::open(&filename)?)),
Some(filename.clone()),
UiCallbacks,
max_work_factor,
) {
if let Some(identity) = identity {
identities.push(Box::new(identity));
continue;
} else {
return Err(ReadError::IdentityEncryptedWithoutPassphrase(filename));
}
}
#[cfg(feature = "ssh")]
match crate::ssh::Identity::from_buffer(
BufReader::new(File::open(&filename)?),
Some(filename.clone()),
) {
Ok(crate::ssh::Identity::Unsupported(k)) => {
return Err(ReadError::UnsupportedKey(filename, k))
}
Ok(identity) => {
identities.push(Box::new(identity.with_callbacks(UiCallbacks)));
continue;
}
Err(_) => (),
}
let identity_file =
IdentityFile::from_file(filename.clone()).map_err(|e| match e.kind() {
io::ErrorKind::NotFound => ReadError::IdentityNotFound(filename),
_ => e.into(),
})?;
for entry in identity_file.into_identities() {
let entry = entry.into_identity(UiCallbacks);
#[cfg(feature = "plugin")]
let entry = entry.map_err(|e| match e {
#[cfg(feature = "plugin")]
crate::DecryptError::MissingPlugin { binary_name } => {
ReadError::MissingPlugin { binary_name }
}
_ => unreachable!(),
})?;
#[cfg(not(feature = "plugin"))]
let entry = entry.unwrap();
identities.push(entry);
}
}
Ok(identities)
}
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 {
let response = term.read_line_initial_text(&initial)?.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> {
if let Some(mut input) = PassphraseInput::with_default_binary() {
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::new)?;
if let Some(confirm_prompt) = confirm {
let confirm_passphrase =
prompt_password(format!("{}: ", confirm_prompt)).map(SecretString::new)?;
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.read_line_initial_text(description)
.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::new(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))
}
}