keyfork-prompt: add SecurePinValidator for making new, secure, PINs
This commit is contained in:
parent
c0b19e2457
commit
5d2309e301
|
@ -11,7 +11,7 @@ use keyfork_derive_openpgp::{
|
||||||
};
|
};
|
||||||
use keyfork_derive_util::{DerivationIndex, DerivationPath};
|
use keyfork_derive_util::{DerivationIndex, DerivationPath};
|
||||||
use keyfork_prompt::{
|
use keyfork_prompt::{
|
||||||
validators::{PinValidator, Validator},
|
validators::{SecurePinValidator, Validator},
|
||||||
Message, PromptHandler, DefaultTerminal, default_terminal
|
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),
|
min_length: Some(6),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
.to_fn();
|
.to_fn();
|
||||||
let admin_pin_validator = PinValidator {
|
let admin_pin_validator = SecurePinValidator {
|
||||||
min_length: Some(8),
|
min_length: Some(8),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,84 @@ pub enum PinError {
|
||||||
/// The PIN contained invalid characters.
|
/// The PIN contained invalid characters.
|
||||||
#[error("PIN contained invalid characters (found {0} at position {1})")]
|
#[error("PIN contained invalid characters (found {0} at position {1})")]
|
||||||
InvalidCharacters(char, usize),
|
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.
|
/// 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 super::Validator;
|
||||||
|
|
||||||
use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError};
|
|
||||||
use keyfork_bug::bug;
|
use keyfork_bug::bug;
|
||||||
|
use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError};
|
||||||
|
|
||||||
/// A mnemonic could not be validated from the given input.
|
/// A mnemonic could not be validated from the given input.
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
|
Loading…
Reference in New Issue