keyfork-prompt: add validator system

This commit is contained in:
Ryan Heywood 2024-01-09 02:21:46 -05:00
parent 37d2f09c6b
commit 4384964ea5
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
4 changed files with 166 additions and 8 deletions

View File

@ -20,6 +20,8 @@ mod raw_mode;
use alternate_screen::AlternateScreen;
use raw_mode::RawMode;
pub mod validators;
#[cfg(feature = "qrencode")]
pub mod qrencode;
@ -28,6 +30,9 @@ pub enum Error {
#[error("The given handler is not a TTY")]
NotATTY,
#[error("Validation of the input failed after {0} retries (last error: {1})")]
Validation(u8, String),
#[error("IO Error: {0}")]
IO(#[from] std::io::Error),
}
@ -80,6 +85,37 @@ where
Ok(line)
}
#[cfg(feature = "mnemonic")]
pub 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,
{
let mut last_error = None;
for _ in 0..retries {
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}")))?;
let _ = last_error.insert(e);
}
}
}
Err(Error::Validation(
retries,
last_error
.map(|e| e.to_string())
.unwrap_or_else(|| "Unknown".to_string()),
))
}
// TODO: create a wrapper for bracketed paste similar to RawMode
#[cfg(feature = "mnemonic")]
#[allow(clippy::too_many_lines)]
@ -204,6 +240,36 @@ where
Ok(input)
}
#[cfg(feature = "mnemonic")]
pub 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,
{
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!("Error validating passphrase: {e}")))?;
let _ = last_error.insert(e);
}
}
}
Err(Error::Validation(
retries,
last_error
.map(|e| e.to_string())
.unwrap_or_else(|| "Unknown".to_string()),
))
}
// TODO: return secrecy::Secret<String>
pub fn prompt_passphrase(&mut self, prompt: &str) -> Result<String> {
let mut terminal = AlternateScreen::new(&mut self.write)?;

View File

@ -0,0 +1,54 @@
#![allow(clippy::type_complexity)]
pub trait Validator {
type Output;
type Error;
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Self::Error>>;
}
#[derive(thiserror::Error, Debug)]
pub enum PinError {
#[error("PIN too short: {0} < {1}")]
TooShort(usize, usize),
#[error("PIN too long: {0} > {1}")]
TooLong(usize, usize),
#[error("PIN contained invalid characters (found {0} at position {1})")]
InvalidCharacters(char, usize),
}
#[derive(Default, Clone)]
pub struct PinValidator {
pub min_length: Option<usize>,
pub max_length: Option<usize>,
pub range: Option<std::ops::RangeInclusive<char>>,
}
impl Validator for PinValidator {
type Output = String;
type Error = PinError;
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<String, PinError>> {
let min_len = self.min_length.unwrap_or(usize::MIN);
let max_len = self.max_length.unwrap_or(usize::MAX);
let range = self.range.clone().unwrap_or('0'..='9');
Box::new(move |mut s: String| {
s.truncate(s.trim_end().len());
let len = s.len();
if len < min_len {
return Err(PinError::TooShort(len, min_len));
}
if len > max_len {
return Err(PinError::TooLong(len, max_len));
}
for (index, ch) in s.chars().enumerate() {
if !range.contains(&ch) {
return Err(PinError::InvalidCharacters(ch, index));
}
}
Ok(s)
})
}
}

View File

@ -1,6 +1,10 @@
use std::collections::{HashMap, HashSet};
use keyfork_prompt::{default_prompt_manager, DefaultPromptManager, Error as PromptError, Message};
use keyfork_prompt::{
default_prompt_manager,
validators::{PinValidator, Validator},
DefaultPromptManager, Error as PromptError, Message,
};
use super::openpgp::{
self,
@ -222,6 +226,11 @@ impl DecryptionHelper for &mut SmartcardManager {
.context("Could not load application identifier")?
.ident();
let mut pin = self.pin_cache.get(&fp).cloned();
let pin_validator = PinValidator {
min_length: Some(6),
..Default::default()
}
.to_fn();
while transaction
.pw_status_bytes()
.map_err(Error::PwStatusBytes)?
@ -240,7 +249,9 @@ impl DecryptionHelper for &mut SmartcardManager {
} else {
format!("Unlock card {card_id} ({cardholder_name})\n{rpea}: {attempts}\n\nPIN: ")
};
let temp_pin = self.pm.prompt_passphrase(&message)?;
let temp_pin = self
.pm
.prompt_validated_passphrase(&message, 3, &pin_validator)?;
let verification_status = transaction.verify_user_pin(temp_pin.as_str().trim());
match verification_status {
#[allow(clippy::ignored_unit_patterns)]
@ -248,8 +259,11 @@ impl DecryptionHelper for &mut SmartcardManager {
self.pin_cache.insert(fp.clone(), temp_pin.clone());
pin.replace(temp_pin);
}
// NOTE: This should not be hit, because of the above validator.
Err(CardError::CardStatus(StatusBytes::IncorrectParametersCommandDataField)) => {
self.pm.prompt_message(&Message::Text("Invalid PIN length entered.".to_string()))?;
self.pm.prompt_message(&Message::Text(
"Invalid PIN length entered.".to_string(),
))?;
}
Err(_) => {}
}

View File

@ -10,7 +10,14 @@ use keyfork_derive_util::{
request::{DerivationAlgorithm, DerivationRequest},
DerivationIndex, DerivationPath,
};
use keyfork_prompt::{Message, PromptManager};
use keyfork_prompt::{
validators::{PinValidator, Validator},
Message, PromptManager,
};
#[derive(thiserror::Error, Debug)]
#[error("Invalid PIN length: {0}")]
pub struct PinLength(usize);
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
@ -105,6 +112,17 @@ fn generate_shard_secret(threshold: u8, max: u8, keys_per_shard: u8) -> Result<(
"not printing shard to terminal, redirect output"
);
let user_pin_validator = PinValidator {
min_length: Some(6),
..Default::default()
}
.to_fn();
let admin_pin_validator = PinValidator {
min_length: Some(8),
..Default::default()
}
.to_fn();
for index in 0..max {
let cert = derive_key(&seed, index)?;
for i in 0..keys_per_shard {
@ -113,10 +131,16 @@ fn generate_shard_secret(threshold: u8, max: u8, keys_per_shard: u8) -> Result<(
i + 1,
index + 1,
)))?;
// TODO: add a second prompt for verification, perhaps as an argument to
// prompt_passphrase
let user_pin = pm.prompt_passphrase("Please enter the new smartcard User PIN: ")?;
let admin_pin = pm.prompt_passphrase("Please enter the new smartcard Admin PIN: ")?;
let user_pin = pm.prompt_validated_passphrase(
"Please enter the new smartcard User PIN: ",
3,
&user_pin_validator,
)?;
let admin_pin = pm.prompt_validated_passphrase(
"Please enter the new smartcard Admin PIN: ",
3,
&admin_pin_validator,
)?;
factory_reset_current_card(&mut seen_cards, user_pin.trim(), admin_pin.trim(), &cert)?;
}
certs.push(cert);