keyfork-prompt: make dyn Trait compatible in prep for allowing dynamic prompt handlers

This commit is contained in:
Ryan Heywood 2025-01-03 23:11:33 -05:00
parent d7bf3d16e1
commit 92dde3dcee
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
7 changed files with 151 additions and 91 deletions

View File

@ -11,10 +11,12 @@ 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,
@ -22,7 +24,6 @@ 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
@ -233,6 +234,17 @@ 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))
@ -244,6 +256,7 @@ pub trait Format {
.as_bytes() .as_bytes()
.try_into() .try_into()
.map_err(|_| InvalidData)? .map_err(|_| InvalidData)?
*/
} }
}; };
@ -501,11 +514,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] = pm let [pubkey_mnemonic, payload_mnemonic] = prompt_validated_wordlist::<English, _>(
.prompt_validated_wordlist::<English, _>( &mut pm,
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

@ -7,6 +7,7 @@ use std::{
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,
}; };
@ -275,11 +276,8 @@ impl<P: PromptHandler> DecryptionHelper for &mut SmartcardManager<P> {
} 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 temp_pin = self let mut prompt = self.pm.lock().expect(bug!(POISONED_MUTEX));
.pm let temp_pin = prompt_validated_passphrase(&mut *prompt, &message, 3, &pin_validator)?;
.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,7 +3,14 @@ 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::{default_terminal, DefaultTerminal}; use keyfork_prompt::{
default_terminal, prompt_validated_wordlist,
validators::{
mnemonic::{MnemonicChoiceValidator, WordLength},
Validator,
},
DefaultTerminal,
};
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>;
@ -54,21 +61,15 @@ impl RecoverSubcommands {
Ok(seed) Ok(seed)
} }
RecoverSubcommands::Mnemonic {} => { RecoverSubcommands::Mnemonic {} => {
use keyfork_prompt::{
validators::{
mnemonic::{MnemonicChoiceValidator, WordLength},
Validator,
},
PromptHandler,
};
let mut term = default_terminal()?; 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 = term.prompt_validated_wordlist::<English, _>( let mnemonic = prompt_validated_wordlist::<English, _>(
&mut term,
"Mnemonic: ", "Mnemonic: ",
3, 3,
validator.to_fn(), &*validator.to_fn(),
)?; )?;
Ok(mnemonic.to_bytes()) Ok(mnemonic.to_bytes())
} }

View File

