keyfork-prompt: add validator system
This commit is contained in:
parent
37d2f09c6b
commit
4384964ea5
|
@ -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)?;
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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(_) => {}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue