keyfork-prompt: traitify

This commit is contained in:
Ryan Heywood 2024-01-10 23:28:56 -05:00
parent 50cc58469d
commit b5320cabf3
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
7 changed files with 102 additions and 36 deletions

View File

@ -2,7 +2,7 @@ use std::io::{stdin, stdout};
use keyfork_prompt::{ use keyfork_prompt::{
validators::{mnemonic, Validator}, validators::{mnemonic, Validator},
Terminal, Terminal, PromptHandler,
}; };
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {

View File

@ -1,6 +1,6 @@
use std::{ use std::{
io::{stderr, stdin, BufRead, BufReader, Read, Stderr, Stdin, Write}, io::{stderr, stdin, BufRead, BufReader, Read, Stderr, Stdin, Write},
os::fd::AsRawFd, os::fd::AsRawFd, borrow::Borrow,
}; };
#[cfg(feature = "mnemonic")] #[cfg(feature = "mnemonic")]
@ -10,9 +10,9 @@ use keyfork_crossterm::{
cursor, cursor,
event::{read, DisableBracketedPaste, EnableBracketedPaste, Event, KeyCode, KeyModifiers}, event::{read, DisableBracketedPaste, EnableBracketedPaste, Event, KeyCode, KeyModifiers},
style::{Print, PrintStyledContent, Stylize}, style::{Print, PrintStyledContent, Stylize},
terminal::{self, TerminalIoctl, FdTerminal, EnterAlternateScreen, LeaveAlternateScreen}, terminal::{self, EnterAlternateScreen, FdTerminal, LeaveAlternateScreen, TerminalIoctl},
tty::IsTty, tty::IsTty,
QueueableCommand, ExecutableCommand ExecutableCommand, QueueableCommand,
}; };
pub mod validators; pub mod validators;
@ -39,18 +39,57 @@ pub enum Message {
Data(String), Data(String),
} }
struct TerminalGuard<'a, R, W> where W: Write + AsRawFd { pub trait PromptHandler {
fn prompt_input(&mut self, prompt: &str) -> Result<String>;
fn prompt_wordlist(&mut self, prompt: &str, wordlist: &Wordlist) -> Result<String>;
#[cfg(feature = "mnemonic")]
fn prompt_validated_wordlist<V, F, E>(
&mut self,
prompt: &str,
wordlist: &Wordlist,
retries: u8,
validator_fn: F,
) -> Result<V, Error>
where
F: Fn(String) -> Result<V, E>,
E: std::error::Error;
fn prompt_passphrase(&mut self, prompt: &str) -> Result<String>;
fn prompt_validated_passphrase<V, F, E>(
&mut self,
prompt: &str,
retries: u8,
validator_fn: F,
) -> Result<V, Error>
where
F: Fn(String) -> Result<V, E>,
E: std::error::Error;
fn prompt_message(&mut self, prompt: impl Borrow<Message>) -> Result<()>;
}
struct TerminalGuard<'a, R, W>
where
W: Write + AsRawFd,
{
read: &'a mut BufReader<R>, read: &'a mut BufReader<R>,
write: &'a mut W, write: &'a mut W,
terminal: &'a mut FdTerminal, terminal: &'a mut FdTerminal,
} }
impl<'a, R, W> TerminalGuard<'a, R, W> where W: Write + AsRawFd, R: Read { impl<'a, R, W> TerminalGuard<'a, R, W>
where
W: Write + AsRawFd,
R: Read,
{
fn new(read: &'a mut BufReader<R>, write: &'a mut W, terminal: &'a mut FdTerminal) -> Self { fn new(read: &'a mut BufReader<R>, write: &'a mut W, terminal: &'a mut FdTerminal) -> Self {
Self { Self {
read, read,
write, write,
terminal terminal,
} }
} }
@ -70,7 +109,11 @@ impl<'a, R, W> TerminalGuard<'a, R, W> where W: Write + AsRawFd, R: Read {
} }
} }
impl<R, W> TerminalIoctl for TerminalGuard<'_, R, W> where R: Read, W: Write + AsRawFd { impl<R, W> TerminalIoctl for TerminalGuard<'_, R, W>
where
R: Read,
W: Write + AsRawFd,
{
fn enable_raw_mode(&mut self) -> std::io::Result<()> { fn enable_raw_mode(&mut self) -> std::io::Result<()> {
self.terminal.enable_raw_mode() self.terminal.enable_raw_mode()
} }
@ -88,13 +131,21 @@ impl<R, W> TerminalIoctl for TerminalGuard<'_, R, W> where R: Read, W: Write + A
} }
} }
impl<R, W> Read for TerminalGuard<'_, R, W> where R: Read, W: Write + AsRawFd { impl<R, W> Read for TerminalGuard<'_, R, W>
where
R: Read,
W: Write + AsRawFd,
{
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.read.read(buf) self.read.read(buf)
} }
} }
impl<R, W> BufRead for TerminalGuard<'_, R, W> where R: Read, W: Write + AsRawFd { impl<R, W> BufRead for TerminalGuard<'_, R, W>
where
R: Read,
W: Write + AsRawFd,
{
fn fill_buf(&mut self) -> std::io::Result<&[u8]> { fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
self.read.fill_buf() self.read.fill_buf()
} }
@ -104,7 +155,10 @@ impl<R, W> BufRead for TerminalGuard<'_, R, W> where R: Read, W: Write + AsRawFd
} }
} }
impl<R, W> Write for TerminalGuard<'_, R, W> where W: Write + AsRawFd { impl<R, W> Write for TerminalGuard<'_, R, W>
where
W: Write + AsRawFd,
{
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.write.write(buf) self.write.write(buf)
} }
@ -114,7 +168,10 @@ impl<R, W> Write for TerminalGuard<'_, R, W> where W: Write + AsRawFd {
} }
} }
impl<R, W> Drop for TerminalGuard<'_, R, W> where W: Write + AsRawFd { impl<R, W> Drop for TerminalGuard<'_, R, W>
where
W: Write + AsRawFd,
{
fn drop(&mut self) { fn drop(&mut self) {
self.write.execute(DisableBracketedPaste).unwrap(); self.write.execute(DisableBracketedPaste).unwrap();
self.write.execute(LeaveAlternateScreen).unwrap(); self.write.execute(LeaveAlternateScreen).unwrap();
@ -148,7 +205,11 @@ where
TerminalGuard::new(&mut self.read, &mut self.write, &mut self.terminal) TerminalGuard::new(&mut self.read, &mut self.write, &mut self.terminal)
} }
pub fn prompt_input(&mut self, prompt: &str) -> Result<String> { }
impl<R, W> PromptHandler for Terminal<R, W> where R: Read + Sized, W: Write + AsRawFd + Sized {
fn prompt_input(&mut self, prompt: &str) -> Result<String> {
let mut terminal = self.lock().alternate_screen()?; let mut terminal = self.lock().alternate_screen()?;
terminal terminal
.queue(terminal::Clear(terminal::ClearType::All))? .queue(terminal::Clear(terminal::ClearType::All))?
@ -170,7 +231,7 @@ where
} }
#[cfg(feature = "mnemonic")] #[cfg(feature = "mnemonic")]
pub fn prompt_validated_wordlist<V, F, E>( fn prompt_validated_wordlist<V, F, E>(
&mut self, &mut self,
prompt: &str, prompt: &str,
wordlist: &Wordlist, wordlist: &Wordlist,
@ -202,8 +263,12 @@ where
#[cfg(feature = "mnemonic")] #[cfg(feature = "mnemonic")]
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
pub fn prompt_wordlist(&mut self, prompt: &str, wordlist: &Wordlist) -> Result<String> { fn prompt_wordlist(&mut self, prompt: &str, wordlist: &Wordlist) -> Result<String> {
let mut terminal = self.lock().alternate_screen()?.raw_mode()?.bracketed_paste()?; let mut terminal = self
.lock()
.alternate_screen()?
.raw_mode()?
.bracketed_paste()?;
terminal terminal
.queue(terminal::Clear(terminal::ClearType::All))? .queue(terminal::Clear(terminal::ClearType::All))?
@ -318,8 +383,7 @@ where
Ok(input) Ok(input)
} }
#[cfg(feature = "mnemonic")] fn prompt_validated_passphrase<V, F, E>(
pub fn prompt_validated_passphrase<V, F, E>(
&mut self, &mut self,
prompt: &str, prompt: &str,
retries: u8, retries: u8,
@ -335,7 +399,9 @@ where
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 passphrase: {e}")))?; self.prompt_message(&Message::Text(format!(
"Error validating passphrase: {e}"
)))?;
let _ = last_error.insert(e); let _ = last_error.insert(e);
} }
} }
@ -349,7 +415,7 @@ where
} }
// TODO: return secrecy::Secret<String> // TODO: return secrecy::Secret<String>
pub fn prompt_passphrase(&mut self, prompt: &str) -> Result<String> { fn prompt_passphrase(&mut self, prompt: &str) -> Result<String> {
let mut terminal = self.lock().alternate_screen()?.raw_mode()?; let mut terminal = self.lock().alternate_screen()?.raw_mode()?;
terminal terminal
@ -407,7 +473,7 @@ where
Ok(passphrase) Ok(passphrase)
} }
pub 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 {
@ -417,7 +483,7 @@ where
.queue(terminal::Clear(terminal::ClearType::All))? .queue(terminal::Clear(terminal::ClearType::All))?
.queue(cursor::MoveTo(0, 0))?; .queue(cursor::MoveTo(0, 0))?;
match &prompt { match prompt.borrow() {
Message::Text(text) => { Message::Text(text) => {
for line in text.lines() { for line in text.lines() {
let mut written_chars = 0; let mut written_chars = 0;

View File

@ -9,7 +9,7 @@ use keyfork_mnemonic_util::{Mnemonic, Wordlist};
use keyfork_prompt::{ use keyfork_prompt::{
qrencode, qrencode,
validators::{mnemonic::MnemonicSetValidator, Validator}, validators::{mnemonic::MnemonicSetValidator, Validator},
Message as PromptMessage, Terminal, Message as PromptMessage, Terminal, PromptHandler
}; };
use sha2::Sha256; use sha2::Sha256;
use sharks::{Share, Sharks}; use sharks::{Share, Sharks};
@ -59,12 +59,12 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
let key_mnemonic = let key_mnemonic =
Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?; Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?;
let combined_mnemonic = format!("{nonce_mnemonic} {key_mnemonic}"); let combined_mnemonic = format!("{nonce_mnemonic} {key_mnemonic}");
pm.prompt_message(&PromptMessage::Text(format!( pm.prompt_message(PromptMessage::Text(format!(
"Our words: {combined_mnemonic}" "Our words: {combined_mnemonic}"
)))?; )))?;
if let Ok(qrcode) = qrencode::qrencode(&combined_mnemonic) { if let Ok(qrcode) = qrencode::qrencode(&combined_mnemonic) {
pm.prompt_message(&PromptMessage::Data(qrcode))?; pm.prompt_message(PromptMessage::Data(qrcode))?;
} }
let validator = MnemonicSetValidator { let validator = MnemonicSetValidator {

View File

@ -19,7 +19,7 @@ use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError, MnemonicGenerationEr
use keyfork_prompt::{ use keyfork_prompt::{
qrencode, qrencode,
validators::{mnemonic::MnemonicSetValidator, Validator}, validators::{mnemonic::MnemonicSetValidator, Validator},
Error as PromptError, Message as PromptMessage, Terminal, Error as PromptError, Message as PromptMessage, Terminal, PromptHandler,
}; };
use openpgp::{ use openpgp::{
armor::{Kind, Writer}, armor::{Kind, Writer},
@ -476,12 +476,12 @@ pub fn decrypt(
let mnemonic = unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) }; let mnemonic = unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) };
let combined_mnemonic = format!("{our_mnemonic} {mnemonic}"); let combined_mnemonic = format!("{our_mnemonic} {mnemonic}");
pm.prompt_message(&PromptMessage::Text(format!( pm.prompt_message(PromptMessage::Text(format!(
"Our words: {combined_mnemonic}" "Our words: {combined_mnemonic}"
)))?; )))?;
if let Ok(qrcode) = qrencode::qrencode(&combined_mnemonic) { if let Ok(qrcode) = qrencode::qrencode(&combined_mnemonic) {
pm.prompt_message(&PromptMessage::Data(qrcode))?; pm.prompt_message(PromptMessage::Data(qrcode))?;
} }
Ok(()) Ok(())

View File

@ -1,4 +1,4 @@
use keyfork_prompt::{Error as PromptError, DefaultTerminal, default_terminal}; use keyfork_prompt::{Error as PromptError, DefaultTerminal, default_terminal, PromptHandler};
use super::openpgp::{ use super::openpgp::{
self, self,

View File

@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet};
use keyfork_prompt::{ use keyfork_prompt::{
default_terminal, default_terminal,
validators::{PinValidator, Validator}, validators::{PinValidator, Validator},
DefaultTerminal, Error as PromptError, Message, DefaultTerminal, Error as PromptError, Message, PromptHandler
}; };
use super::openpgp::{ use super::openpgp::{
@ -93,14 +93,14 @@ impl SmartcardManager {
/// Load any backend. /// Load any backend.
pub fn load_any_card(&mut self) -> Result<Fingerprint> { pub fn load_any_card(&mut self) -> Result<Fingerprint> {
let card_backend = loop { let card_backend = loop {
self.pm.prompt_message(&Message::Text( self.pm.prompt_message(Message::Text(
"Please plug in a smart card and press enter".to_string(), "Please plug in a smart card and press enter".to_string(),
))?; ))?;
if let Some(c) = PcscBackend::cards(None)?.next().transpose()? { if let Some(c) = PcscBackend::cards(None)?.next().transpose()? {
break c; break c;
} }
self.pm self.pm
.prompt_message(&Message::Text("No smart card was found".to_string()))?; .prompt_message(Message::Text("No smart card was found".to_string()))?;
}; };
let mut card = Card::<Open>::new(card_backend).map_err(Error::OpenSmartCard)?; let mut card = Card::<Open>::new(card_backend).map_err(Error::OpenSmartCard)?;
let transaction = card.transaction().map_err(Error::Transaction)?; let transaction = card.transaction().map_err(Error::Transaction)?;
@ -154,7 +154,7 @@ impl SmartcardManager {
} }
} }
self.pm.prompt_message(&Message::Text( self.pm.prompt_message(Message::Text(
"Please plug in a smart card and press enter".to_string(), "Please plug in a smart card and press enter".to_string(),
))?; ))?;
} }
@ -266,7 +266,7 @@ impl DecryptionHelper for &mut SmartcardManager {
} }
// NOTE: This should not be hit, because of the above validator. // NOTE: This should not be hit, because of the above validator.
Err(CardError::CardStatus(StatusBytes::IncorrectParametersCommandDataField)) => { Err(CardError::CardStatus(StatusBytes::IncorrectParametersCommandDataField)) => {
self.pm.prompt_message(&Message::Text( self.pm.prompt_message(Message::Text(
"Invalid PIN length entered.".to_string(), "Invalid PIN length entered.".to_string(),
))?; ))?;
} }

View File

@ -12,7 +12,7 @@ use keyfork_derive_util::{
}; };
use keyfork_prompt::{ use keyfork_prompt::{
validators::{PinValidator, Validator}, validators::{PinValidator, Validator},
Message, Terminal, Message, Terminal, PromptHandler,
}; };
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
@ -125,7 +125,7 @@ fn generate_shard_secret(threshold: u8, max: u8, keys_per_shard: u8) -> Result<(
for index in 0..max { for index in 0..max {
let cert = derive_key(&seed, index)?; let cert = derive_key(&seed, index)?;
for i in 0..keys_per_shard { for i in 0..keys_per_shard {
pm.prompt_message(&Message::Text(format!( pm.prompt_message(Message::Text(format!(
"Please insert key #{} for user #{}", "Please insert key #{} for user #{}",
i + 1, i + 1,
index + 1, index + 1,