diff options
author | Andreas Grois <andi@grois.info> | 2022-10-09 00:06:35 +0200 |
---|---|---|
committer | Andreas Grois <andi@grois.info> | 2022-10-09 00:06:35 +0200 |
commit | 5e51b706d54a26470f33d1342f4666d5aab921fc (patch) | |
tree | 97307b8419b6572dcffc2842ef1474b4aa89f397 |
Initial Commit: PasswordMaker itself.
It's compiling, and the public interface is semi-OK now.
The internals are still a bit gory, but they'll likely see an iteartion
later on anyhow.
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | Cargo.toml | 19 | ||||
-rw-r--r-- | clippy.toml | 1 | ||||
-rw-r--r-- | src/lib.rs | 184 | ||||
-rw-r--r-- | src/passwordmaker/grapheme.rs | 11 | ||||
-rw-r--r-- | src/passwordmaker/leet.rs | 100 | ||||
-rw-r--r-- | src/passwordmaker/mod.rs | 413 | ||||
-rw-r--r-- | src/passwordmaker/remainders.rs | 74 | ||||
-rw-r--r-- | src/passwordmaker/remainders_impl.rs | 76 |
9 files changed, 881 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..317aad4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +/Cargo.lock +*~
\ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c1090ce --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "passwordmaker-rs" +version = "0.1.0" +edition = "2018" +authors = ["Andreas Grois"] +rust-version = "1.52" +description = "Rust reimplementation of the PasswordMaker Pro algorithm. This project is not directly associated with PasswordMaker Pro." +repository = "https://github.com/soulsource/passwordmaker-rs" +# License is LGPL, because original PasswordMaker Pro is LPGL too. +license = "LGPL-3.0-or-later" +keywords = ["password", "crypto", "password-generator", "security"] +categories = ["cryptography"] + +[dependencies] +unicode-segmentation = "1.10.0" + +[dev-dependencies] +strum = "0.24" +strum_macros = "0.24"
\ No newline at end of file diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..935336a --- /dev/null +++ b/clippy.toml @@ -0,0 +1 @@ +msrv = "1.52.0" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ad4ae5c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,184 @@ +mod passwordmaker; +use passwordmaker::{PasswordPartParameters, PasswordAssemblyParameters}; +use passwordmaker::leet::LeetReplacementTable; +use std::error::Error; +use std::fmt::Display; +use std::marker::PhantomData; + +/// Trait you need to implement for the various hash functions you need to provide. +/// Currently only a single function, that computes the hash of a string slice, is needed. This may change in a later version. +pub trait Hasher { + type Output; + fn hash(input : &[u8]) -> Self::Output; +} + +/// Trait your Md4 hash function needs to implement. +pub trait Md4 : Hasher<Output = [u8;16]> {} +/// Trait your Md5 hash function needs to implement. +pub trait Md5 : Hasher<Output = [u8;16]> {} +/// Trait your Sha1 hash function needs to implement. +pub trait Sha1 : Hasher<Output = [u8;20]> {} +/// Trait your Sha256 hash function needs to implement. +pub trait Sha256 : Hasher<Output = [u8;32]> {} +/// Trait your Ripemd160 hash function needs to implement. +pub trait Ripemd160 : Hasher<Output = [u8;20]> {} + +/// List of hash functions to use. Trait may change in later versions to include constructors for actual hasher objects. +pub trait HasherList { + type MD4 : Md4; + type MD5 : Md5; + type SHA1 : Sha1; + type SHA256 : Sha256; + type RIPEMD160 : Ripemd160; +} + +/// A single-use instance of PasswordMaker, created after all inputs are verified to be usable. +/// Only has one method, which is to generate the password. +pub struct PasswordMaker<'a, T : HasherList>{ + data : &'a str, //aka url aka used text + key : &'a str, //aka master password + username : &'a str, + modifier : &'a str, + password_part_parameters : PasswordPartParameters<'a>, //contains pre_leet, as this is different for different algorithms + post_leet : Option<LeetReplacementTable>, //same for all algorithms. applied before before password assembly. + assembly_settings : PasswordAssemblyParameters<'a>, + _hashers : PhantomData<T>, +} + +impl<'a, T : HasherList> PasswordMaker<'a, T>{ + /// Validates user input and returns a PasswordMaker if the input is valid. + /// `data` is the string to use, typically a URL or a part of it. + /// `key` is the master password. + /// `hash_algorithm` is a PasswordMaker Pro algorithm selection. + /// `use_leet` details when to use leet, if at all. + /// `characters` is the list of output password characters. Actually this is not true. It's the list of grapheme clusters. + /// `username` is the "username" field of PasswordMaker Pro. + /// `modifier` is the "modifier" field of PasswordMaker Pro. + /// `password_length` is the desired password length to generate. + /// `prefix` is the prefix to which the password gets appended. Counts towards `password_length`. + /// `suffix` is the suffix appended to the password. Counts towards `password_length`. + pub fn validate_input( + data : &'a str, + key: &'a str, + hash_algorithm : HashAlgorithm, + use_leet : UseLeetWhenGenerating, + characters : &'a str, + username : &'a str, + modifier: &'a str, + password_length : usize, + prefix : &'a str, + suffix : &'a str, + ) -> Result<Self, GenerationError> { + if data.len() == 0 { + Err(GenerationError::MissingTextToUse) + } else if key.len() == 0 { + Err(GenerationError::MissingMasterPassword) + } else if !Self::is_suitable_as_output_characters(characters) { + Err(GenerationError::InsufficientCharset) + } else { + let post_leet = match &use_leet { + UseLeetWhenGenerating::NotAtAll + | UseLeetWhenGenerating::Before { .. } + => None, + UseLeetWhenGenerating::After { level } + | UseLeetWhenGenerating::BeforeAndAfter { level } + => Some(LeetReplacementTable::get(level)), + }; + Ok(PasswordMaker { + data, + key, + username, + modifier, + password_part_parameters: PasswordPartParameters::from_public_parameters(hash_algorithm, &use_leet, characters), + post_leet, + assembly_settings: PasswordAssemblyParameters::from_public_parameters(prefix, suffix, password_length), + _hashers: PhantomData, + }) + } + } + + /// Consumes the PasswordMaker and returns the generated password. + pub fn generate(self) -> String { + self.generate_password_verified_input() + } +} + +/// The leet level to use. The higher the value, the more obfuscated the results. +#[cfg_attr(test, derive(strum_macros::EnumIter))] +#[derive(Debug,Clone, Copy)] +pub enum LeetLevel { + One, + Two, + Three, + Four, + Five, + Six, + Seven, + Eight, + Nine, +} + +/// The hash algorithm to use, as shown in the GUI of the JavaScript edition of PasswordMaker Pro. +/// Most algorithms work by computing the hash of the input values and doing a number system base conversion to indices into +/// the supplied character array. +/// Notable exceptions are the HMAC algorithms, which not only compute the HMAC for the input, but also, before that, encode the +/// input as UTF-16 and discard all upper bytes. +/// The `Md5Version06` variant is for compatibility with ancient versions of PasswordMaker Pro. Not only does it also do the conversion +/// to UTF-16 and the discarding of the upper bytes, in addition it disregards the user-supplied character set completely, and instead +/// just outputs the hash encoded as hexadecimal numbers. +/// The `HmacMd5Version06` is similarly ignoring the supplied characters and using hexadecimal numbers as output. +#[derive(Debug,Clone, Copy)] +pub enum HashAlgorithm { + Md4, + HmacMd4, + Md5, + Md5Version06, + HmacMd5, + HmacMd5Version06, + Sha1, + HmacSha1, + Sha256, + HmacSha256, + Ripemd160, + HmacRipemd160, +} + +/// When the leet replacement shown in leet.rs is applied. It is always applied to each password part when the required password length +/// is longer than the length obtained by computing a single hash. This is important if the input data or output charset contains certain +/// characters where the lower case representation depends on context (e.g. 'Σ'). +#[derive(Debug,Clone, Copy)] +pub enum UseLeetWhenGenerating { + NotAtAll, + Before { + level : LeetLevel, + }, + After { + level : LeetLevel, + }, + BeforeAndAfter { + level : LeetLevel, + }, +} + +/// Error returned if the supplied input did not meet expectations. +/// The two "missing" variants are self-explanatory, but the `InsufficientCharset` might need some explanation: +/// `InsufficientCharset` means that the output character set does not contain at least two grapheme clusters. +/// Since the output string is computed by doing a base system conversion from binary to number-of-grapheme-clusters, +/// any number of grapheme clusters lower than 2 forms a nonsensical input. There simply is no base-1 or base-0 number system. +#[derive(Debug, Clone, Copy)] +pub enum GenerationError { + MissingMasterPassword, + MissingTextToUse, + InsufficientCharset +} + +impl Display for GenerationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GenerationError::MissingMasterPassword => write!(f, "No master password given."), + GenerationError::MissingTextToUse => write!(f, "No text to use. Would just hash the master password."), + GenerationError::InsufficientCharset => write!(f, "Charset needs to have at least 2 characters."), + } + } +} +impl Error for GenerationError{}
\ No newline at end of file diff --git a/src/passwordmaker/grapheme.rs b/src/passwordmaker/grapheme.rs new file mode 100644 index 0000000..ee10146 --- /dev/null +++ b/src/passwordmaker/grapheme.rs @@ -0,0 +1,11 @@ +use unicode_segmentation::UnicodeSegmentation; +#[derive(Clone)] +pub(super) struct Grapheme<'a>(&'a str); + +impl<'a> Grapheme<'a> { + pub(super) fn iter_from_str(string : &'a str) -> impl Iterator<Item=Grapheme<'a>> { + string.graphemes(true).map(Self::extract_grapheme_unchecked) + } + pub(super) fn get<'b>(&'b self) -> &'a str { self.0 } + fn extract_grapheme_unchecked(s : &str) -> Grapheme { Grapheme(s) } +}
\ No newline at end of file diff --git a/src/passwordmaker/leet.rs b/src/passwordmaker/leet.rs new file mode 100644 index 0000000..858b2ad --- /dev/null +++ b/src/passwordmaker/leet.rs @@ -0,0 +1,100 @@ +use crate::LeetLevel; + +pub(crate) struct LeetReplacementTable{ + lookup_table : &'static [&'static str; 26], +} + +enum CharOrSlice{ + Char(char), + Slice(&'static str) +} + +impl LeetReplacementTable { + /// Gets the appropriate leet replacement table for a given leet level. + pub(crate) fn get(leet_level : &LeetLevel) -> LeetReplacementTable { + let lookup_table = match leet_level { + LeetLevel::One => &["4", "b", "c", "d", "3", "f", "g", "h", "i", "j", "k", "1", "m", "n", "0", "p", "9", "r", "s", "7", "u", "v", "w", "x", "y", "z"], + LeetLevel::Two => &["4", "b", "c", "d", "3", "f", "g", "h", "1", "j", "k", "1", "m", "n", "0", "p", "9", "r", "5", "7", "u", "v", "w", "x", "y", "2"], + LeetLevel::Three => &["4", "8", "c", "d", "3", "f", "6", "h", "'", "j", "k", "1", "m", "n", "0", "p", "9", "r", "5", "7", "u", "v", "w", "x", "'/", "2"], + LeetLevel::Four => &["@", "8", "c", "d", "3", "f", "6", "h", "'", "j", "k", "1", "m", "n", "0", "p", "9", "r", "5", "7", "u", "v", "w", "x", "'/", "2"], + LeetLevel::Five => &["@", "|3", "c", "d", "3", "f", "6", "#", "!", "7", "|<", "1", "m", "n", "0", "|>", "9", "|2", "$", "7", "u", "\\/", "w", "x", "'/", "2"], + LeetLevel::Six => &["@", "|3", "c", "|)", "&", "|=", "6", "#", "!", ",|", "|<", "1", "m", "n", "0", "|>", "9", "|2", "$", "7", "u", "\\/", "w", "x", "'/", "2"], + LeetLevel::Seven => &["@", "|3", "[", "|)", "&", "|=", "6", "#", "!", ",|", "|<", "1", "^^", "^/", "0", "|*", "9", "|2", "5", "7", "(_)", "\\/", "\\/\\/", "><", "'/", "2"], + LeetLevel::Eight => &["@", "8", "(", "|)", "&", "|=", "6", "|-|", "!", "_|", "|(", "1", "|\\/|", "|\\|", "()", "|>", "(,)", "|2", "$", "|", "|_|", "\\/", "\\^/", ")(", "'/", "\"/_"], + LeetLevel::Nine => &["@", "8", "(", "|)", "&", "|=", "6", "|-|", "!", "_|", "|{", "|_", "/\\/\\", "|\\|", "()", "|>", "(,)", "|2", "$", "|", "|_|", "\\/", "\\^/", ")(", "'/", "\"/_"], + }; + LeetReplacementTable { lookup_table } + } + + /// Applies this replacement table to an input string slice. + /// Needs an intermediate allocation. + pub(super) fn leetify(&self, input: &str) -> String{ + //PasswordMaker Pro is converting input to lower-case before leet is applied. + //We must apply to_lowercase on the whole input. PasswordMaker Pro is properly treating Final_Sigma, what we cannot do if we just + //iterate on a per-char basis. + input.to_lowercase().chars() + .map(|c| self.conditionally_replace(c)) + .fold(String::with_capacity(input.len()), |mut result, c| { + match c { + CharOrSlice::Char(c) => result.push(c), + CharOrSlice::Slice(s) => result.push_str(s), + }; + result + }) + } + + fn conditionally_replace(&self, character : char) -> CharOrSlice { + match (character as usize).checked_sub(0x61).and_then(|index| self.lookup_table.get(index)) { + Some(s) => CharOrSlice::Slice(s), + None => CharOrSlice::Char(character), + } + } +} + +#[cfg(test)] +mod leet_tests{ + use super::*; + use strum::IntoEnumIterator; + fn get_icelandic_test_string() -> &'static str { + "Kæmi ný Öxi hér, ykist þjófum nú bæði víl og ádrepa." //yes, I know, the upper case Ö is wrong, but it's there to test a property. + } + fn get_icelandic_test_result(level : LeetLevel) -> &'static str { + match level { + LeetLevel::One => "kæmi ný öxi hér, ykis7 þjófum nú bæði ví1 0g ádr3p4.", + LeetLevel::Two => "kæm1 ný öx1 hér, yk157 þjófum nú bæð1 ví1 0g ádr3p4.", + LeetLevel::Three => "kæm' ný öx' hér, '/k'57 þjófum nú 8æð' ví1 06 ádr3p4.", + LeetLevel::Four => "kæm' ný öx' hér, '/k'57 þjófum nú 8æð' ví1 06 ádr3p@.", + LeetLevel::Five => "|<æm! ný öx! #é|2, '/|<!$7 þ7ófum nú |3æð! \\/í1 06 ád|23|>@.", + LeetLevel::Six => "|<æm! ný öx! #é|2, '/|<!$7 þ,|ó|=um nú |3æð! \\/í1 06 á|)|2&|>@.", + LeetLevel::Seven => "|<æ^^! ^/ý ö><! #é|2, '/|<!57 þ,|ó|=(_)^^ ^/ú |3æð! \\/í1 06 á|)|2&|*@.", + LeetLevel::Eight => "|(æ|\\/|! |\\|ý ö)(! |-|é|2, '/|(!$| þ_|ó|=|_||\\/| |\\|ú 8æð! \\/í1 ()6 á|)|2&|>@.", + LeetLevel::Nine => "|{æ/\\/\\! |\\|ý ö)(! |-|é|2, '/|{!$| þ_|ó|=|_|/\\/\\ |\\|ú 8æð! \\/í|_ ()6 á|)|2&|>@.", + } + } + + /// Runs a simple icelandic test sentence as found on the web through the leetifier for all levels. + #[test] + fn leet_test_icelandic(){ + for leet_level in LeetLevel::iter(){ + let result = LeetReplacementTable::get(&leet_level).leetify(get_icelandic_test_string()); + let expected = get_icelandic_test_result(leet_level); + assert_eq!(result, expected); + } + } + + fn get_greek_test_string() -> &'static str { + "ΕΤΥΜΟΛΟΓΙΚΌ ΛΕΞΙΚΌ ΤΗΣ ΕΛΛΗΝΙΚΉΣ ΓΛΏΣΣΑΣ" + } + fn get_greek_test_result(_level : LeetLevel) -> &'static str { + "ετυμολογικό λεξικό της ελληνικής γλώσσας" + } + + #[test] + fn leet_test_greek(){ + for leet_level in LeetLevel::iter(){ + let result = LeetReplacementTable::get(&leet_level).leetify(get_greek_test_string()); + let expected = get_greek_test_result(leet_level); + assert_eq!(result, expected); + } + } +}
\ No newline at end of file diff --git a/src/passwordmaker/mod.rs b/src/passwordmaker/mod.rs new file mode 100644 index 0000000..4874758 --- /dev/null +++ b/src/passwordmaker/mod.rs @@ -0,0 +1,413 @@ +use std::convert::identity; +use std::convert::TryInto; +use unicode_segmentation::UnicodeSegmentation; +use leet::LeetReplacementTable; +use remainders::CalcRemainders; +use grapheme::Grapheme; + +use super::Hasher; + +mod remainders; +mod remainders_impl; +mod grapheme; +pub(crate) mod leet; + +impl<'y, H : super::HasherList> super::PasswordMaker<'y, H>{ + pub(super) fn is_suitable_as_output_characters(characters : &str) -> bool { + characters.graphemes(true).nth(1).is_some() + } + + pub(super) fn generate_password_verified_input(self) -> String { + let modified_data = self.data.to_owned() + self.username + self.modifier; + let key = self.key.to_owned(); + let get_modified_key = move |i : usize| { if i == 0 {key.clone()} else {key.clone() + "\n" + &i.to_string()}}; + + //In Passwordmaker Pro, leet is applied on a per-password-part basis. This means that if a password part ends in an upper-case Sigma, + //the results would differ if we moved leeting to after all password parts were joined, or worse, did it on a per-character level. + //However, this makes the code a lot more complex, as it forces us to create an owned string for each password part before combining. + //Therefore, we treat that case special. + match self.post_leet { + None => Self::generate_password_verified_no_post_leet(&modified_data, get_modified_key, &self.assembly_settings, &self.password_part_parameters), + Some(leet_level) => Self::generate_password_verified_with_post_leet(&modified_data, get_modified_key,&self.assembly_settings , &self.password_part_parameters, &leet_level), + } + } + + fn generate_password_verified_no_post_leet<G : Fn(usize)->String>(modified_data : &str, get_modified_key : G, assembly_settings : &PasswordAssemblyParameters, password_part_parameters : &PasswordPartParameters) -> String { + let password = (0..).flat_map(|i| Self::generate_password_part(modified_data, get_modified_key(i), password_part_parameters)); + combine_prefix_password_suffix(password, assembly_settings) + } + + + fn generate_password_verified_with_post_leet<G : Fn(usize)->String>(modified_data : &str, get_modified_key : G, assembly_settings : &PasswordAssemblyParameters, password_part_parameters : &PasswordPartParameters, post_leet : &LeetReplacementTable) -> String { + let suffix_length = assembly_settings.suffix_length; + let prefix_length = assembly_settings.prefix_length; + let needed_password_length = assembly_settings.password_length.saturating_sub(suffix_length).saturating_sub(prefix_length); + + //Helper function that is used in try_fold below. Appends string part p to the input string, and counts graphemes. + //Once grapheme count in total is >= needed_password_length, it returns a ControlFlow::Break. + //Or, wait. Our target platform is limited to Rust 1.52 for now, so it's a Result::Err once the required length is reached. + let append_strings_till_needed_length = |s: (String, usize),p : String| { + let new_length = s.1 + p.graphemes(true).count(); + let st = s.0 + &p; + if new_length >= needed_password_length { + Err(st) + } else { + Ok((st, new_length)) + } + }; + + //here we have to work on a string level... Because word-final sigma and leet's ToLower... + let password = (0..) + .map(|i| Self::generate_password_part(modified_data, get_modified_key(i), password_part_parameters)) + .map(|i| i.map(|g| g.get()).collect::<String>()) //make string from password part... + .map(|non_leeted_password| post_leet.leetify(&non_leeted_password)) //leet it + .try_fold((String::new(), 0), append_strings_till_needed_length).unwrap_err(); + + combine_prefix_password_suffix(Grapheme::iter_from_str(&password), assembly_settings) + } + + fn generate_password_part<'a>(data : &str, key : String, parameters : &'a PasswordPartParameters<'a>) -> GetGraphemesIterator<'a> { + //Must follow PasswordMaker Pro closely here. For instance: + // leet(key) + leet(data) != leet(key+data) + //Soo, easiest way is to just make a _different_ function for each different combination of operations. + //To make what happens explicit. + + match ¶meters.hash_algorithm{ + AlgoSelection::V06(V06HmacOrNot::Hmac) => + Self::generate_password_part_v06_hmac(data, key, ¶meters.pre_leet_level, ¶meters.characters), + AlgoSelection::V06(V06HmacOrNot::NonHmac) => + Self::generate_password_part_v06(data, key, ¶meters.pre_leet_level, ¶meters.characters), + AlgoSelection::Modern(HmacOrNot::Hmac(a)) => + Self::generate_password_part_modern_hmac(data, key, a, ¶meters.pre_leet_level, ¶meters.characters), + AlgoSelection::Modern(HmacOrNot::NonHmac(a)) => + Self::generate_password_part_modern(data, key, a, ¶meters.pre_leet_level, ¶meters.characters), + } + } + + fn generate_password_part_v06<'a>( + second_part : &str, + message : String, + pre_leet_level: &Option<LeetReplacementTable>, + characters : &'a Vec<Grapheme<'a>>, + ) -> GetGraphemesIterator<'a> { + let message = message + second_part; + let message = pre_leet_level.as_ref().map(|l| l.leetify(&message)).unwrap_or(message); + let message = yeet_upper_bytes(&message).collect::<Vec<u8>>(); + let hash = H::MD5::hash(&message); + let hash_as_integer = u128::from_be_bytes(hash); + let grapheme_indices : Vec<_> = hash_as_integer.calc_remainders(characters.len() as u128).map(|llll| llll as usize).collect(); + let grapheme_indices = yoink_additional_graphemes_for_06_if_needed(grapheme_indices); + GetGraphemesIterator { graphemes : characters, inner: grapheme_indices.into_iter().rev()} + } + + fn generate_password_part_v06_hmac<'a>( + data : &str, + key : String, + pre_leet_level: &Option<LeetReplacementTable>, + characters : &'a Vec<Grapheme<'a>>, + ) -> GetGraphemesIterator<'a> { + let key = pre_leet_level.as_ref().map(|l| l.leetify(&key)).unwrap_or(key); + let leetified_data = pre_leet_level.as_ref().map(|l| l.leetify(data)); + let data = leetified_data.as_deref().unwrap_or(data); + let key = yeet_upper_bytes(&key); + let data = yeet_upper_bytes(data); + let hash = hmac::<H::MD5,_,_>(key, data); + let hash_as_integer = u128::from_be_bytes(hash); + let grapheme_indices : Vec<_> = hash_as_integer.calc_remainders(characters.len() as u128).map(|llll| llll as usize).collect(); + let grapheme_indices = yoink_additional_graphemes_for_06_if_needed(grapheme_indices); + GetGraphemesIterator { graphemes : characters, inner: grapheme_indices.into_iter().rev()} + } + + fn generate_password_part_modern_hmac<'a>( + data : &str, + key : String, + algo : &Algorithm, + pre_leet_level: &Option<LeetReplacementTable>, + characters : &'a Vec<Grapheme<'a>>, + ) -> GetGraphemesIterator<'a> { + let key = pre_leet_level.as_ref().map(|l| l.leetify(&key)).unwrap_or(key); + let leetified_data = pre_leet_level.as_ref().map(|l| l.leetify(data)); + let data = leetified_data.as_deref().unwrap_or(data); + let to_usize = |l : u128| l as usize; + let grapheme_indices : Vec<_> = match algo { + Algorithm::Md4 => + modern_hmac_to_grapheme_indices::<H::MD4,_,_,_,_,_>(&key, data, u128::from_be_bytes, characters.len() as u128, to_usize), + Algorithm::Md5 => + modern_hmac_to_grapheme_indices::<H::MD5,_,_,_,_,_>(&key, data, u128::from_be_bytes, characters.len() as u128, to_usize), + Algorithm::Sha1 => + modern_hmac_to_grapheme_indices::<H::SHA1,_,_,_,_,_>(&key, data, ToI32Array::to_int_array, characters.len(), identity), + Algorithm::Sha256 => + modern_hmac_to_grapheme_indices::<H::SHA256,_,_,_,_,_>(&key, data, ToI32Array::to_int_array, characters.len(), identity), + Algorithm::Ripemd160 => + modern_hmac_to_grapheme_indices::<H::RIPEMD160,_,_,_,_,_>(&key, data, ToI32Array::to_int_array, characters.len(), identity), + }; + GetGraphemesIterator { graphemes : characters, inner: grapheme_indices.into_iter().rev()} + } + + fn generate_password_part_modern<'a>( + second_part : &str, + message : String, + algo : &Algorithm, + pre_leet_level: &Option<LeetReplacementTable>, + characters : &'a Vec<Grapheme<'a>>, + ) -> GetGraphemesIterator<'a> { + let message = message + second_part; + let message = pre_leet_level.as_ref().map(|l| l.leetify(&message)).unwrap_or(message); + let to_usize = |l : u128| l as usize; + let grapheme_indices : Vec<_> = match algo { + Algorithm::Md4 => + modern_message_to_grapheme_indices::<H::MD4,_,_,_,_,_>(&message,u128::from_be_bytes,characters.len() as u128, to_usize), + Algorithm::Md5 => + modern_message_to_grapheme_indices::<H::MD5,_,_,_,_,_>(&message,u128::from_be_bytes,characters.len() as u128, to_usize), + Algorithm::Sha1 => + modern_message_to_grapheme_indices::<H::SHA1,_,_,_,_,_>(&message,ToI32Array::to_int_array,characters.len(), identity), + Algorithm::Sha256 => + modern_message_to_grapheme_indices::<H::SHA256,_,_,_,_,_>(&message,ToI32Array::to_int_array,characters.len(), identity), + Algorithm::Ripemd160 => + modern_message_to_grapheme_indices::<H::RIPEMD160,_,_,_,_,_>(&message,ToI32Array::to_int_array,characters.len(), identity), + }; + GetGraphemesIterator { graphemes : characters, inner: grapheme_indices.into_iter().rev()} + } +} + +pub(super) struct PasswordAssemblyParameters<'a> { + suffix : &'a str, + prefix : &'a str, + password_length : usize, + suffix_length : usize, + prefix_length : usize, +} +impl<'a> PasswordAssemblyParameters<'a> { + pub(super) fn from_public_parameters(prefix : &'a str, suffix : &'a str, password_length : usize) -> Self{ + PasswordAssemblyParameters { + suffix, + prefix, + password_length, + suffix_length: Grapheme::iter_from_str(suffix).count(), + prefix_length: Grapheme::iter_from_str(prefix).count(), + } + } +} + +fn combine_prefix_password_suffix<'a, T : Iterator<Item=Grapheme<'a>>>(password: T, assembly_settings : &PasswordAssemblyParameters<'a>) -> String { + Grapheme::iter_from_str(assembly_settings.prefix) + .chain(password) + .take(assembly_settings.password_length.saturating_sub(assembly_settings.suffix_length)) + .chain(Grapheme::iter_from_str(assembly_settings.suffix)) + .take(assembly_settings.password_length)//cut end if suffix_length is larger than password_length... + .map(|g| g.get()) + .collect() +} + +struct GetGraphemesIterator<'a> { + graphemes : &'a Vec<Grapheme<'a>>, + inner : std::iter::Rev<std::vec::IntoIter<usize>>, + //There really should be a better solution than storing those values. If we had arbitrary-length multiplication and subtraction maybe? + //like, finding the highest potence of divisor that still is smaller than the dividend, and dividing by that one to get the left-most digit, + //dividing the remainder of this operation by the next-lower potence of divisor to get the second digit, and so on? +} + +impl<'a> Iterator for GetGraphemesIterator<'a> { + type Item = Grapheme<'a>; + + fn next(&mut self) -> Option<Self::Item> { + self.inner.next().and_then(|i| self.graphemes.get(i)).cloned() + } +} + +fn modern_hmac_to_grapheme_indices<T, F, C, Z, D, U>(key : &str, data: &str, to_dividend : F, divisor : D, to_usize : U) -> Vec<usize> + where T:Hasher, + C: CalcRemainders<Z,D>, + F: Fn(T::Output) -> C, + Z:remainders::Division<D>, + U: Fn(D) -> usize, + <T as Hasher>::Output: AsRef<[u8]> +{ + let key = yeet_upper_bytes(key); + let data = yeet_upper_bytes(data); + to_dividend(hmac::<T,_,_>(key, data)).calc_remainders(divisor).map(to_usize).collect() +} + +fn modern_message_to_grapheme_indices<T, F, C, Z, D, U>(data: &str, to_dividend : F, divisor : D, to_usize : U) -> Vec<usize> + where T:Hasher, + C: CalcRemainders<Z,D>, + F: Fn(T::Output) -> C, + Z:remainders::Division<D>, + U: Fn(D) -> usize, + <T as Hasher>::Output: AsRef<[u8]> +{ + to_dividend(T::hash(data.as_bytes())).calc_remainders(divisor).map(to_usize).collect() +} + +pub(super) struct PasswordPartParameters<'a>{ + hash_algorithm : AlgoSelection, + pre_leet_level : Option<LeetReplacementTable>, + characters : Vec<Grapheme<'a>>, +} + +impl<'a> PasswordPartParameters<'a>{ + pub(super) fn from_public_parameters(hash_algorithm : super::HashAlgorithm, leet : &super::UseLeetWhenGenerating, characters : &'a str) -> Self { + use super::UseLeetWhenGenerating; + let hash_algorithm = AlgoSelection::from_settings_algorithm(hash_algorithm); + PasswordPartParameters{ + characters: match &hash_algorithm { + AlgoSelection::V06(_) => Grapheme::iter_from_str("0123456789abcdef").collect(), + AlgoSelection::Modern(_) => Grapheme::iter_from_str(characters).collect(), + }, + pre_leet_level: match leet { + UseLeetWhenGenerating::NotAtAll + | UseLeetWhenGenerating::After{..} => None, + UseLeetWhenGenerating::Before { level } + | UseLeetWhenGenerating::BeforeAndAfter { level } => Some(LeetReplacementTable::get(level)), + }, + hash_algorithm, + } + } +} + +enum Algorithm { + Md4, + Md5, + Sha1, + Sha256, + Ripemd160, +} + +enum HmacOrNot{ + Hmac(Algorithm), + NonHmac(Algorithm), +} + +enum V06HmacOrNot{ + Hmac, + NonHmac, +} + +enum AlgoSelection{ + V06(V06HmacOrNot), + Modern(HmacOrNot), +} + +impl AlgoSelection { + fn from_settings_algorithm(settings_algorithm : super::HashAlgorithm) -> Self { + use super::HashAlgorithm; + match settings_algorithm { + HashAlgorithm::Md5Version06 => AlgoSelection::V06(V06HmacOrNot::NonHmac), + HashAlgorithm::HmacMd5Version06 => AlgoSelection::V06(V06HmacOrNot::Hmac), + HashAlgorithm::Md4 => AlgoSelection::Modern(HmacOrNot::NonHmac(Algorithm::Md4)), + HashAlgorithm::HmacMd4 => AlgoSelection::Modern(HmacOrNot::Hmac(Algorithm::Md4)), + HashAlgorithm::Md5 => AlgoSelection::Modern(HmacOrNot::NonHmac(Algorithm::Md5)), + HashAlgorithm::HmacMd5 => AlgoSelection::Modern(HmacOrNot::Hmac(Algorithm::Md5)), + HashAlgorithm::Sha1 => AlgoSelection::Modern(HmacOrNot::NonHmac(Algorithm::Sha1)), + HashAlgorithm::HmacSha1 => AlgoSelection::Modern(HmacOrNot::Hmac(Algorithm::Sha1)), + HashAlgorithm::Sha256 => AlgoSelection::Modern(HmacOrNot::NonHmac(Algorithm::Sha256)), + HashAlgorithm::HmacSha256 => AlgoSelection::Modern(HmacOrNot::Hmac(Algorithm::Sha256)), + HashAlgorithm::Ripemd160 => AlgoSelection::Modern(HmacOrNot::NonHmac(Algorithm::Ripemd160)), + HashAlgorithm::HmacRipemd160 => AlgoSelection::Modern(HmacOrNot::Hmac(Algorithm::Ripemd160)), + } + } +} + +// Rust 1.52 only has a very limited support for const generics. This means, we'll have multiple impls for array conversion. +trait ToI32Array<const I : usize> { + fn to_int_array(self) -> [u32; I]; +} + +//this could of course be done in a generic manner, but it's ugly without array_mut, which we don't have in Rust 1.52. +//Soo, pedestrian's approach :D +impl ToI32Array<5> for [u8;20] { + fn to_int_array(self) -> [u32; 5] { + [ + u32::from_be_bytes(self[0..4].try_into().unwrap()), + u32::from_be_bytes(self[4..8].try_into().unwrap()), + u32::from_be_bytes(self[8..12].try_into().unwrap()), + u32::from_be_bytes(self[12..16].try_into().unwrap()), + u32::from_be_bytes(self[16..20].try_into().unwrap()), + ] + } +} + +impl ToI32Array<8> for [u8;32] { + fn to_int_array(self) -> [u32; 8] { + [ + u32::from_be_bytes(self[0..4].try_into().unwrap()), + u32::from_be_bytes(self[4..8].try_into().unwrap()), + u32::from_be_bytes(self[8..12].try_into().unwrap()), + u32::from_be_bytes(self[12..16].try_into().unwrap()), + u32::from_be_bytes(self[16..20].try_into().unwrap()), + u32::from_be_bytes(self[20..24].try_into().unwrap()), + u32::from_be_bytes(self[24..28].try_into().unwrap()), + u32::from_be_bytes(self[28..32].try_into().unwrap()), + ] + } +} + +// Yeets the upper bytes of each UTF-16 char representation. Needed, because PasswordMaker Pro does that for some algorithms (HMAC, MD5 0.6) +// Returns bytes, because there's no way that this transform doesn't break the string. +#[allow(clippy::cast_possible_truncation)] //clippy, stop complaining. Truncating is the very purpose of this function... +fn yeet_upper_bytes(input : &str) -> impl Iterator<Item=u8> + Clone + '_ { + input.encode_utf16().map(|wide_char| wide_char as u8) +} + +// 0.6 Md5 might need to yoink an additional 0 for output graphemes. +fn yoink_additional_graphemes_for_06_if_needed(mut input : Vec<usize>) -> Vec<usize> { + input.resize(32, 0); + input +} + +fn hmac<T, K, M>(key : K, data : M) -> T::Output + where T : Hasher, + T::Output : AsRef<[u8]>, + K : Iterator<Item=u8> + Clone, + M : Iterator<Item=u8>, +{ + let key_len = key.clone().count(); + let key = if key_len > 64 { + KeyOrHash::from_hash(T::hash(&key.collect::<Vec<_>>())) + } else { + KeyOrHash::from_key(key) + }; + let key = key.chain(std::iter::repeat(0)); //if key[i] does not exist, use 0 instead. + + let mut inner_pad = [0u8;64]; + let mut outer_pad = [0u8;64]; + + let pads = inner_pad.iter_mut().zip(outer_pad.iter_mut()); + for ((i,o),k) in pads.zip(key) { + *i = k ^ 0x36; + *o = k ^ 0x5C; + } + + let hash = T::hash(&inner_pad.iter().copied().chain(data).collect::<Vec<_>>()); + T::hash(&outer_pad.iter().chain(hash.as_ref().iter()).copied().collect::<Vec<_>>()) +} + +enum KeyOrHash<K: Iterator<Item=u8>, H: AsRef<[u8]>> { + Key(K), + Hash{ + hash : H, + idx : usize + } +} + +impl<K: Iterator<Item=u8>, H: AsRef<[u8]>> KeyOrHash<K, H>{ + fn from_key(key : K) -> Self { + Self::Key(key) + } + fn from_hash(hash : H) -> Self { + Self::Hash { hash, idx: 0 } + } +} + +impl<K: Iterator<Item=u8>, H: AsRef<[u8]>> Iterator for KeyOrHash<K, H>{ + type Item = u8; + fn next(&mut self) -> Option<Self::Item> { + match self { + KeyOrHash::Key(k) => k.next(), + KeyOrHash::Hash { hash: owned, idx } => { + *idx += 1; + owned.as_ref().get(*idx-1).copied() + }, + } + } +}
\ No newline at end of file diff --git a/src/passwordmaker/remainders.rs b/src/passwordmaker/remainders.rs new file mode 100644 index 0000000..93570a1 --- /dev/null +++ b/src/passwordmaker/remainders.rs @@ -0,0 +1,74 @@ +/// Adds `calc_remainders(divisor)` method to types that have some implementation of the Division trait. +pub(super) trait CalcRemainders<T, D>{ + fn calc_remainders(self, divisor : D) -> Remainders<T,D>; +} + +/// Implement `Division` to enable the `calc_remainders()` method for your type. +pub(super) trait Division<D> where Self:Sized { + /// does in-place arbitrary-length division. Returns remainder. + fn divide(self, divisor : &D) -> DivisionResult<Self, D>; + fn is_zero(&self) -> bool; +} + +/// Or mark your type as `UseGenericDivision` to just use `/` and `%` operators for types. Makes only sense for integers. +pub(super) trait UseGenericDivision : Clone + + for <'a> std::ops::Div<&'a Self, Output = Self> + + for <'a> std::ops::Rem<&'a Self, Output = Self> + + Default + + Eq {} + +impl<T, D> CalcRemainders<T, D> for T + where T:Division<D> +{ + fn calc_remainders(self, divisor : D) -> Remainders<T, D> { + Remainders::new(self,divisor) + } +} + +pub(super) struct Remainders<T, U>{ + value : Option<T>, + divisor : U, +} + +impl<U, T:Division<U>> Remainders<T, U> { + fn new(value : T, divisor : U) -> Self { + let value = if value.is_zero() { None } else { Some(value) }; + Remainders { + value, + divisor, + } + } +} + +impl<U, T:Division<U>> Iterator for Remainders<T,U>{ + type Item=U; + + fn next(&mut self) -> Option<Self::Item> { + if let Some(v) = self.value.take() { + let DivisionResult{result, remainder} = v.divide(&self.divisor); + self.value = if result.is_zero() { None } else { Some(result) }; + Some(remainder) + } else { + None + } + } +} + +pub(super) struct DivisionResult<T:Division<U>, U> { + pub result : T, + pub remainder : U, +} + +impl<U> Division<U> for U + where U: UseGenericDivision +{ + fn divide(self, divisor : &Self) -> DivisionResult<Self, Self> { + DivisionResult { + result: self.clone().div(divisor), + remainder: self.rem(divisor) + } + } + fn is_zero(&self) -> bool { + *self == Self::default() + } +}
\ No newline at end of file diff --git a/src/passwordmaker/remainders_impl.rs b/src/passwordmaker/remainders_impl.rs new file mode 100644 index 0000000..7de2189 --- /dev/null +++ b/src/passwordmaker/remainders_impl.rs @@ -0,0 +1,76 @@ +use super::remainders::{Division, UseGenericDivision, DivisionResult}; + +impl UseGenericDivision for u128{} //for Md4, Md5 + +impl<const N : usize> Division<usize> for [u32;N] { + #[allow(clippy::cast_possible_truncation)] + fn divide(mut self, divisor : &usize) -> DivisionResult<Self, usize> { + #[cfg(target_pointer_width = "64")] + type UsizeAndFour = u128; + #[cfg(not(target_pointer_width = "64"))] + type UsizeAndFour = u64; + assert!((UsizeAndFour::MAX >> 32) as u128 >= usize::MAX as u128); + + //uses mutation, because why not? self is owned after all :D + let divisor : UsizeAndFour = *divisor as UsizeAndFour; + let remainder = self.iter_mut().fold(0 as UsizeAndFour,|carry, current| { + assert_eq!(carry, carry & (usize::MAX as UsizeAndFour)); //carry has to be lower than divisor, and divisor is usize. + let carry_shifted = carry << 32; + let dividend = (carry_shifted) + (*current as UsizeAndFour); + let ratio = dividend / divisor; + assert_eq!(ratio, ratio & 0xffff_ffff); //this is fine. The first digit after re-adding the carry is alwys zero. + *current = (ratio) as u32; + dividend - (*current as UsizeAndFour) * divisor + }); + assert_eq!(remainder, remainder & (usize::MAX as UsizeAndFour)); + let remainder = remainder as usize; + DivisionResult{ + result: self, + remainder, + } + } + + fn is_zero(&self) -> bool { + self.iter().all(|x| *x == 0) + } +} + +#[cfg(test)] +mod remainders_tests{ + use super::super::remainders::CalcRemainders; + + use super::*; + #[test] + fn test_generic_division(){ + let v = 50u128; + let d = 7u128; + let DivisionResult{result, remainder}=v.divide(&d); + assert_eq!(7, result); + assert_eq!(1, remainder); + } + + #[test] + fn test_remainders() { + //relies on generic division. + let v = 141u128; + let d = 3u128; + let results : Vec<u128> = v.calc_remainders(d).collect(); + assert_eq!(results, vec![0u128,2u128,0u128,2u128,1u128]) + } + + #[test] + fn test_array_divide() { + let dividend_int = 0xe7f1ec3a5f35af805407a8a531eefb79u128; + let dividend = [(dividend_int >> 96) as u32, ((dividend_int >> 64) & 0xffffffff) as u32, ((dividend_int >> 32) & 0xffffffff) as u32, (dividend_int & 0xffffffff) as u32]; + #[cfg(target_pointer_width = "64")] + let divisor = 1531534813576487; + #[cfg(not(target_pointer_width = "64"))] + let divisor = 1531534813; + let result_int = dividend_int / (divisor as u128); + let remainder_int = dividend_int % (divisor as u128); + let result = dividend.divide(&divisor); + assert_eq!(result.result, [(result_int >> 96) as u32, ((result_int >> 64) & 0xffffffff) as u32, ((result_int >> 32) & 0xffffffff) as u32, (result_int & 0xffffffff) as u32]); + assert_eq!(remainder_int, result.remainder as u128); + } + +}
\ No newline at end of file |