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 alternate_screen::AlternateScreen;
|
||||||
use raw_mode::RawMode;
|
use raw_mode::RawMode;
|
||||||
|
|
||||||
|
pub mod validators;
|
||||||
|
|
||||||
#[cfg(feature = "qrencode")]
|
#[cfg(feature = "qrencode")]
|
||||||
pub mod qrencode;
|
pub mod qrencode;
|
||||||
|
|
||||||
|
@ -28,6 +30,9 @@ pub enum Error {
|
||||||
#[error("The given handler is not a TTY")]
|
#[error("The given handler is not a TTY")]
|
||||||
NotATTY,
|
NotATTY,
|
||||||
|
|
||||||
|
#[error("Validation of the input failed after {0} retries (last error: {1})")]
|
||||||
|
Validation(u8, String),
|
||||||
|
|
||||||
#[error("IO Error: {0}")]
|
#[error("IO Error: {0}")]
|
||||||
IO(#[from] std::io::Error),
|
IO(#[from] std::io::Error),
|
||||||
}
|
}
|
||||||
|
@ -80,6 +85,37 @@ where
|
||||||
Ok(line)
|
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
|
// TODO: create a wrapper for bracketed paste similar to RawMode
|
||||||
#[cfg(feature = "mnemonic")]
|
#[cfg(feature = "mnemonic")]
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
|
@ -204,6 +240,36 @@ where
|
||||||
Ok(input)
|
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>
|
// TODO: return secrecy::Secret<String>
|
||||||
pub fn prompt_passphrase(&mut self, prompt: &str) -> Result<String> {
|
pub fn prompt_passphrase(&mut self, prompt: &str) -> Result<String> {
|
||||||
let mut terminal = AlternateScreen::new(&mut self.write)?;
|
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 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::{
|
use super::openpgp::{
|
||||||
self,
|
self,
|
||||||
|
@ -222,6 +226,11 @@ impl DecryptionHelper for &mut SmartcardManager {
|
||||||
.context("Could not load application identifier")?
|
.context("Could not load application identifier")?
|
||||||
.ident();
|
.ident();
|
||||||
let mut pin = self.pin_cache.get(&fp).cloned();
|
let mut pin = self.pin_cache.get(&fp).cloned();
|
||||||
|
let pin_validator = PinValidator {
|
||||||
|
min_length: Some(6),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.to_fn();
|
||||||
while transaction
|
while transaction
|
||||||
.pw_status_bytes()
|
.pw_status_bytes()
|
||||||
.map_err(Error::PwStatusBytes)?
|
.map_err(Error::PwStatusBytes)?
|
||||||
|
@ -240,7 +249,9 @@ impl DecryptionHelper for &mut SmartcardManager {
|
||||||
} 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.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());
|
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)]
|
||||||
|
@ -248,8 +259,11 @@ impl DecryptionHelper for &mut SmartcardManager {
|
||||||
self.pin_cache.insert(fp.clone(), temp_pin.clone());
|
self.pin_cache.insert(fp.clone(), temp_pin.clone());
|
||||||
pin.replace(temp_pin);
|
pin.replace(temp_pin);
|
||||||
}
|
}
|
||||||
|
// 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("Invalid PIN length entered.".to_string()))?;
|
self.pm.prompt_message(&Message::Text(
|
||||||
|
"Invalid PIN length entered.".to_string(),
|
||||||
|
))?;
|
||||||
}
|
}
|
||||||
Err(_) => {}
|
Err(_) => {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,14 @@ use keyfork_derive_util::{
|
||||||
request::{DerivationAlgorithm, DerivationRequest},
|
request::{DerivationAlgorithm, DerivationRequest},
|
||||||
DerivationIndex, DerivationPath,
|
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>;
|
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"
|
"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 {
|
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 {
|
||||||
|
@ -113,10 +131,16 @@ fn generate_shard_secret(threshold: u8, max: u8, keys_per_shard: u8) -> Result<(
|
||||||
i + 1,
|
i + 1,
|
||||||
index + 1,
|
index + 1,
|
||||||
)))?;
|
)))?;
|
||||||
// TODO: add a second prompt for verification, perhaps as an argument to
|
let user_pin = pm.prompt_validated_passphrase(
|
||||||
// prompt_passphrase
|
"Please enter the new smartcard User PIN: ",
|
||||||
let user_pin = pm.prompt_passphrase("Please enter the new smartcard User PIN: ")?;
|
3,
|
||||||
let admin_pin = pm.prompt_passphrase("Please enter the new smartcard Admin PIN: ")?;
|
&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)?;
|
factory_reset_current_card(&mut seen_cards, user_pin.trim(), admin_pin.trim(), &cert)?;
|
||||||
}
|
}
|
||||||
certs.push(cert);
|
certs.push(cert);
|
||||||
|
|
Loading…
Reference in New Issue