keyfork-prompt: add SecurePinValidator for making new, secure, PINs

This commit is contained in:
Ryan Heywood 2024-04-18 23:01:03 -04:00
parent c0b19e2457
commit 5d2309e301
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
2 changed files with 82 additions and 4 deletions

View File

@ -11,7 +11,7 @@ use keyfork_derive_openpgp::{
};
use keyfork_derive_util::{DerivationIndex, DerivationPath};
use keyfork_prompt::{
validators::{PinValidator, Validator},
validators::{SecurePinValidator, Validator},
Message, PromptHandler, DefaultTerminal, default_terminal
};
@ -116,12 +116,12 @@ fn generate_shard_secret(
);
}
let user_pin_validator = PinValidator {
let user_pin_validator = SecurePinValidator {
min_length: Some(6),
..Default::default()
}
.to_fn();
let admin_pin_validator = PinValidator {
let admin_pin_validator = SecurePinValidator {
min_length: Some(8),
..Default::default()
}

View File

@ -29,6 +29,84 @@ pub enum PinError {
/// The PIN contained invalid characters.
#[error("PIN contained invalid characters (found {0} at position {1})")]
InvalidCharacters(char, usize),
/// The provided PIN had either too many repeated characters or too many sequential characters.
#[error("PIN contained too many repeated or sequential characters")]
InsecurePIN,
}
/// Validate that a PIN is of a certain length, matches a range of characters, and does not use
/// incrementing or decrementing sequences of characters.
///
/// The validator determines a score for a passphrase and, if the score is high enough, returns an
/// error.
///
/// Score is calculated based on:
/// * how many sequential characters are in the passphrase (ascending or descending)
/// * how many repeated characters are in the passphrase
#[derive(Default, Clone)]
pub struct SecurePinValidator {
/// The minimum length of provided PINs.
pub min_length: Option<usize>,
/// The maximum length of provided PINs.
pub max_length: Option<usize>,
/// The characters allowed by the PIN parser.
pub range: Option<RangeInclusive<char>>,
/// Whether repeated characters count against the PIN.
pub ignore_repeated_characters: bool,
/// Whether sequential characters count against the PIN.
pub ignore_sequential_characters: bool,
}
impl Validator for SecurePinValidator {
type Output = String;
type Error = PinError;
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<String, Box<dyn std::error::Error>>> {
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');
let ignore_repeated_characters = self.ignore_repeated_characters;
let ignore_sequential_characters = self.ignore_sequential_characters;
Box::new(move |mut s: String| {
s.truncate(s.trim_end().len());
let len = s.len();
if len < min_len {
return Err(Box::new(PinError::TooShort(len, min_len)));
}
if len > max_len {
return Err(Box::new(PinError::TooLong(len, max_len)));
}
let mut last_char = 0;
let mut score = 0;
for (index, ch) in s.chars().enumerate() {
if !range.contains(&ch) {
return Err(Box::new(PinError::InvalidCharacters(ch, index)));
}
if [-1, 1].contains(&(ch as i32 - last_char))
&& !ignore_sequential_characters
{
score += 1;
}
last_char = ch as i32;
}
let mut chars = s.chars().collect::<Vec<_>>();
chars.sort();
chars.dedup();
if !ignore_repeated_characters {
// SAFETY: the amount of characters can't have _increased_ since deduping
score += s.chars().count() - chars.len();
}
if score * 2 > s.chars().count() {
return Err(Box::new(PinError::InsecurePIN))
}
Ok(s)
})
}
}
/// Validate that a PIN is of a certain length and matches a range of characters.
@ -79,8 +157,8 @@ pub mod mnemonic {
use super::Validator;
use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError};
use keyfork_bug::bug;
use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError};
/// A mnemonic could not be validated from the given input.
#[derive(thiserror::Error, Debug)]