aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndreas Grois <andi@grois.info>2022-10-09 00:06:35 +0200
committerAndreas Grois <andi@grois.info>2022-10-09 00:06:35 +0200
commit5e51b706d54a26470f33d1342f4666d5aab921fc (patch)
tree97307b8419b6572dcffc2842ef1474b4aa89f397
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--.gitignore3
-rw-r--r--Cargo.toml19
-rw-r--r--clippy.toml1
-rw-r--r--src/lib.rs184
-rw-r--r--src/passwordmaker/grapheme.rs11
-rw-r--r--src/passwordmaker/leet.rs100
-rw-r--r--src/passwordmaker/mod.rs413
-rw-r--r--src/passwordmaker/remainders.rs74
-rw-r--r--src/passwordmaker/remainders_impl.rs76
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 &parameters.hash_algorithm{
+ AlgoSelection::V06(V06HmacOrNot::Hmac) =>
+ Self::generate_password_part_v06_hmac(data, key, &parameters.pre_leet_level, &parameters.characters),
+ AlgoSelection::V06(V06HmacOrNot::NonHmac) =>
+ Self::generate_password_part_v06(data, key, &parameters.pre_leet_level, &parameters.characters),
+ AlgoSelection::Modern(HmacOrNot::Hmac(a)) =>
+ Self::generate_password_part_modern_hmac(data, key, a, &parameters.pre_leet_level, &parameters.characters),
+ AlgoSelection::Modern(HmacOrNot::NonHmac(a)) =>
+ Self::generate_password_part_modern(data, key, a, &parameters.pre_leet_level, &parameters.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