Compare commits

..

No commits in common. "c46f9e48b7aa0258b704ae1e976930279abee7be" and "d7bf3d16e14ca236185860c2a6b777e6d8280d72" have entirely different histories.

15 changed files with 182 additions and 410 deletions

View File

@ -11,7 +11,7 @@ pub struct BincodeLayer<'a, Request> {
phantom_request: PhantomData<&'a Request>, phantom_request: PhantomData<&'a Request>,
} }
impl<Request> BincodeLayer<'_, Request> { impl<'a, Request> BincodeLayer<'a, Request> {
/// Create a new [`BincodeLayer`]. /// Create a new [`BincodeLayer`].
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@ -21,7 +21,7 @@ impl<Request> BincodeLayer<'_, Request> {
} }
} }
impl<Request> Default for BincodeLayer<'_, Request> { impl<'a, Request> Default for BincodeLayer<'a, Request> {
fn default() -> Self { fn default() -> Self {
Self::new() Self::new()
} }

View File

@ -7,7 +7,7 @@ use std::{
process::ExitCode, process::ExitCode,
}; };
use keyfork_prompt::default_handler; use keyfork_prompt::{DefaultTerminal, default_terminal};
use keyfork_shard::{openpgp::OpenPGP, Format}; use keyfork_shard::{openpgp::OpenPGP, Format};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>; type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
@ -32,8 +32,8 @@ fn run() -> Result<()> {
_ => panic!("Usage: {program_name} <shard> [key_discovery]"), _ => panic!("Usage: {program_name} <shard> [key_discovery]"),
}; };
let openpgp = OpenPGP; let openpgp = OpenPGP::<DefaultTerminal>::new();
let prompt_handler = default_handler()?; let prompt_handler = default_terminal()?;
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery.as_deref(), messages_file, prompt_handler)?; let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery.as_deref(), messages_file, prompt_handler)?;
print!("{}", smex::encode(bytes)); print!("{}", smex::encode(bytes));

View File

@ -7,7 +7,7 @@ use std::{
process::ExitCode, process::ExitCode,
}; };
use keyfork_prompt::default_handler; use keyfork_prompt::{DefaultTerminal, default_terminal};
use keyfork_shard::{Format, openpgp::OpenPGP}; use keyfork_shard::{Format, openpgp::OpenPGP};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>; type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
@ -32,8 +32,8 @@ fn run() -> Result<()> {
_ => panic!("Usage: {program_name} <shard> [key_discovery]"), _ => panic!("Usage: {program_name} <shard> [key_discovery]"),
}; };
let openpgp = OpenPGP; let openpgp = OpenPGP::<DefaultTerminal>::new();
let prompt_handler = default_handler()?; let prompt_handler = default_terminal()?;
openpgp.decrypt_one_shard_for_transport(key_discovery.as_deref(), messages_file, prompt_handler)?; openpgp.decrypt_one_shard_for_transport(key_discovery.as_deref(), messages_file, prompt_handler)?;

View File

@ -2,6 +2,7 @@
use std::{env, path::PathBuf, process::ExitCode, str::FromStr}; use std::{env, path::PathBuf, process::ExitCode, str::FromStr};
use keyfork_prompt::terminal::DefaultTerminal;
use keyfork_shard::{Format, openpgp::OpenPGP}; use keyfork_shard::{Format, openpgp::OpenPGP};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -50,7 +51,7 @@ fn run() -> Result<()> {
smex::decode(line?)? smex::decode(line?)?
}; };
let openpgp = OpenPGP; let openpgp = OpenPGP::<DefaultTerminal>::new();
openpgp.shard_and_encrypt(threshold, max, &input, key_discovery.as_path(), std::io::stdout())?; openpgp.shard_and_encrypt(threshold, max, &input, key_discovery.as_path(), std::io::stdout())?;
Ok(()) Ok(())

View File

