2024-01-16 02:44:48 +00:00
|
|
|
//! Validator and parser types.
|
|
|
|
|
2024-01-09 07:21:46 +00:00
|
|
|
#![allow(clippy::type_complexity)]
|
2024-01-10 20:13:42 +00:00
|
|
|
use std::ops::RangeInclusive;
|
2024-01-09 07:21:46 +00:00
|
|
|
|
2024-01-16 02:44:48 +00:00
|
|
|
/// A trait to create validator functions.
|
2024-01-09 07:21:46 +00:00
|
|
|
pub trait Validator {
|
2024-01-16 02:44:48 +00:00
|
|
|
/// The output of the validator function.
|
2024-01-09 07:21:46 +00:00
|
|
|
type Output;
|
2024-01-16 02:44:48 +00:00
|
|
|
|
|
|
|
/// The error type returned from the validator function.
|
2024-01-09 07:21:46 +00:00
|
|
|
type Error;
|
|
|
|
|
2024-01-16 02:44:48 +00:00
|
|
|
/// Create a validator function from the given parameters.
|
2024-02-19 10:32:24 +00:00
|
|
|
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Box<dyn std::error::Error>>>;
|
2024-01-09 07:21:46 +00:00
|
|
|
}
|
|
|
|
|
2024-01-16 02:44:48 +00:00
|
|
|
/// A PIN could not be validated from the given input.
|
2024-01-09 07:21:46 +00:00
|
|
|
#[derive(thiserror::Error, Debug)]
|
|
|
|
pub enum PinError {
|
2024-01-16 02:44:48 +00:00
|
|
|
/// The provided PIN was too short.
|
2024-01-09 07:21:46 +00:00
|
|
|
#[error("PIN too short: {0} < {1}")]
|
|
|
|
TooShort(usize, usize),
|
|
|
|
|
2024-01-16 02:44:48 +00:00
|
|
|
/// The provided PIN was too long.
|
2024-01-09 07:21:46 +00:00
|
|
|
#[error("PIN too long: {0} > {1}")]
|
|
|
|
TooLong(usize, usize),
|
|
|
|
|
2024-01-16 02:44:48 +00:00
|
|
|
/// The PIN contained invalid characters.
|
2024-01-09 07:21:46 +00:00
|
|
|
#[error("PIN contained invalid characters (found {0} at position {1})")]
|
|
|
|
InvalidCharacters(char, usize),
|
2024-04-19 03:01:03 +00:00
|
|
|
|
|
|
|
/// 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)
|
|
|
|
})
|
|
|
|
}
|
2024-01-09 07:21:46 +00:00
|
|
|
}
|
|
|
|
|
2024-01-10 20:13:42 +00:00
|
|
|
/// Validate that a PIN is of a certain length and matches a range of characters.
|
2024-01-09 07:21:46 +00:00
|
|
|
#[derive(Default, Clone)]
|
|
|
|
pub struct PinValidator {
|
2024-01-16 02:44:48 +00:00
|
|
|
/// The minimum length of provided PINs.
|
2024-01-09 07:21:46 +00:00
|
|
|
pub min_length: Option<usize>,
|
2024-01-16 02:44:48 +00:00
|
|
|
|
|
|
|
/// The maximum length of provided PINs.
|
2024-01-09 07:21:46 +00:00
|
|
|
pub max_length: Option<usize>,
|
2024-01-16 02:44:48 +00:00
|
|
|
|
|
|
|
/// The characters allowed by the PIN parser.
|
2024-01-10 20:13:42 +00:00
|
|
|
pub range: Option<RangeInclusive<char>>,
|
2024-01-09 07:21:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Validator for PinValidator {
|
|
|
|
type Output = String;
|
|
|
|
type Error = PinError;
|
|
|
|
|
2024-02-19 10:32:24 +00:00
|
|
|
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<String, Box<dyn std::error::Error>>> {
|
2024-01-09 07:21:46 +00:00
|
|
|
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 {
|
2024-02-19 10:32:24 +00:00
|
|
|
return Err(Box::new(PinError::TooShort(len, min_len)));
|
2024-01-09 07:21:46 +00:00
|
|
|
}
|
|
|
|
if len > max_len {
|
2024-02-19 10:32:24 +00:00
|
|
|
return Err(Box::new(PinError::TooLong(len, max_len)));
|
2024-01-09 07:21:46 +00:00
|
|
|
}
|
|
|
|
for (index, ch) in s.chars().enumerate() {
|
|
|
|
if !range.contains(&ch) {
|
2024-02-19 10:32:24 +00:00
|
|
|
return Err(Box::new(PinError::InvalidCharacters(ch, index)));
|
2024-01-09 07:21:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(s)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2024-01-10 20:13:42 +00:00
|
|
|
|
|
|
|
#[cfg(feature = "mnemonic")]
|
|
|
|
pub mod mnemonic {
|
2024-01-16 02:44:48 +00:00
|
|
|
//! Validators for mnemonics.
|
|
|
|
|
2024-01-10 20:34:17 +00:00
|
|
|
use std::{ops::Range, str::FromStr};
|
2024-01-10 20:13:42 +00:00
|
|
|
|
|
|
|
use super::Validator;
|
|
|
|
|
2024-02-21 01:39:28 +00:00
|
|
|
use keyfork_bug::bug;
|
2024-08-01 13:50:30 +00:00
|
|
|
use keyfork_mnemonic::{Mnemonic, MnemonicFromStrError};
|
2024-01-10 20:13:42 +00:00
|
|
|
|
2024-01-16 02:44:48 +00:00
|
|
|
/// A mnemonic could not be validated from the given input.
|
2024-01-10 20:13:42 +00:00
|
|
|
#[derive(thiserror::Error, Debug)]
|
|
|
|
pub enum MnemonicValidationError {
|
2024-01-16 02:44:48 +00:00
|
|
|
/// The provided mnemonic had an unexpected word length.
|
2024-01-10 20:13:42 +00:00
|
|
|
#[error("Invalid word length: {0} does not match {1:?}")]
|
|
|
|
InvalidLength(usize, WordLength),
|
|
|
|
|
2024-01-16 02:44:48 +00:00
|
|
|
/// A mnemonic could not be parsed from the given mnemonic.
|
2024-01-10 20:13:42 +00:00
|
|
|
#[error("{0}")]
|
|
|
|
MnemonicFromStrError(#[from] MnemonicFromStrError),
|
|
|
|
}
|
|
|
|
|
2024-01-16 02:44:48 +00:00
|
|
|
/// The mnemonic had an unexpected word length.
|
2024-01-10 20:13:42 +00:00
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
pub enum WordLength {
|
2024-01-16 02:44:48 +00:00
|
|
|
/// The bounds of a mnemonic.
|
2024-01-10 20:13:42 +00:00
|
|
|
Range(Range<usize>),
|
2024-01-16 02:44:48 +00:00
|
|
|
|
|
|
|
/// The exact count of words.
|
2024-01-10 20:13:42 +00:00
|
|
|
Count(usize),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl WordLength {
|
|
|
|
fn matches(&self, word_count: usize) -> bool {
|
|
|
|
match self {
|
|
|
|
WordLength::Range(r) => r.contains(&word_count),
|
|
|
|
WordLength::Count(c) => c == &word_count,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Validate a mnemonic of a range of word lengths or a specific length.
|
|
|
|
#[derive(Default, Clone)]
|
|
|
|
pub struct MnemonicValidator {
|
2024-01-16 02:44:48 +00:00
|
|
|
/// The allowed word length of provided mnemonics.
|
2024-01-10 20:13:42 +00:00
|
|
|
pub word_length: Option<WordLength>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Validator for MnemonicValidator {
|
|
|
|
type Output = Mnemonic;
|
|
|
|
type Error = MnemonicValidationError;
|
|
|
|
|
2024-02-19 10:32:24 +00:00
|
|
|
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Mnemonic, Box<dyn std::error::Error>>> {
|
2024-01-10 20:13:42 +00:00
|
|
|
let word_length = self.word_length.clone();
|
|
|
|
Box::new(move |s: String| match word_length.as_ref() {
|
|
|
|
Some(wl) => {
|
|
|
|
let count = s.split_whitespace().count();
|
|
|
|
if !wl.matches(count) {
|
2024-02-19 10:32:24 +00:00
|
|
|
return Err(Box::new(Self::Error::InvalidLength(count, wl.clone())));
|
2024-01-10 20:13:42 +00:00
|
|
|
}
|
|
|
|
let m = Mnemonic::from_str(&s)?;
|
|
|
|
Ok(m)
|
|
|
|
}
|
|
|
|
None => {
|
|
|
|
let m = Mnemonic::from_str(&s)?;
|
|
|
|
Ok(m)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-18 02:35:01 +00:00
|
|
|
/// A mnemonic of a given choice of lengths. For example, a 128-bit or 256-bit BIP-0032 seed.
|
2024-01-18 02:21:11 +00:00
|
|
|
#[derive(thiserror::Error, Debug)]
|
|
|
|
pub enum MnemonicChoiceValidationError {
|
|
|
|
/// The provided mnemonic did not match any of the valid ranges.
|
|
|
|
#[error("Invalid word length: {0} was not in any {1:?}")]
|
|
|
|
InvalidLength(usize, Vec<WordLength>),
|
|
|
|
|
|
|
|
/// A mnemonic could not be parsed from the provided mnemonic.
|
|
|
|
#[error("{0}")]
|
|
|
|
MnemonicFromStrError(#[from] MnemonicFromStrError),
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Validate a single mnemonic against a set of possible word lengths.
|
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct MnemonicChoiceValidator<const N: usize> {
|
|
|
|
/// The accepted [`WordLength`] of the mnemonic.
|
|
|
|
pub word_lengths: [WordLength; N],
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<const N: usize> Validator for MnemonicChoiceValidator<N> {
|
|
|
|
type Output = Mnemonic;
|
|
|
|
type Error = MnemonicChoiceValidationError;
|
|
|
|
|
2024-02-19 10:32:24 +00:00
|
|
|
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Box<dyn std::error::Error>>> {
|
2024-01-18 02:21:11 +00:00
|
|
|
let word_lengths = self.word_lengths.clone();
|
|
|
|
Box::new(move |s: String| {
|
|
|
|
let count = s.split_whitespace().count();
|
|
|
|
for word_length in &word_lengths {
|
|
|
|
if word_length.matches(count) {
|
|
|
|
let m = Mnemonic::from_str(&s)?;
|
|
|
|
return Ok(m);
|
|
|
|
}
|
|
|
|
}
|
2024-02-19 10:32:24 +00:00
|
|
|
Err(Box::new(MnemonicChoiceValidationError::InvalidLength(
|
2024-01-18 02:21:11 +00:00
|
|
|
count,
|
|
|
|
word_lengths.to_vec(),
|
2024-02-19 10:32:24 +00:00
|
|
|
)))
|
2024-01-18 02:21:11 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-16 02:44:48 +00:00
|
|
|
/// A mnemonic in the set of mnemonics could not be validated from the given inputs.
|
2024-01-10 20:13:42 +00:00
|
|
|
#[derive(thiserror::Error, Debug)]
|
|
|
|
pub enum MnemonicSetValidationError {
|
2024-01-16 02:44:48 +00:00
|
|
|
/// The provided mnemonic did not have the correct amount of words.
|
2024-01-10 20:13:42 +00:00
|
|
|
#[error("Invalid word length in set {0}: {1} != expected {2}")]
|
|
|
|
InvalidSetLength(usize, usize, usize),
|
|
|
|
|
2024-01-16 02:44:48 +00:00
|
|
|
/// A mnemonic could not be parsed from the provided mnemonics.
|
2024-01-10 20:13:42 +00:00
|
|
|
#[error("Error parsing mnemonic set {0}: {1}")]
|
|
|
|
MnemonicFromStrError(usize, MnemonicFromStrError),
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Validate a set of mnemonics of a specific word length.
|
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct MnemonicSetValidator<const N: usize> {
|
2024-01-16 02:44:48 +00:00
|
|
|
/// The exact word lengths of all mnemonics. Unlike [`MnemonicValidator`], ranges of words
|
|
|
|
/// are not allowed.
|
2024-01-10 20:13:42 +00:00
|
|
|
pub word_lengths: [usize; N],
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<const N: usize> Validator for MnemonicSetValidator<N> {
|
|
|
|
type Output = [Mnemonic; N];
|
|
|
|
type Error = MnemonicSetValidationError;
|
|
|
|
|
2024-02-19 10:32:24 +00:00
|
|
|
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Box<dyn std::error::Error>>> {
|
2024-01-10 20:13:42 +00:00
|
|
|
let word_lengths = self.word_lengths;
|
|
|
|
Box::new(move |s: String| {
|
|
|
|
let mut counter: usize = 0;
|
|
|
|
let mut output = Vec::with_capacity(N);
|
|
|
|
for (word_set, word_length) in word_lengths.into_iter().enumerate() {
|
|
|
|
let words = s
|
|
|
|
.split_whitespace()
|
|
|
|
.skip(counter)
|
|
|
|
.take(word_length)
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
if words.len() != word_length {
|
2024-02-19 10:32:24 +00:00
|
|
|
return Err(Box::new(MnemonicSetValidationError::InvalidSetLength(
|
2024-01-10 20:13:42 +00:00
|
|
|
word_set,
|
|
|
|
words.len(),
|
|
|
|
word_length,
|
2024-02-19 10:32:24 +00:00
|
|
|
)));
|
2024-01-10 20:13:42 +00:00
|
|
|
}
|
|
|
|
let mnemonic = match Mnemonic::from_str(&words.join(" ")) {
|
|
|
|
Ok(m) => m,
|
2024-02-19 10:32:24 +00:00
|
|
|
Err(e) => {
|
|
|
|
return Err(Box::new(Self::Error::MnemonicFromStrError(word_set, e)))
|
|
|
|
}
|
2024-01-10 20:13:42 +00:00
|
|
|
};
|
|
|
|
output.push(mnemonic);
|
|
|
|
counter += word_length;
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(output
|
|
|
|
.try_into()
|
2024-02-21 01:39:28 +00:00
|
|
|
.expect(bug!("vec with capacity of const N was not filled")))
|
2024-01-10 20:13:42 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|