From 5d2309e301fc0ec4b2a7c4b46060008ed589d412 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 18 Apr 2024 23:01:03 -0400 Subject: [PATCH] keyfork-prompt: add SecurePinValidator for making new, secure, PINs --- crates/keyfork/src/cli/wizard.rs | 6 +- crates/util/keyfork-prompt/src/validators.rs | 80 +++++++++++++++++++- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/crates/keyfork/src/cli/wizard.rs b/crates/keyfork/src/cli/wizard.rs index 596973e..ef5d9f3 100644 --- a/crates/keyfork/src/cli/wizard.rs +++ b/crates/keyfork/src/cli/wizard.rs @@ -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() } diff --git a/crates/util/keyfork-prompt/src/validators.rs b/crates/util/keyfork-prompt/src/validators.rs index 82ba8af..8f2913c 100644 --- a/crates/util/keyfork-prompt/src/validators.rs +++ b/crates/util/keyfork-prompt/src/validators.rs @@ -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, + + /// The maximum length of provided PINs. + pub max_length: Option, + + /// The characters allowed by the PIN parser. + pub range: Option>, + + /// 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 Result>> { + 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::>(); + 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)]