@ -3,8 +3,7 @@
use std::{ use std::{
io::{stdin, stdout, Read, Write}, io::{stdin, stdout, Read, Write},
sync::Mutex, sync::{Arc, Mutex},
rc::Rc,
}; };
use aes_gcm::{ use aes_gcm::{
@ -12,12 +11,10 @@ use aes_gcm::{
Aes256Gcm, KeyInit, Nonce, Aes256Gcm, KeyInit, Nonce,
}; };
use base64::prelude::{Engine, BASE64_STANDARD}; use base64::prelude::{Engine, BASE64_STANDARD};
use blahaj::{Share, Sharks};
use hkdf::Hkdf; use hkdf::Hkdf;
use keyfork_bug::{bug, POISONED_MUTEX}; use keyfork_bug::{bug, POISONED_MUTEX};
use keyfork_mnemonic::{English, Mnemonic}; use keyfork_mnemonic::{English, Mnemonic};
use keyfork_prompt::{ use keyfork_prompt::{
prompt_validated_wordlist,
validators::{ validators::{
mnemonic::{MnemonicSetValidator, MnemonicValidator, WordLength}, mnemonic::{MnemonicSetValidator, MnemonicValidator, WordLength},
Validator, Validator,
@ -25,6 +22,7 @@ use keyfork_prompt::{
Message as PromptMessage, PromptHandler, Terminal, Message as PromptMessage, PromptHandler, Terminal,
}; };
use sha2::Sha256; use sha2::Sha256;
use blahaj::{Share, Sharks};
use x25519_dalek::{EphemeralSecret, PublicKey}; use x25519_dalek::{EphemeralSecret, PublicKey};
const PLAINTEXT_LENGTH: u8 = 32 // shard const PLAINTEXT_LENGTH: u8 = 32 // shard
@ -137,7 +135,7 @@ pub trait Format {
&self, &self,
private_keys: Option<Self::PrivateKeyData>, private_keys: Option<Self::PrivateKeyData>,
encrypted_messages: &[Self::EncryptedData], encrypted_messages: &[Self::EncryptedData],
prompt: Rc<Mutex<Box<dyn PromptHandler>>>, prompt: Arc<Mutex<impl PromptHandler>>,
) -> Result<(Vec<Share>, u8), Self::Error>; ) -> Result<(Vec<Share>, u8), Self::Error>;
/// Decrypt a single share and associated metadata from a reaable input. For the current /// Decrypt a single share and associated metadata from a reaable input. For the current
@ -151,7 +149,7 @@ pub trait Format {
&self, &self,
private_keys: Option<Self::PrivateKeyData>, private_keys: Option<Self::PrivateKeyData>,
encrypted_data: &[Self::EncryptedData], encrypted_data: &[Self::EncryptedData],
prompt: Rc<Mutex<Box<dyn PromptHandler>>>, prompt: Arc<Mutex<impl PromptHandler>>,
) -> Result<(Share, u8), Self::Error>; ) -> Result<(Share, u8), Self::Error>;
/// Decrypt multiple shares and combine them to recreate a secret. /// Decrypt multiple shares and combine them to recreate a secret.
@ -163,7 +161,7 @@ pub trait Format {
&self, &self,
private_key_discovery: Option<impl KeyDiscovery<Self>>, private_key_discovery: Option<impl KeyDiscovery<Self>>,
reader: impl Read + Send + Sync, reader: impl Read + Send + Sync,
prompt: Box<dyn PromptHandler>, prompt: impl PromptHandler,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> { ) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let private_keys = private_key_discovery let private_keys = private_key_discovery
.map(|p| p.discover_private_keys()) .map(|p| p.discover_private_keys())
@ -172,7 +170,7 @@ pub trait Format {
let (shares, threshold) = self.decrypt_all_shards( let (shares, threshold) = self.decrypt_all_shards(
private_keys, private_keys,
&encrypted_messages, &encrypted_messages,
Rc::new(Mutex::new(prompt)), Arc::new(Mutex::new(prompt)),
)?; )?;
let secret = Sharks(threshold) let secret = Sharks(threshold)
@ -193,9 +191,9 @@ pub trait Format {
&self, &self,
private_key_discovery: Option<impl KeyDiscovery<Self>>, private_key_discovery: Option<impl KeyDiscovery<Self>>,
reader: impl Read + Send + Sync, reader: impl Read + Send + Sync,
prompt: Box<dyn PromptHandler>, prompt: impl PromptHandler,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let prompt = Rc::new(Mutex::new(prompt)); let prompt = Arc::new(Mutex::new(prompt));
// parse input // parse input
let private_keys = private_key_discovery let private_keys = private_key_discovery
@ -235,17 +233,6 @@ pub trait Format {
let validator = MnemonicValidator { let validator = MnemonicValidator {
word_length: Some(WordLength::Count(24)), word_length: Some(WordLength::Count(24)),
}; };
let mut prompt = prompt.lock().expect(bug!(POISONED_MUTEX));
prompt_validated_wordlist::<English, _>(
&mut **prompt,
QRCODE_COULDNT_READ,
3,
&*validator.to_fn(),
)?
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?
/*
prompt prompt
.lock() .lock()
.expect(bug!(POISONED_MUTEX)) .expect(bug!(POISONED_MUTEX))
@ -257,7 +244,6 @@ pub trait Format {
.as_bytes() .as_bytes()
.try_into() .try_into()
.map_err(|_| InvalidData)? .map_err(|_| InvalidData)?
*/
} }
}; };
@ -515,11 +501,11 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
word_lengths: [24, 39], word_lengths: [24, 39],
}; };
let [pubkey_mnemonic, payload_mnemonic] = prompt_validated_wordlist::<English, _>( let [pubkey_mnemonic, payload_mnemonic] = pm
&mut pm, .prompt_validated_wordlist::<English, _>(
QRCODE_COULDNT_READ, QRCODE_COULDNT_READ,
3, 3,
&*validator.to_fn(), validator.to_fn(),
)?; )?;
let pubkey = pubkey_mnemonic let pubkey = pubkey_mnemonic
.as_bytes() .as_bytes()

View File

@ -5,10 +5,10 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
io::{Read, Write}, io::{Read, Write},
marker::PhantomData,
path::Path, path::Path,
str::FromStr, str::FromStr,
sync::Mutex, sync::{Arc, Mutex},
rc::Rc,
}; };
use keyfork_bug::bug; use keyfork_bug::bug;
@ -186,9 +186,18 @@ impl EncryptedMessage {
} }
/// Encoding and decoding shards using OpenPGP. /// Encoding and decoding shards using OpenPGP.
pub struct OpenPGP; pub struct OpenPGP<P: PromptHandler> {
p: PhantomData<P>,
}
impl OpenPGP { impl<P: PromptHandler> OpenPGP<P> {
#[allow(clippy::new_without_default, missing_docs)]
pub fn new() -> Self {
Self { p: PhantomData }
}
}
impl<P: PromptHandler> OpenPGP<P> {
/// Read all OpenPGP certificates in a path and return a [`Vec`] of them. /// Read all OpenPGP certificates in a path and return a [`Vec`] of them.
/// ///
/// Certificates are read from a file, or from files one level deep in a directory. /// Certificates are read from a file, or from files one level deep in a directory.
@ -247,7 +256,7 @@ impl OpenPGP {
const METADATA_MESSAGE_MISSING: &str = "Metadata message was not found in parsed packets"; const METADATA_MESSAGE_MISSING: &str = "Metadata message was not found in parsed packets";
impl Format for OpenPGP { impl<P: PromptHandler> Format for OpenPGP<P> {
type Error = Error; type Error = Error;
type PublicKey = Cert; type PublicKey = Cert;
type PrivateKeyData = Vec<Cert>; type PrivateKeyData = Vec<Cert>;
@ -444,7 +453,7 @@ impl Format for OpenPGP {
&self, &self,
private_keys: Option<Self::PrivateKeyData>, private_keys: Option<Self::PrivateKeyData>,
encrypted_data: &[Self::EncryptedData], encrypted_data: &[Self::EncryptedData],
prompt: Rc<Mutex<Box<dyn PromptHandler>>>, prompt: Arc<Mutex<impl PromptHandler>>,
) -> std::result::Result<(Vec<Share>, u8), Self::Error> { ) -> std::result::Result<(Vec<Share>, u8), Self::Error> {
// Be as liberal as possible when decrypting. // Be as liberal as possible when decrypting.
// We don't want to invalidate someone's keys just because the old sig expired. // We don't want to invalidate someone's keys just because the old sig expired.
@ -505,7 +514,7 @@ impl Format for OpenPGP {
&self, &self,
private_keys: Option<Self::PrivateKeyData>, private_keys: Option<Self::PrivateKeyData>,
encrypted_data: &[Self::EncryptedData], encrypted_data: &[Self::EncryptedData],
prompt: Rc<Mutex<Box<dyn PromptHandler>>>, prompt: Arc<Mutex<impl PromptHandler>>,
) -> std::result::Result<(Share, u8), Self::Error> { ) -> std::result::Result<(Share, u8), Self::Error> {
let policy = NullPolicy::new(); let policy = NullPolicy::new();
@ -551,22 +560,22 @@ impl Format for OpenPGP {
} }
} }
impl KeyDiscovery<OpenPGP> for &Path { impl<P: PromptHandler> KeyDiscovery<OpenPGP<P>> for &Path {
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP as Format>::PublicKey>> { fn discover_public_keys(&self) -> Result<Vec<<OpenPGP<P> as Format>::PublicKey>> {
OpenPGP::discover_certs(self) OpenPGP::<P>::discover_certs(self)
} }
fn discover_private_keys(&self) -> Result<<OpenPGP as Format>::PrivateKeyData> { fn discover_private_keys(&self) -> Result<<OpenPGP<P> as Format>::PrivateKeyData> {
OpenPGP::discover_certs(self) OpenPGP::<P>::discover_certs(self)
} }
} }
impl KeyDiscovery<OpenPGP> for &[Cert] { impl<P: PromptHandler> KeyDiscovery<OpenPGP<P>> for &[Cert] {
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP as Format>::PublicKey>> { fn discover_public_keys(&self) -> Result<Vec<<OpenPGP<P> as Format>::PublicKey>> {
Ok(self.to_vec()) Ok(self.to_vec())
} }
fn discover_private_keys(&self) -> Result<<OpenPGP as Format>::PrivateKeyData> { fn discover_private_keys(&self) -> Result<<OpenPGP<P> as Format>::PrivateKeyData> {
Ok(self.to_vec()) Ok(self.to_vec())
} }
} }
@ -628,12 +637,12 @@ fn decode_metadata_v1(buf: &[u8]) -> Result<(u8, Cert, Vec<Cert>)> {
// NOTE: When using single-decryptor mechanism, use this method with `threshold = 1` to return a // NOTE: When using single-decryptor mechanism, use this method with `threshold = 1` to return a
// single message. // single message.
fn decrypt_with_manager( fn decrypt_with_manager<P: PromptHandler>(
threshold: u8, threshold: u8,
messages: &mut HashMap<KeyID, EncryptedMessage>, messages: &mut HashMap<KeyID, EncryptedMessage>,
certs: &[Cert], certs: &[Cert],
policy: &dyn Policy, policy: &dyn Policy,
manager: &mut SmartcardManager, manager: &mut SmartcardManager<P>,
) -> Result<HashMap<KeyID, Vec<u8>>> { ) -> Result<HashMap<KeyID, Vec<u8>>> {
let mut decrypted_messages = HashMap::new(); let mut decrypted_messages = HashMap::new();
@ -678,11 +687,11 @@ fn decrypt_with_manager(
// NOTE: When using single-decryptor mechanism, only a single key should be provided in Keyring to // NOTE: When using single-decryptor mechanism, only a single key should be provided in Keyring to
// decrypt messages with. // decrypt messages with.
fn decrypt_with_keyring( fn decrypt_with_keyring<P: PromptHandler>(
messages: &mut HashMap<KeyID, EncryptedMessage>, messages: &mut HashMap<KeyID, EncryptedMessage>,
certs: &[Cert], certs: &[Cert],
policy: &NullPolicy, policy: &NullPolicy,
keyring: &mut Keyring, keyring: &mut Keyring<P>,
) -> Result<HashMap<KeyID, Vec<u8>>, Error> { ) -> Result<HashMap<KeyID, Vec<u8>>, Error> {
let mut decrypted_messages = HashMap::new(); let mut decrypted_messages = HashMap::new();
@ -712,11 +721,11 @@ fn decrypt_with_keyring(
Ok(decrypted_messages) Ok(decrypted_messages)
} }
fn decrypt_metadata( fn decrypt_metadata<P: PromptHandler>(
message: &EncryptedMessage, message: &EncryptedMessage,
policy: &NullPolicy, policy: &NullPolicy,
keyring: &mut Keyring, keyring: &mut Keyring<P>,
manager: &mut SmartcardManager, manager: &mut SmartcardManager<P>,
) -> Result<Vec<u8>> { ) -> Result<Vec<u8>> {
Ok(if keyring.is_empty() { Ok(if keyring.is_empty() {
manager.load_any_card()?; manager.load_any_card()?;

View File

@ -1,6 +1,6 @@
#![allow(clippy::expect_fun_call)] #![allow(clippy::expect_fun_call)]
use std::{rc::Rc, sync::Mutex}; use std::sync::{Arc, Mutex};
use keyfork_bug::{bug, POISONED_MUTEX}; use keyfork_bug::{bug, POISONED_MUTEX};
use keyfork_prompt::{Error as PromptError, PromptHandler}; use keyfork_prompt::{Error as PromptError, PromptHandler};
@ -27,14 +27,14 @@ pub enum Error {
pub type Result<T, E = Error> = std::result::Result<T, E>; pub type Result<T, E = Error> = std::result::Result<T, E>;
pub struct Keyring { pub struct Keyring<P: PromptHandler> {
full_certs: Vec<Cert>, full_certs: Vec<Cert>,
root: Option<Cert>, root: Option<Cert>,
pm: Rc<Mutex<Box<dyn PromptHandler>>>, pm: Arc<Mutex<P>>,
} }
impl Keyring { impl<P: PromptHandler> Keyring<P> {
pub fn new(certs: impl AsRef<[Cert]>, p: Rc<Mutex<Box<dyn PromptHandler>>>) -> Result<Self> { pub fn new(certs: impl AsRef<[Cert]>, p: Arc<Mutex<P>>) -> Result<Self> {
Ok(Self { Ok(Self {
full_certs: certs.as_ref().to_vec(), full_certs: certs.as_ref().to_vec(),
root: Default::default(), root: Default::default(),
@ -62,7 +62,7 @@ impl Keyring {
} }
} }
impl VerificationHelper for &mut Keyring { impl<P: PromptHandler> VerificationHelper for &mut Keyring<P> {
fn get_certs(&mut self, ids: &[KeyHandle]) -> openpgp::Result<Vec<Cert>> { fn get_certs(&mut self, ids: &[KeyHandle]) -> openpgp::Result<Vec<Cert>> {
Ok(ids Ok(ids
.iter() .iter()
@ -108,7 +108,7 @@ impl VerificationHelper for &mut Keyring {
} }
} }
impl DecryptionHelper for &mut Keyring { impl<P: PromptHandler> DecryptionHelper for &mut Keyring<P> {
fn decrypt<D>( fn decrypt<D>(
&mut self, &mut self,
pkesks: &[PKESK], pkesks: &[PKESK],

View File

@ -2,13 +2,11 @@
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
rc::Rc, sync::{Arc, Mutex},
sync::Mutex,
}; };
use keyfork_bug::{bug, POISONED_MUTEX}; use keyfork_bug::{bug, POISONED_MUTEX};
use keyfork_prompt::{ use keyfork_prompt::{
prompt_validated_passphrase,
validators::{PinValidator, Validator}, validators::{PinValidator, Validator},
Error as PromptError, Message, PromptHandler, Error as PromptError, Message, PromptHandler,
}; };
@ -73,15 +71,15 @@ fn format_name(input: impl AsRef<str>) -> String {
} }
#[allow(clippy::module_name_repetitions)] #[allow(clippy::module_name_repetitions)]
pub struct SmartcardManager { pub struct SmartcardManager<P: PromptHandler> {
current_card: Option<Card<Open>>, current_card: Option<Card<Open>>,
root: Option<Cert>, root: Option<Cert>,
pm: Rc<Mutex<Box<dyn PromptHandler>>>, pm: Arc<Mutex<P>>,
pin_cache: HashMap<Fingerprint, String>, pin_cache: HashMap<Fingerprint, String>,
} }
impl SmartcardManager { impl<P: PromptHandler> SmartcardManager<P> {
pub fn new(p: Rc<Mutex<Box<dyn PromptHandler>>>) -> Result<Self> { pub fn new(p: Arc<Mutex<P>>) -> Result<Self> {
Ok(Self { Ok(Self {
current_card: None, current_card: None,
root: None, root: None,
@ -175,7 +173,7 @@ impl SmartcardManager {
} }
} }
impl VerificationHelper for &mut SmartcardManager { impl<P: PromptHandler> VerificationHelper for &mut SmartcardManager<P> {
fn get_certs(&mut self, ids: &[openpgp::KeyHandle]) -> openpgp::Result<Vec<Cert>> { fn get_certs(&mut self, ids: &[openpgp::KeyHandle]) -> openpgp::Result<Vec<Cert>> {
#[allow(clippy::flat_map_option)] #[allow(clippy::flat_map_option)]
Ok(ids Ok(ids
@ -219,7 +217,7 @@ impl VerificationHelper for &mut SmartcardManager {
} }
} }
impl DecryptionHelper for &mut SmartcardManager { impl<P: PromptHandler> DecryptionHelper for &mut SmartcardManager<P> {
fn decrypt<D>( fn decrypt<D>(
&mut self, &mut self,
pkesks: &[PKESK], pkesks: &[PKESK],
@ -277,8 +275,11 @@ impl DecryptionHelper for &mut SmartcardManager {
} else { } else {
format!("Unlock card {card_id} ({cardholder_name})\n{rpea}: {attempts}\n\nPIN: ") format!("Unlock card {card_id} ({cardholder_name})\n{rpea}: {attempts}\n\nPIN: ")
}; };
let mut prompt = self.pm.lock().expect(bug!(POISONED_MUTEX)); let temp_pin = self
let temp_pin = prompt_validated_passphrase(&mut **prompt, &message, 3, &pin_validator)?; .pm
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_validated_passphrase(&message, 3, &pin_validator)?;
let verification_status = transaction.verify_user_pin(temp_pin.as_str().trim()); let verification_status = transaction.verify_user_pin(temp_pin.as_str().trim());
match verification_status { match verification_status {
#[allow(clippy::ignored_unit_patterns)] #[allow(clippy::ignored_unit_patterns)]

View File

@ -3,13 +3,7 @@ use clap::{Parser, Subcommand};
use std::path::PathBuf; use std::path::PathBuf;
use keyfork_mnemonic::{English, Mnemonic}; use keyfork_mnemonic::{English, Mnemonic};
use keyfork_prompt::{ use keyfork_prompt::{default_terminal, DefaultTerminal};
default_handler, prompt_validated_wordlist,
validators::{
mnemonic::{MnemonicChoiceValidator, WordLength},
Validator,
},
};
use keyfork_shard::{remote_decrypt, Format}; use keyfork_shard::{remote_decrypt, Format};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>; type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
@ -41,8 +35,8 @@ impl RecoverSubcommands {
} => { } => {
let content = std::fs::read_to_string(shard_file)?; let content = std::fs::read_to_string(shard_file)?;
if content.contains("BEGIN PGP MESSAGE") { if content.contains("BEGIN PGP MESSAGE") {
let openpgp = keyfork_shard::openpgp::OpenPGP; let openpgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
let prompt_handler = default_handler()?; let prompt_handler = default_terminal()?;
// TODO: remove .clone() by making handle() consume self // TODO: remove .clone() by making handle() consume self
let seed = openpgp.decrypt_all_shards_to_secret( let seed = openpgp.decrypt_all_shards_to_secret(
key_discovery.as_deref(), key_discovery.as_deref(),
@ -60,15 +54,21 @@ impl RecoverSubcommands {
Ok(seed) Ok(seed)
} }
RecoverSubcommands::Mnemonic {} => { RecoverSubcommands::Mnemonic {} => {
let mut prompt_handler = default_handler()?; use keyfork_prompt::{
validators::{
mnemonic::{MnemonicChoiceValidator, WordLength},
Validator,
},
PromptHandler,
};
let mut term = default_terminal()?;
let validator = MnemonicChoiceValidator { let validator = MnemonicChoiceValidator {
word_lengths: [WordLength::Count(12), WordLength::Count(24)], word_lengths: [WordLength::Count(12), WordLength::Count(24)],
}; };
let mnemonic = prompt_validated_wordlist::<English, _>( let mnemonic = term.prompt_validated_wordlist::<English, _>(
&mut *prompt_handler,
"Mnemonic: ", "Mnemonic: ",
3, 3,
&*validator.to_fn(), validator.to_fn(),
)?; )?;
Ok(mnemonic.to_bytes()) Ok(mnemonic.to_bytes())
} }

View File

@ -1,6 +1,6 @@
use super::Keyfork; use super::Keyfork;
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum}; use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
use keyfork_prompt::default_handler; use keyfork_prompt::{default_terminal, DefaultTerminal};
use keyfork_shard::Format as _; use keyfork_shard::Format as _;
use std::{ use std::{
io::{stdin, stdout, Read, Write}, io::{stdin, stdout, Read, Write},
@ -64,7 +64,7 @@ impl ShardExec for OpenPGP {
secret: &[u8], secret: &[u8],
output: &mut (impl Write + Send + Sync), output: &mut (impl Write + Send + Sync),
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let opgp = keyfork_shard::openpgp::OpenPGP; let opgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
opgp.shard_and_encrypt(threshold, max, secret, key_discovery, output) opgp.shard_and_encrypt(threshold, max, secret, key_discovery, output)
} }
@ -74,8 +74,8 @@ impl ShardExec for OpenPGP {
input: impl Read + Send + Sync, input: impl Read + Send + Sync,
output: &mut impl Write, output: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let openpgp = keyfork_shard::openpgp::OpenPGP; let openpgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
let prompt = default_handler()?; let prompt = default_terminal()?;
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, input, prompt)?; let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, input, prompt)?;
write!(output, "{}", smex::encode(bytes))?; write!(output, "{}", smex::encode(bytes))?;
@ -87,8 +87,8 @@ impl ShardExec for OpenPGP {
key_discovery: Option<&Path>, key_discovery: Option<&Path>,
input: impl Read + Send + Sync, input: impl Read + Send + Sync,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let openpgp = keyfork_shard::openpgp::OpenPGP; let openpgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
let prompt = default_handler()?; let prompt = default_terminal()?;
openpgp.decrypt_one_shard_for_transport(key_discovery, input, prompt)?; openpgp.decrypt_one_shard_for_transport(key_discovery, input, prompt)?;
Ok(()) Ok(())
} }

View File

@ -20,10 +20,9 @@ use keyfork_derive_path_data::paths;
use keyfork_derive_util::DerivationIndex; use keyfork_derive_util::DerivationIndex;
use keyfork_mnemonic::Mnemonic; use keyfork_mnemonic::Mnemonic;
use keyfork_prompt::{ use keyfork_prompt::{
default_handler, default_terminal,
prompt_validated_passphrase,
validators::{SecurePinValidator, Validator}, validators::{SecurePinValidator, Validator},
Message, DefaultTerminal, Message, PromptHandler,
}; };
use keyfork_shard::{openpgp::OpenPGP, Format}; use keyfork_shard::{openpgp::OpenPGP, Format};
@ -175,7 +174,7 @@ impl WizardSubcommands {
impl GenerateShardSecret { impl GenerateShardSecret {
fn handle(&self) -> Result<()> { fn handle(&self) -> Result<()> {
let seed = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?; let seed = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?;
let mut pm = default_handler()?; let mut pm = default_terminal()?;
let mut certs = vec![]; let mut certs = vec![];
let mut seen_cards: HashSet<String> = HashSet::new(); let mut seen_cards: HashSet<String> = HashSet::new();
let stdout = std::io::stdout(); let stdout = std::io::stdout();
@ -214,14 +213,12 @@ impl GenerateShardSecret {
.to_string(), .to_string(),
))?; ))?;
}; };
let user_pin = prompt_validated_passphrase( let user_pin = pm.prompt_validated_passphrase(
&mut *pm,
"Please enter the new smartcard User PIN: ", "Please enter the new smartcard User PIN: ",
3, 3,
&user_pin_validator, &user_pin_validator,
)?; )?;
let admin_pin = prompt_validated_passphrase( let admin_pin = pm.prompt_validated_passphrase(
&mut *pm,
"Please enter the new smartcard Admin PIN: ", "Please enter the new smartcard Admin PIN: ",
3, 3,
&admin_pin_validator, &admin_pin_validator,
@ -237,7 +234,7 @@ impl GenerateShardSecret {
certs.push(cert); certs.push(cert);
} }
let opgp = OpenPGP; let opgp = OpenPGP::<DefaultTerminal>::new();
if let Some(output_file) = self.output.as_ref() { if let Some(output_file) = self.output.as_ref() {
let output = File::create(output_file)?; let output = File::create(output_file)?;
@ -285,8 +282,8 @@ impl BottomsUp {
cert.serialize(&mut w)?; cert.serialize(&mut w)?;
w.finalize()?; w.finalize()?;
let opgp = OpenPGP; let opgp = OpenPGP::<DefaultTerminal>::new();
let certs = OpenPGP::discover_certs(&self.key_discovery)?; let certs = OpenPGP::<DefaultTerminal>::discover_certs(&self.key_discovery)?;
let shardfile = File::create(&self.output_shardfile)?; let shardfile = File::create(&self.output_shardfile)?;
opgp.shard_and_encrypt( opgp.shard_and_encrypt(

View File

@ -1,26 +1,38 @@
#![allow(missing_docs)] #![allow(missing_docs)]
use std::io::{stdin, stdout};
use keyfork_prompt::{ use keyfork_prompt::{
prompt_validated_wordlist,
validators::{mnemonic, Validator}, validators::{mnemonic, Validator},
default_handler, Terminal, PromptHandler,
}; };
use keyfork_mnemonic::English; use keyfork_mnemonic::English;
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut handler = default_handler().unwrap(); let mut mgr = Terminal::new(stdin(), stdout())?;
let transport_validator = mnemonic::MnemonicSetValidator { let transport_validator = mnemonic::MnemonicSetValidator {
word_lengths: [24], word_lengths: [9, 24],
};
let combine_validator = mnemonic::MnemonicSetValidator {
word_lengths: [24, 48],
}; };
let mnemonics = prompt_validated_wordlist::<English, _>( let mnemonics = mgr.prompt_validated_wordlist::<English, _>(
&mut *handler, "Enter a 9-word and 24-word mnemonic: ",
"Enter a 24-word mnemonic: ",
3, 3,
&*transport_validator.to_fn(), transport_validator.to_fn(),
)?;
assert_eq!(mnemonics[0].as_bytes().len(), 12);
assert_eq!(mnemonics[1].as_bytes().len(), 32);
let mnemonics = mgr.prompt_validated_wordlist::<English, _>(
"Enter a 24 and 48-word mnemonic: ",
3,
combine_validator.to_fn(),
)?; )?;
assert_eq!(mnemonics[0].as_bytes().len(), 32); assert_eq!(mnemonics[0].as_bytes().len(), 32);
assert_eq!(mnemonics[1].as_bytes().len(), 64);
Ok(()) Ok(())
} }

View File

@ -1,120 +0,0 @@
//! A headless prompt handler.
//!
//! This prompt handler uses the program's standard input and output to read inputs. It is not
//! directly intended to be machine-readable, but can be used for scriptable automation in a
//! fashion similar to a terminal handler.
use std::io::{IsTerminal, Write};
use crate::{BoxResult, Error, Message, PromptHandler, Result};
/// A headless prompt handler, usable in situations when a terminal might not be available, or for
/// scripting purposes where manual input from a terminal is not desirable.
pub struct Headless {
stdin: std::io::Stdin,
stderr: std::io::Stderr,
}
impl Headless {
/// Create a new [`Headless`] prompt handler.
#[allow(clippy::missing_errors_doc, clippy::new_without_default)]
pub fn new() -> Self {
Self {
stdin: std::io::stdin(),
stderr: std::io::stderr(),
}
}
}
impl PromptHandler for Headless {
fn prompt_input(&mut self, prompt: &str) -> Result<String> {
self.stderr.write_all(prompt.as_bytes())?;
self.stderr.flush()?;
let mut line = String::new();
self.stdin.read_line(&mut line)?;
Ok(line)
}
fn prompt_wordlist(&mut self, prompt: &str, _wordlist: &[&str]) -> Result<String> {
self.stderr.write_all(prompt.as_bytes())?;
self.stderr.flush()?;
let mut line = String::new();
self.stdin.read_line(&mut line)?;
Ok(line)
}
fn prompt_passphrase(&mut self, prompt: &str) -> Result<String> {
// Temporarily perform an IOCTL to disable printed output.
if self.stdin.is_terminal() {
eprintln!("WARNING: Headless terminal mode may leak passwords!");
}
self.stderr.write_all(prompt.as_bytes())?;
self.stderr.flush()?;
let mut line = String::new();
self.stdin.read_line(&mut line)?;
Ok(line)
}
fn prompt_message(&mut self, prompt: Message) -> Result<()> {
match prompt {
Message::Text(s) => {
self.stderr.write_all(s.as_bytes())?;
self.stderr.flush()?;
}
Message::Data(s) => {
self.stderr.write_all(s.as_bytes())?;
self.stderr.flush()?;
}
}
Ok(())
}
fn prompt_validated_wordlist(
&mut self,
prompt: &str,
retries: u8,
_wordlist: &[&str],
validator_fn: &mut dyn FnMut(String) -> BoxResult,
) -> Result<()> {
let mut line = String::new();
let mut last_error = String::new();
for _ in 0..retries {
self.stderr.write_all(prompt.as_bytes())?;
self.stderr.flush()?;
self.stderr.flush()?;
self.stdin.read_line(&mut line)?;
if let Err(e) = validator_fn(std::mem::take(&mut line)) {
last_error = e.to_string();
self.stderr.write_all(e.to_string().as_bytes())?;
self.stderr.flush()?;
} else {
return Ok(());
}
}
Err(Error::Validation(retries, last_error))
}
fn prompt_validated_passphrase(
&mut self,
prompt: &str,
retries: u8,
validator_fn: &mut dyn FnMut(String) -> BoxResult,
) -> Result<()> {
let mut line = String::new();
let mut last_error = String::new();
for _ in 0..retries {
self.stderr.write_all(prompt.as_bytes())?;
self.stderr.flush()?;
self.stdin.read_line(&mut line)?;
if let Err(e) = validator_fn(std::mem::take(&mut line)) {
last_error = e.to_string();
self.stderr.write_all(e.to_string().as_bytes())?;
self.stderr.write_all(b"\n")?;
self.stderr.flush()?;
} else {
return Ok(());
}
}
Err(Error::Validation(retries, last_error))
}
}

View File

@ -1,36 +1,14 @@
//! Prompt display and interaction management. //! Prompt display and interaction management.
use std::io::IsTerminal; use std::borrow::Borrow;
#[cfg(feature = "mnemonic")] #[cfg(feature = "mnemonic")]
use keyfork_mnemonic::Wordlist; use keyfork_mnemonic::Wordlist;
pub mod headless; ///
pub mod terminal; pub mod terminal;
pub mod validators; pub mod validators;
pub use headless::Headless; pub use terminal::{Terminal, DefaultTerminal, default_terminal};
pub use terminal::{default_terminal, DefaultTerminal, Terminal};
/// An error occurred in the process of loading a default handler.
#[derive(thiserror::Error, Debug)]
pub enum DefaultHandlerError {
/// An invalid handler was loaded.
#[error("An invalid handler was loaded: {handler} ({error})")]
InvalidHandler {
/// The handle that caused an error.
handler: String,
/// The error that occurred.
error: String,
},
/// An unknown handler was requested.
#[error("An unknown handler was requested: {handler}")]
UnknownHandler {
/// The requested, but unknown, handler.
handler: String,
},
}
/// An error occurred while displaying a prompt. /// An error occurred while displaying a prompt.
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
@ -64,9 +42,6 @@ pub enum Message {
Data(String), Data(String),
} }
#[doc(hidden)]
pub type BoxResult = std::result::Result<(), Box<dyn std::error::Error>>;
/// A trait to allow displaying prompts and accepting input. /// A trait to allow displaying prompts and accepting input.
pub trait PromptHandler { pub trait PromptHandler {
/// Prompt the user for input. /// Prompt the user for input.
@ -82,7 +57,25 @@ pub trait PromptHandler {
/// # Errors /// # Errors
/// The method may return an error if the message was not able to be displayed or if the input /// The method may return an error if the message was not able to be displayed or if the input
/// could not be read. /// could not be read.
fn prompt_wordlist(&mut self, prompt: &str, wordlist: &[&str]) -> Result<String>; #[cfg(feature = "mnemonic")]
fn prompt_wordlist<X>(&mut self, prompt: &str) -> Result<String> where X: Wordlist;
/// Prompt the user for input based on a wordlist, while validating the wordlist using a
/// provided parser function, returning the type from the parser. A language must be specified
/// as the generic parameter `X` (any type implementing [`Wordlist`]) when parsing a wordlist.
///
/// # Errors
/// The method may return an error if the message was not able to be displayed, if the input
/// could not be read, or if the parser returned an error.
#[cfg(feature = "mnemonic")]
fn prompt_validated_wordlist<X, V>(
&mut self,
prompt: &str,
retries: u8,
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
) -> Result<V, Error>
where
X: Wordlist;
/// Prompt the user for a passphrase, which is hidden while typing. /// Prompt the user for a passphrase, which is hidden while typing.
/// ///
@ -91,140 +84,23 @@ pub trait PromptHandler {
/// could not be read. /// could not be read.
fn prompt_passphrase(&mut self, prompt: &str) -> Result<String>; fn prompt_passphrase(&mut self, prompt: &str) -> Result<String>;
/// Prompt the user for a passphrase, which is hidden while typing, and validate the passphrase
/// using a provided parser function, returning the type from the parser.
///
/// # Errors
/// The method may return an error if the message was not able to be displayed, if the input
/// could not be read, or if the parser returned an error.
fn prompt_validated_passphrase<V>(
&mut self,
prompt: &str,
retries: u8,
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
) -> Result<V, Error>;
/// Prompt the user with a [`Message`]. /// Prompt the user with a [`Message`].
/// ///
/// # Errors /// # Errors
/// The method may return an error if the message was not able to be displayed or if an error /// The method may return an error if the message was not able to be displayed or if an error
/// occurred while waiting for the user to dismiss the message. /// occurred while waiting for the user to dismiss the message.
fn prompt_message(&mut self, prompt: Message) -> Result<()>; fn prompt_message(&mut self, prompt: impl Borrow<Message>) -> Result<()>;
/// Prompt the user for input based on a wordlist, while validating the wordlist using a
/// provided parser function, returning the type from the parser. A language must be specified
/// as the generic parameter `X` (any type implementing [`Wordlist`]) when parsing a wordlist.
///
/// This method MUST NOT be used directly. Instead, use
/// [`prompt_validated_wordlist`].
///
/// # Errors
/// The method may return an error if the message was not able to be displayed, if the input
/// could not be read, or if the parser returned an error.
fn prompt_validated_wordlist(
&mut self,
prompt: &str,
retries: u8,
wordlist: &[&str],
validator_fn: &mut dyn FnMut(String) -> BoxResult,
) -> Result<(), Error>;
/// Prompt the user for a passphrase, which is hidden while typing, and validate the passphrase
/// using a provided parser function, returning the type from the parser.
///
/// This method MUST NOT be used directly. Instead, use
/// [`prompt_validated_wordlist`].
///
/// # Errors
/// The method may return an error if the message was not able to be displayed, if the input
/// could not be read, or if the parser returned an error.
fn prompt_validated_passphrase(
&mut self,
prompt: &str,
retries: u8,
validator_fn: &mut dyn FnMut(String) -> BoxResult,
) -> Result<(), Error>;
}
/// Prompt the user for input based on a wordlist, while validating the wordlist using a
/// provided parser function, returning the type from the parser. A language must be specified
/// as the generic parameter `X` (any type implementing [`Wordlist`]) when parsing a wordlist.
///
/// # Errors
/// The method may return an error if the message was not able to be displayed, if the input
/// could not be read, or if the parser returned an error.
#[cfg(feature = "mnemonic")]
#[allow(clippy::missing_panics_doc)]
pub fn prompt_validated_wordlist<X, V>(
handler: &mut dyn PromptHandler,
prompt: &str,
retries: u8,
validator_fn: &dyn Fn(String) -> Result<V, Box<dyn std::error::Error>>,
) -> Result<V, Error>
where
X: Wordlist,
{
let wordlist = X::get_singleton();
let words = wordlist.to_str_array();
let mut opt: Option<V> = None;
handler.prompt_validated_wordlist(prompt, retries, &words, &mut |string| {
opt = Some(validator_fn(string)?);
Ok(())
})?;
Ok(opt.unwrap())
}
/// Prompt the user for a passphrase, which is hidden while typing, and validate the passphrase
/// using a provided parser function, returning the type from the parser.
///
/// # Errors
/// The method may return an error if the message was not able to be displayed, if the input
/// could not be read, or if the parser returned an error.
#[allow(clippy::missing_panics_doc)]
pub fn prompt_validated_passphrase<V>(
handler: &mut dyn PromptHandler,
prompt: &str,
retries: u8,
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
) -> Result<V, Error> {
let mut opt: Option<V> = None;
handler.prompt_validated_passphrase(prompt, retries, &mut |string| {
opt = Some(validator_fn(string)?);
Ok(())
})?;
Ok(opt.unwrap())
}
/// Get a Prompt Handler that is most suitable for the given environment.
///
/// The following handlers will be used based on the `KEYFORK_PROMPT_TYPE` variable:
/// * `KEYFORK_PROMPT_TYPE=terminal`: [`DefaultTerminal`]
/// * `KEYFORK_PROMPT_TYPE=headless`: [`Headless`]
///
/// Otherwise, the following heuristics are followed:
/// * [`std::io::IsTerminal::is_terminal`]: [`DefaultTerminal`]
/// * default: [`Headless`]
///
/// # Errors
///
/// The function will return an error if a specific handler was requested but could not be
/// constructed.
pub fn default_handler() -> Result<Box<dyn PromptHandler>, DefaultHandlerError> {
if let Some((_, value)) = std::env::vars().find(|(k, _)| k == "KEYFORK_PROMPT_TYPE") {
match value.as_str() {
"terminal" => match default_terminal() {
Ok(terminal) => return Ok(Box::new(terminal)),
Err(e) => {
return Err(DefaultHandlerError::InvalidHandler {
handler: value,
error: e.to_string(),
})
}
},
"headless" => {
return Ok(Box::new(Headless::new()));
}
_ => {
return Err(DefaultHandlerError::UnknownHandler { handler: value });
}
}
}
// stdout can be not-a-terminal and we'll just override it anyways, stdin is the
// important one.
if std::io::stdin().is_terminal() {
// because this is a "guessed" handler, let's take the nice route and not error, just skip.
if let Ok(terminal) = default_terminal() {
return Ok(Box::new(terminal));
}
}
Ok(Box::new(Headless::new()))
} }

View File

@ -21,7 +21,7 @@ use keyfork_crossterm::{
use keyfork_bug::bug; use keyfork_bug::bug;
use crate::{BoxResult, Error, Message, PromptHandler}; use crate::{Error, Message, PromptHandler, Wordlist};
#[allow(missing_docs)] #[allow(missing_docs)]
pub type Result<T, E = Error> = std::result::Result<T, E>; pub type Result<T, E = Error> = std::result::Result<T, E>;
@ -198,20 +198,23 @@ where
Ok(line) Ok(line)
} }
fn prompt_validated_wordlist( #[cfg(feature = "mnemonic")]
fn prompt_validated_wordlist<X, V>(
&mut self, &mut self,
prompt: &str, prompt: &str,
retries: u8, retries: u8,
wordlist: &[&str], validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
validator_fn: &mut dyn FnMut(String) -> BoxResult, ) -> Result<V, Error>
) -> Result<(), Error> { where
X: Wordlist,
{
let mut last_error = None; let mut last_error = None;
for _ in 0..retries { for _ in 0..retries {
let s = self.prompt_wordlist(prompt, wordlist)?; let s = self.prompt_wordlist::<X>(prompt)?;
match validator_fn(s) { match validator_fn(s) {
Ok(v) => return Ok(v), Ok(v) => return Ok(v),
Err(e) => { Err(e) => {
self.prompt_message(Message::Text(format!("Error validating wordlist: {e}")))?; self.prompt_message(&Message::Text(format!("Error validating wordlist: {e}")))?;
let _ = last_error.insert(e); let _ = last_error.insert(e);
} }
} }
@ -224,8 +227,15 @@ where
)) ))
} }
#[cfg(feature = "mnemonic")]
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
fn prompt_wordlist(&mut self, prompt: &str, wordlist: &[&str]) -> Result<String> { fn prompt_wordlist<X>(&mut self, prompt: &str) -> Result<String>
where
X: Wordlist,
{
let wordlist = X::get_singleton();
let words = wordlist.to_str_array();
let mut terminal = self let mut terminal = self
.lock() .lock()
.alternate_screen()? .alternate_screen()?
@ -295,7 +305,7 @@ where
let word = input.split_whitespace().next_back().map(ToOwned::to_owned); let word = input.split_whitespace().next_back().map(ToOwned::to_owned);
if let Some(steel_word) = word { if let Some(steel_word) = word {
if steel_word.len() >= 4 { if steel_word.len() >= 4 {
for word in wordlist.iter().filter(|word| word.len() >= 4) { for word in words.iter().filter(|word| word.len() >= 4) {
if word[..4] == steel_word { if word[..4] == steel_word {
input.push_str(&word[4..]); input.push_str(&word[4..]);
input.push(' '); input.push(' ');
@ -341,7 +351,7 @@ where
let mut iter = printable_input.split_whitespace().peekable(); let mut iter = printable_input.split_whitespace().peekable();
while let Some(word) = iter.next() { while let Some(word) = iter.next() {
if wordlist.contains(&word) { if words.contains(&word) {
terminal.queue(PrintStyledContent(word.green()))?; terminal.queue(PrintStyledContent(word.green()))?;
} else { } else {
terminal.queue(PrintStyledContent(word.red()))?; terminal.queue(PrintStyledContent(word.red()))?;
@ -362,19 +372,19 @@ where
Ok(input) Ok(input)
} }
fn prompt_validated_passphrase( fn prompt_validated_passphrase<V>(
&mut self, &mut self,
prompt: &str, prompt: &str,
retries: u8, retries: u8,
validator_fn: &mut dyn FnMut(String) -> BoxResult, validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
) -> Result<(), Error> { ) -> Result<V, Error> {
let mut last_error = None; let mut last_error = None;
for _ in 0..retries { for _ in 0..retries {
let s = self.prompt_passphrase(prompt)?; let s = self.prompt_passphrase(prompt)?;
match validator_fn(s) { match validator_fn(s) {
Ok(v) => return Ok(v), Ok(v) => return Ok(v),
Err(e) => { Err(e) => {
self.prompt_message(Message::Text(format!( self.prompt_message(&Message::Text(format!(
"Error validating passphrase: {e}" "Error validating passphrase: {e}"
)))?; )))?;
let _ = last_error.insert(e); let _ = last_error.insert(e);
@ -451,7 +461,7 @@ where
Ok(passphrase) Ok(passphrase)
} }
fn prompt_message(&mut self, prompt: Message) -> Result<()> { fn prompt_message(&mut self, prompt: impl Borrow<Message>) -> Result<()> {
let mut terminal = self.lock().alternate_screen()?.raw_mode()?; let mut terminal = self.lock().alternate_screen()?.raw_mode()?;
loop { loop {