@ -20,7 +20,7 @@ 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_terminal, default_terminal, prompt_validated_passphrase,
validators::{SecurePinValidator, Validator}, validators::{SecurePinValidator, Validator},
DefaultTerminal, Message, PromptHandler, DefaultTerminal, Message, PromptHandler,
}; };
@ -213,12 +213,14 @@ impl GenerateShardSecret {
.to_string(), .to_string(),
))?; ))?;
}; };
let user_pin = pm.prompt_validated_passphrase( let user_pin = 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 = pm.prompt_validated_passphrase( let admin_pin = 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,

View File

@ -3,8 +3,9 @@
use std::io::{stdin, stdout}; use std::io::{stdin, stdout};
use keyfork_prompt::{ use keyfork_prompt::{
prompt_validated_wordlist,
validators::{mnemonic, Validator}, validators::{mnemonic, Validator},
Terminal, PromptHandler, Terminal,
}; };
use keyfork_mnemonic::English; use keyfork_mnemonic::English;
@ -18,18 +19,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
word_lengths: [24, 48], word_lengths: [24, 48],
}; };
let mnemonics = mgr.prompt_validated_wordlist::<English, _>( let mnemonics = prompt_validated_wordlist::<English, _>(
&mut mgr,
"Enter a 9-word and 24-word mnemonic: ", "Enter a 9-word and 24-word mnemonic: ",
3, 3,
transport_validator.to_fn(), &*transport_validator.to_fn(),
)?; )?;
assert_eq!(mnemonics[0].as_bytes().len(), 12); assert_eq!(mnemonics[0].as_bytes().len(), 12);
assert_eq!(mnemonics[1].as_bytes().len(), 32); assert_eq!(mnemonics[1].as_bytes().len(), 32);
let mnemonics = mgr.prompt_validated_wordlist::<English, _>( let mnemonics = prompt_validated_wordlist::<English, _>(
&mut mgr,
"Enter a 24 and 48-word mnemonic: ", "Enter a 24 and 48-word mnemonic: ",
3, 3,
combine_validator.to_fn(), &*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); assert_eq!(mnemonics[1].as_bytes().len(), 64);

View File

@ -1,14 +1,12 @@
//! Prompt display and interaction management. //! Prompt display and interaction management.
use std::borrow::Borrow;
#[cfg(feature = "mnemonic")] #[cfg(feature = "mnemonic")]
use keyfork_mnemonic::Wordlist; use keyfork_mnemonic::Wordlist;
/// ///
pub mod terminal; pub mod terminal;
pub mod validators; pub mod validators;
pub use terminal::{Terminal, DefaultTerminal, default_terminal}; pub use terminal::{default_terminal, DefaultTerminal, Terminal};
/// An error occurred while displaying a prompt. /// An error occurred while displaying a prompt.
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
@ -42,6 +40,9 @@ 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.
@ -57,8 +58,56 @@ 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.
#[cfg(feature = "mnemonic")] fn prompt_wordlist(&mut self, prompt: &str, wordlist: &[&str]) -> Result<String>;
fn prompt_wordlist<X>(&mut self, prompt: &str) -> Result<String> where X: Wordlist;
/// Prompt the user for a passphrase, which is hidden while typing.
///
/// # Errors
/// The method may return an error if the message was not able to be displayed or if the input
/// could not be read.
fn prompt_passphrase(&mut self, prompt: &str) -> Result<String>;
/// Prompt the user with a [`Message`].
///
/// # Errors
/// 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.
fn prompt_message(&mut self, prompt: 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 /// 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 /// provided parser function, returning the type from the parser. A language must be specified
@ -68,21 +117,25 @@ pub trait PromptHandler {
/// The method may return an error if the message was not able to be displayed, if the input /// 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. /// could not be read, or if the parser returned an error.
#[cfg(feature = "mnemonic")] #[cfg(feature = "mnemonic")]
fn prompt_validated_wordlist<X, V>( #[allow(clippy::missing_panics_doc)]
&mut self, pub fn prompt_validated_wordlist<X, V>(
handler: &mut dyn PromptHandler,
prompt: &str, prompt: &str,
retries: u8, retries: u8,
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>, validator_fn: &dyn Fn(String) -> Result<V, Box<dyn std::error::Error>>,
) -> Result<V, Error> ) -> Result<V, Error>
where where
X: Wordlist; X: Wordlist,
{
/// Prompt the user for a passphrase, which is hidden while typing. let wordlist = X::get_singleton();
/// let words = wordlist.to_str_array();
/// # Errors let mut opt: Option<V> = None;
/// The method may return an error if the message was not able to be displayed or if the input handler.prompt_validated_wordlist(prompt, retries, &words, &mut |string| {
/// could not be read. opt = Some(validator_fn(string)?);
fn prompt_passphrase(&mut self, prompt: &str) -> Result<String>; Ok(())
})?;
Ok(opt.unwrap())
}
/// Prompt the user for a passphrase, which is hidden while typing, and validate the passphrase /// 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. /// using a provided parser function, returning the type from the parser.
@ -90,17 +143,17 @@ pub trait PromptHandler {
/// # Errors /// # Errors
/// The method may return an error if the message was not able to be displayed, if the input /// 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. /// could not be read, or if the parser returned an error.
fn prompt_validated_passphrase<V>( #[allow(clippy::missing_panics_doc)]
&mut self, pub fn prompt_validated_passphrase<V>(
handler: &mut dyn PromptHandler,
prompt: &str, prompt: &str,
retries: u8, retries: u8,
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>, validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
) -> Result<V, Error>; ) -> Result<V, Error> {
let mut opt: Option<V> = None;
/// Prompt the user with a [`Message`]. handler.prompt_validated_passphrase(prompt, retries, &mut |string| {
/// opt = Some(validator_fn(string)?);
/// # Errors Ok(())
/// 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. Ok(opt.unwrap())
fn prompt_message(&mut self, prompt: impl Borrow<Message>) -> Result<()>;
} }

View File

@ -21,7 +21,7 @@ use keyfork_crossterm::{
use keyfork_bug::bug; use keyfork_bug::bug;
use crate::{Error, Message, PromptHandler, Wordlist}; use crate::{BoxResult, Error, Message, PromptHandler};
#[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,23 +198,20 @@ where
Ok(line) Ok(line)
} }
#[cfg(feature = "mnemonic")] fn prompt_validated_wordlist(
fn prompt_validated_wordlist<X, V>(
&mut self, &mut self,
prompt: &str, prompt: &str,
retries: u8, retries: u8,
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>, wordlist: &[&str],
) -> Result<V, Error> validator_fn: &mut dyn FnMut(String) -> BoxResult,
where ) -> Result<(), Error> {
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::<X>(prompt)?; let s = self.prompt_wordlist(prompt, wordlist)?;
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);
} }
} }
@ -227,15 +224,8 @@ where
)) ))
} }
#[cfg(feature = "mnemonic")]
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
fn prompt_wordlist<X>(&mut self, prompt: &str) -> Result<String> fn prompt_wordlist(&mut self, prompt: &str, wordlist: &[&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()?
@ -305,7 +295,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 words.iter().filter(|word| word.len() >= 4) { for word in wordlist.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(' ');
@ -351,7 +341,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 words.contains(&word) { if wordlist.contains(&word) {
terminal.queue(PrintStyledContent(word.green()))?; terminal.queue(PrintStyledContent(word.green()))?;
} else { } else {
terminal.queue(PrintStyledContent(word.red()))?; terminal.queue(PrintStyledContent(word.red()))?;
@ -372,19 +362,19 @@ where
Ok(input) Ok(input)
} }
fn prompt_validated_passphrase<V>( fn prompt_validated_passphrase(
&mut self, &mut self,
prompt: &str, prompt: &str,
retries: u8, retries: u8,
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>, validator_fn: &mut dyn FnMut(String) -> BoxResult,
) -> Result<V, Error> { ) -> Result<(), 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);
@ -461,7 +451,7 @@ where
Ok(passphrase) Ok(passphrase)
} }
fn prompt_message(&mut self, prompt: impl Borrow<Message>) -> Result<()> { fn prompt_message(&mut self, prompt: Message) -> Result<()> {
let mut terminal = self.lock().alternate_screen()?.raw_mode()?; let mut terminal = self.lock().alternate_screen()?.raw_mode()?;
loop { loop {