diff --git a/crates/keyfork-shard/src/lib.rs b/crates/keyfork-shard/src/lib.rs index 8abf5e8..5a0963d 100644 --- a/crates/keyfork-shard/src/lib.rs +++ b/crates/keyfork-shard/src/lib.rs @@ -11,10 +11,12 @@ use aes_gcm::{ Aes256Gcm, KeyInit, Nonce, }; use base64::prelude::{Engine, BASE64_STANDARD}; +use blahaj::{Share, Sharks}; use hkdf::Hkdf; use keyfork_bug::{bug, POISONED_MUTEX}; use keyfork_mnemonic::{English, Mnemonic}; use keyfork_prompt::{ + prompt_validated_wordlist, validators::{ mnemonic::{MnemonicSetValidator, MnemonicValidator, WordLength}, Validator, @@ -22,7 +24,6 @@ use keyfork_prompt::{ Message as PromptMessage, PromptHandler, Terminal, }; use sha2::Sha256; -use blahaj::{Share, Sharks}; use x25519_dalek::{EphemeralSecret, PublicKey}; const PLAINTEXT_LENGTH: u8 = 32 // shard @@ -233,6 +234,17 @@ pub trait Format { let validator = MnemonicValidator { word_length: Some(WordLength::Count(24)), }; + let mut prompt = prompt.lock().expect(bug!(POISONED_MUTEX)); + prompt_validated_wordlist::( + &mut *prompt, + QRCODE_COULDNT_READ, + 3, + &*validator.to_fn(), + )? + .as_bytes() + .try_into() + .map_err(|_| InvalidData)? + /* prompt .lock() .expect(bug!(POISONED_MUTEX)) @@ -244,6 +256,7 @@ pub trait Format { .as_bytes() .try_into() .map_err(|_| InvalidData)? + */ } }; @@ -501,12 +514,12 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box( - QRCODE_COULDNT_READ, - 3, - validator.to_fn(), - )?; + let [pubkey_mnemonic, payload_mnemonic] = prompt_validated_wordlist::( + &mut pm, + QRCODE_COULDNT_READ, + 3, + &*validator.to_fn(), + )?; let pubkey = pubkey_mnemonic .as_bytes() .try_into() diff --git a/crates/keyfork-shard/src/openpgp/smartcard.rs b/crates/keyfork-shard/src/openpgp/smartcard.rs index c1ed0c3..48b4e32 100644 --- a/crates/keyfork-shard/src/openpgp/smartcard.rs +++ b/crates/keyfork-shard/src/openpgp/smartcard.rs @@ -7,6 +7,7 @@ use std::{ use keyfork_bug::{bug, POISONED_MUTEX}; use keyfork_prompt::{ + prompt_validated_passphrase, validators::{PinValidator, Validator}, Error as PromptError, Message, PromptHandler, }; @@ -275,11 +276,8 @@ impl DecryptionHelper for &mut SmartcardManager

{ } else { format!("Unlock card {card_id} ({cardholder_name})\n{rpea}: {attempts}\n\nPIN: ") }; - let temp_pin = self - .pm - .lock() - .expect(bug!(POISONED_MUTEX)) - .prompt_validated_passphrase(&message, 3, &pin_validator)?; + let mut prompt = self.pm.lock().expect(bug!(POISONED_MUTEX)); + let temp_pin = prompt_validated_passphrase(&mut *prompt, &message, 3, &pin_validator)?; let verification_status = transaction.verify_user_pin(temp_pin.as_str().trim()); match verification_status { #[allow(clippy::ignored_unit_patterns)] diff --git a/crates/keyfork/src/cli/recover.rs b/crates/keyfork/src/cli/recover.rs index 96d3e6a..fa6a604 100644 --- a/crates/keyfork/src/cli/recover.rs +++ b/crates/keyfork/src/cli/recover.rs @@ -3,7 +3,14 @@ use clap::{Parser, Subcommand}; use std::path::PathBuf; 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}; type Result> = std::result::Result; @@ -54,21 +61,15 @@ impl RecoverSubcommands { Ok(seed) } RecoverSubcommands::Mnemonic {} => { - use keyfork_prompt::{ - validators::{ - mnemonic::{MnemonicChoiceValidator, WordLength}, - Validator, - }, - PromptHandler, - }; let mut term = default_terminal()?; let validator = MnemonicChoiceValidator { word_lengths: [WordLength::Count(12), WordLength::Count(24)], }; - let mnemonic = term.prompt_validated_wordlist::( + let mnemonic = prompt_validated_wordlist::( + &mut term, "Mnemonic: ", 3, - validator.to_fn(), + &*validator.to_fn(), )?; Ok(mnemonic.to_bytes()) } diff --git a/crates/keyfork/src/cli/wizard.rs b/crates/keyfork/src/cli/wizard.rs index 7e9b728..a15a179 100644 --- a/crates/keyfork/src/cli/wizard.rs +++ b/crates/keyfork/src/cli/wizard.rs @@ -20,7 +20,7 @@ use keyfork_derive_path_data::paths; use keyfork_derive_util::DerivationIndex; use keyfork_mnemonic::Mnemonic; use keyfork_prompt::{ - default_terminal, + default_terminal, prompt_validated_passphrase, validators::{SecurePinValidator, Validator}, DefaultTerminal, Message, PromptHandler, }; @@ -213,12 +213,14 @@ impl GenerateShardSecret { .to_string(), ))?; }; - let user_pin = pm.prompt_validated_passphrase( + let user_pin = prompt_validated_passphrase( + &mut pm, "Please enter the new smartcard User PIN: ", 3, &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: ", 3, &admin_pin_validator, diff --git a/crates/util/keyfork-prompt/examples/test-basic-prompt.rs b/crates/util/keyfork-prompt/examples/test-basic-prompt.rs index 11e6d97..17199ac 100644 --- a/crates/util/keyfork-prompt/examples/test-basic-prompt.rs +++ b/crates/util/keyfork-prompt/examples/test-basic-prompt.rs @@ -3,8 +3,9 @@ use std::io::{stdin, stdout}; use keyfork_prompt::{ + prompt_validated_wordlist, validators::{mnemonic, Validator}, - Terminal, PromptHandler, + Terminal, }; use keyfork_mnemonic::English; @@ -18,18 +19,20 @@ fn main() -> Result<(), Box> { word_lengths: [24, 48], }; - let mnemonics = mgr.prompt_validated_wordlist::( + let mnemonics = prompt_validated_wordlist::( + &mut mgr, "Enter a 9-word and 24-word mnemonic: ", 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::( + let mnemonics = prompt_validated_wordlist::( + &mut mgr, "Enter a 24 and 48-word mnemonic: ", 3, - combine_validator.to_fn(), + &*combine_validator.to_fn(), )?; assert_eq!(mnemonics[0].as_bytes().len(), 32); assert_eq!(mnemonics[1].as_bytes().len(), 64); diff --git a/crates/util/keyfork-prompt/src/lib.rs b/crates/util/keyfork-prompt/src/lib.rs index 3cf6eda..8be0fbb 100644 --- a/crates/util/keyfork-prompt/src/lib.rs +++ b/crates/util/keyfork-prompt/src/lib.rs @@ -1,14 +1,12 @@ //! Prompt display and interaction management. -use std::borrow::Borrow; - #[cfg(feature = "mnemonic")] use keyfork_mnemonic::Wordlist; /// pub mod terminal; pub mod validators; -pub use terminal::{Terminal, DefaultTerminal, default_terminal}; +pub use terminal::{default_terminal, DefaultTerminal, Terminal}; /// An error occurred while displaying a prompt. #[derive(thiserror::Error, Debug)] @@ -42,6 +40,9 @@ pub enum Message { Data(String), } +#[doc(hidden)] +pub type BoxResult = std::result::Result<(), Box>; + /// A trait to allow displaying prompts and accepting input. pub trait PromptHandler { /// Prompt the user for input. @@ -57,25 +58,7 @@ pub trait PromptHandler { /// # Errors /// The method may return an error if the message was not able to be displayed or if the input /// could not be read. - #[cfg(feature = "mnemonic")] - fn prompt_wordlist(&mut self, prompt: &str) -> Result 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( - &mut self, - prompt: &str, - retries: u8, - validator_fn: impl Fn(String) -> Result>, - ) -> Result - where - X: Wordlist; + fn prompt_wordlist(&mut self, prompt: &str, wordlist: &[&str]) -> Result; /// Prompt the user for a passphrase, which is hidden while typing. /// @@ -84,23 +67,93 @@ pub trait PromptHandler { /// could not be read. fn prompt_passphrase(&mut self, prompt: &str) -> Result; - /// 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( - &mut self, - prompt: &str, - retries: u8, - validator_fn: impl Fn(String) -> Result>, - ) -> Result; - /// 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: impl Borrow) -> Result<()>; + 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 +/// 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( + handler: &mut dyn PromptHandler, + prompt: &str, + retries: u8, + validator_fn: &dyn Fn(String) -> Result>, +) -> Result +where + X: Wordlist, +{ + let wordlist = X::get_singleton(); + let words = wordlist.to_str_array(); + let mut opt: Option = 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( + handler: &mut dyn PromptHandler, + prompt: &str, + retries: u8, + validator_fn: impl Fn(String) -> Result>, +) -> Result { + let mut opt: Option = None; + handler.prompt_validated_passphrase(prompt, retries, &mut |string| { + opt = Some(validator_fn(string)?); + Ok(()) + })?; + Ok(opt.unwrap()) } diff --git a/crates/util/keyfork-prompt/src/terminal.rs b/crates/util/keyfork-prompt/src/terminal.rs index 8247c45..fdfd388 100644 --- a/crates/util/keyfork-prompt/src/terminal.rs +++ b/crates/util/keyfork-prompt/src/terminal.rs @@ -21,7 +21,7 @@ use keyfork_crossterm::{ use keyfork_bug::bug; -use crate::{Error, Message, PromptHandler, Wordlist}; +use crate::{BoxResult, Error, Message, PromptHandler}; #[allow(missing_docs)] pub type Result = std::result::Result; @@ -198,23 +198,20 @@ where Ok(line) } - #[cfg(feature = "mnemonic")] - fn prompt_validated_wordlist( + fn prompt_validated_wordlist( &mut self, prompt: &str, retries: u8, - validator_fn: impl Fn(String) -> Result>, - ) -> Result - where - X: Wordlist, - { + wordlist: &[&str], + validator_fn: &mut dyn FnMut(String) -> BoxResult, + ) -> Result<(), Error> { let mut last_error = None; for _ in 0..retries { - let s = self.prompt_wordlist::(prompt)?; + let s = self.prompt_wordlist(prompt, wordlist)?; match validator_fn(s) { Ok(v) => return Ok(v), 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); } } @@ -227,15 +224,8 @@ where )) } - #[cfg(feature = "mnemonic")] #[allow(clippy::too_many_lines)] - fn prompt_wordlist(&mut self, prompt: &str) -> Result - where - X: Wordlist, - { - let wordlist = X::get_singleton(); - let words = wordlist.to_str_array(); - + fn prompt_wordlist(&mut self, prompt: &str, wordlist: &[&str]) -> Result { let mut terminal = self .lock() .alternate_screen()? @@ -305,7 +295,7 @@ where let word = input.split_whitespace().next_back().map(ToOwned::to_owned); if let Some(steel_word) = word { 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 { input.push_str(&word[4..]); input.push(' '); @@ -351,7 +341,7 @@ where let mut iter = printable_input.split_whitespace().peekable(); while let Some(word) = iter.next() { - if words.contains(&word) { + if wordlist.contains(&word) { terminal.queue(PrintStyledContent(word.green()))?; } else { terminal.queue(PrintStyledContent(word.red()))?; @@ -372,19 +362,19 @@ where Ok(input) } - fn prompt_validated_passphrase( + fn prompt_validated_passphrase( &mut self, prompt: &str, retries: u8, - validator_fn: impl Fn(String) -> Result>, - ) -> Result { + validator_fn: &mut dyn FnMut(String) -> BoxResult, + ) -> Result<(), Error> { let mut last_error = None; for _ in 0..retries { let s = self.prompt_passphrase(prompt)?; match validator_fn(s) { Ok(v) => return Ok(v), Err(e) => { - self.prompt_message(&Message::Text(format!( + self.prompt_message(Message::Text(format!( "Error validating passphrase: {e}" )))?; let _ = last_error.insert(e); @@ -461,7 +451,7 @@ where Ok(passphrase) } - fn prompt_message(&mut self, prompt: impl Borrow) -> Result<()> { + fn prompt_message(&mut self, prompt: Message) -> Result<()> { let mut terminal = self.lock().alternate_screen()?.raw_mode()?; loop {