passwordmaker_rs/
lib.rs

1#![warn(clippy::pedantic)]
2#![warn(missing_docs)]
3//#![allow(clippy::doc_markdown)]
4//! Library that should allow quick implementation of tools that are compatible with [PasswordMaker Pro](https://passwordmaker.org).
5//! 
6//! It forms the core of an upcoming PasswordMaker Pro compatible Sailfish OS App (as of yet unnamed).
7//! This library intentionally does not depend on any specific implementation of the cryptographic hashes
8//! it relies on. To see an example of how to integrate with the [Rust Crypto Hashes](https://github.com/RustCrypto/hashes),
9//! see the integration tests.
10//! 
11//! # Description
12//! There are two types in this library, you'll likely want to use: [`UrlParsing`] and [`PasswordMaker`].
13//! 
14//! [`UrlParsing`] takes a user-supplied string, and generates another string from it, according to the passed in settings.
15//! The idea is to strip unwanted parts of an URI when generating passwords. For instance, you usually want the same result
16//! for all sub-pages of a given website.
17//! 
18//! [`PasswordMaker`] is the main part of this crate. You give it settings similar to those of a PasswordMaker Pro profile,
19//! and it gives you a password that's hopfully the same you'd get from PasswordMaker Pro for the same input.
20//! 
21//! # Features
22//! The library comes with a set of precomputed powers to (slightly) speed up computation in common use cases. By default, constants
23//! for the lengths of the pre-defined character sets of PasswordMaker Pro are included (10, 16, 32, 52, 62, 94), amounting to a total
24//! of 360 bytes on a 32bit machine, and 408 bytes on a 64bit machine (and some instructions to read them). For all other character
25//! set lengths the values are computed at runtime when needed. Those values are in the (default-enabled)
26//! `precomputed_common_max_powers` feature.
27//! 
28//! If you prefer simpler code and want to save a couple of bytes in the binary, you can disable `default-features` to use runtime
29//! computation for all values, at the cost of a slight performance impact.
30//! 
31//! On the other hand, if binary size is not of concern, you might want to enable the `precomputed_max_powers` feature.
32//! This feature enables precomputed powers for all bases in the range 2..130. It therefore needs 7680 bytes on a 32bit machine, and
33//! 8704 bytes on a 64bit machine (plus some extra instructions).
34//! 
35//! # Warning
36//! This library has NOT been tested on 16bit machines. It might work, but probably does not.
37
38
39mod passwordmaker;
40mod url_parsing;
41use passwordmaker::{PasswordPartParameters, PasswordAssemblyParameters};
42use passwordmaker::leet::LeetReplacementTable;
43use std::error::Error;
44use std::fmt::Display;
45use std::marker::PhantomData;
46
47/// Trait you need to implement for the various hash functions you need to provide.
48/// Currently only a single function, that computes the hash of a string slice, is needed. This may change in a later version.
49/// 
50/// Beware: There is currently no way to put constraints on associated constants in Rust, so Block Size is not exposed.
51/// It's anyhow the same (currently hardcoded) value for all supported algorithms.
52pub trait Hasher {
53    /// The output type of the respective hash function. Typically some form of byte array.
54    type Output;
55    /// Function that takes a byte array as input, and generates the cryptographic hash of it as output.
56    fn hash(input : &[u8]) -> Self::Output;
57}
58
59/// Trait your Md4 hash function needs to implement.
60pub trait Md4 : Hasher<Output = [u8;16]> {}
61/// Trait your Md5 hash function needs to implement.
62pub trait Md5 : Hasher<Output = [u8;16]> {}
63/// Trait your Sha1 hash function needs to implement.
64pub trait Sha1 : Hasher<Output = [u8;20]> {}
65/// Trait your Sha256 hash function needs to implement.
66pub trait Sha256 : Hasher<Output = [u8;32]> {}
67/// Trait your Ripemd160 hash function needs to implement.
68pub trait Ripemd160 : Hasher<Output = [u8;20]> {}
69
70/// List of hash functions to use. Trait may change in later versions to include constructors for actual hasher objects.
71pub trait HasherList {
72    /// The type that offers MD4 hashing. See the [`Md4`] trait.
73    type MD4 : Md4;
74    /// The type that offers MD5 hashing. See the [`Md5`] trait.
75    type MD5 : Md5;
76    /// The type that offers SHA1 hashing. See the [`Sha1`] trait.
77    type SHA1 : Sha1;
78    /// The type that offers SHA256 hashing. See the [`Sha256`] trait.
79    type SHA256 : Sha256;
80    /// The type that offers Ripemd160 hashing. See the [`Ripemd160`] trait.
81    type RIPEMD160 : Ripemd160;
82}
83
84/// A cached instance of validated `PasswordMaker` settings. See [`new`][PasswordMaker::new] for details.
85pub struct PasswordMaker<'a, T : HasherList>{
86    username : &'a str,
87    modifier : &'a str,
88    password_part_parameters : PasswordPartParameters<'a>, //contains pre_leet, as this is different for different algorithms
89    post_leet : Option<LeetReplacementTable>, //same for all algorithms. applied before before password assembly.
90    assembly_settings : PasswordAssemblyParameters<'a>,
91    _hashers : PhantomData<T>,
92}
93
94impl<'a, T : HasherList> PasswordMaker<'a, T>{
95    /// Validates user input and returns a `PasswordMaker` object if the input is valid.
96    /// 
97    /// `hash_algorithm` is a PasswordMaker Pro algorithm selection.
98    /// `use_leet` details when to use leet, if at all.
99    /// `characters` is the list of output password characters. Actually this is not true. It's the list of grapheme clusters.
100    /// `username` is the "username" field of PasswordMaker Pro.
101    /// `modifier` is the "modifier" field of PasswordMaker Pro.
102    /// `password_length` is the desired password length to generate.
103    /// `prefix` is the prefix to which the password gets appended. Counts towards `password_length`.
104    /// `suffix` is the suffix appended to the password. Counts towards `password_length`.
105    /// 
106    /// # Errors
107    /// Fails if characters does not contain at least 2 grapheme clusters. Mapping to output happens by number system conversion,
108    /// and a number system base 1 or base 0 does not make any sense.
109    #[allow(clippy::too_many_arguments)]
110    pub fn new(
111        hash_algorithm : HashAlgorithm,
112        use_leet : UseLeetWhenGenerating,
113        characters : &'a str,
114        username : &'a str,
115        modifier: &'a str,
116        password_length : usize,
117        prefix : &'a str,
118        suffix : &'a str,
119    ) -> Result<Self, SettingsError> {
120        if Self::is_suitable_as_output_characters(characters) {
121            let post_leet = match &use_leet {
122                UseLeetWhenGenerating::NotAtAll
123                 | UseLeetWhenGenerating::Before { .. }
124                 => None,
125                UseLeetWhenGenerating::After { level }
126                 | UseLeetWhenGenerating::BeforeAndAfter { level }
127                 => Some(LeetReplacementTable::get(*level)),
128            };
129            Ok(PasswordMaker {
130                username,
131                modifier,
132                password_part_parameters: PasswordPartParameters::from_public_parameters(hash_algorithm, use_leet, characters),
133                post_leet,
134                assembly_settings: PasswordAssemblyParameters::from_public_parameters(prefix, suffix, password_length),
135                _hashers: PhantomData,
136            })
137        } else {
138            Err(SettingsError::InsufficientCharset)
139        }
140    }
141
142    /// Generates a password for the given `data` and `key`.
143    /// `data` is the "text-to-use", typically the output of [`UrlParsing`].
144    /// `key` is the key, also known as "master password".
145    /// 
146    ///  # Errors
147    ///  Fails if either of the parameters has zero-length.
148    pub fn generate(&self, data: String, key: String) -> Result<String, GenerationError> {
149        if data.is_empty() {
150            Err(GenerationError::MissingTextToUse)
151        } else if key.is_empty(){
152            Err(GenerationError::MissingMasterPassword)
153        } else {
154            Ok(self.generate_password_verified_input(data, key))
155        }
156    }
157}
158
159/// The leet level to use. The higher the value, the more obfuscated the results.
160#[cfg_attr(test, derive(strum_macros::EnumIter))]
161#[cfg_attr(feature = "strum", derive(strum_macros::EnumString, strum_macros::VariantNames))]
162#[derive(Debug,Clone, Copy)]
163pub enum LeetLevel {
164    /// First Leet level:\
165    /// `["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"]`
166    One,
167    /// Second Leet level:\
168    /// `["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"]`
169    Two,
170    /// Third Leet level:\
171    /// `["4", "8", "c", "d", "3", "f", "6", "h", "'", "j", "k", "1", "m", "n", "0", "p", "9", "r", "5", "7", "u", "v", "w", "x", "'/", "2"]`
172    Three,
173    /// Fourth Leet level:\
174    /// `["@", "8", "c", "d", "3", "f", "6", "h", "'", "j", "k", "1", "m", "n", "0", "p", "9", "r", "5", "7", "u", "v", "w", "x", "'/", "2"]`
175    Four,
176    /// Fifth Leet level:\
177    /// `["@", "|3", "c", "d", "3", "f", "6", "#", "!", "7", "|<", "1", "m", "n", "0", "|>", "9", "|2", "$", "7", "u", "\\/", "w", "x", "'/", "2"]`
178    Five,
179    /// Sixth Leet level:\
180    /// `["@", "|3", "c", "|)", "&", "|=", "6", "#", "!", ",|", "|<", "1", "m", "n", "0", "|>", "9", "|2", "$", "7", "u", "\\/", "w", "x", "'/", "2"]`
181    Six,
182    /// Seventh Leet level:\
183    /// `["@", "|3", "[", "|)", "&", "|=", "6", "#", "!", ",|", "|<", "1", "^^", "^/", "0", "|*", "9", "|2", "5", "7", "(_)", "\\/", "\\/\\/", "><", "'/", "2"]`
184    Seven,
185    /// Eigth Leet level:\
186    /// `["@", "8", "(", "|)", "&", "|=", "6", "|-|", "!", "_|", "|(", "1", "|\\/|", "|\\|", "()", "|>", "(,)", "|2", "$", "|", "|_|", "\\/", "\\^/", ")(", "'/", "\"/_"]`
187    Eight,
188    /// Ninth Leet level:\
189    /// `["@", "8", "(", "|)", "&", "|=", "6", "|-|", "!", "_|", "|{", "|_", "/\\/\\", "|\\|", "()", "|>", "(,)", "|2", "$", "|", "|_|", "\\/", "\\^/", ")(", "'/", "\"/_"]`
190    Nine,
191}
192
193/// The hash algorithm to use, as shown in the GUI of the JavaScript edition of PasswordMaker Pro.
194/// 
195/// # Description 
196/// 
197/// Most algorithms work by computing the hash of the input values and doing a number system base conversion to indices into
198/// the supplied character array.
199/// Notable exceptions are the HMAC algorithms, which not only compute the HMAC for the input, but also, before that, encode the
200/// input as UTF-16 and discard all upper bytes.
201/// The `Md5Version06` variant is for compatibility with ancient versions of PasswordMaker Pro. Not only does it also do the conversion
202/// to UTF-16 and the discarding of the upper bytes, in addition it disregards the user-supplied character set completely, and instead
203/// just outputs the hash encoded as hexadecimal numbers.
204/// The `HmacMd5Version06` is similarly ignoring the supplied characters and using hexadecimal numbers as output.
205#[cfg_attr(feature = "strum", derive(strum_macros::EnumString, strum_macros::VariantNames))]
206#[derive(Debug,Clone, Copy)]
207pub enum HashAlgorithm {
208    /// Regular Md4 PasswordMaker Pro setting.
209    Md4,
210    /// HAMC Md4 PasswordMaker Pro setting. Encodes input as UTF-16 and discards upper byte (just as PasswordMaker Pro does for HMAC).
211    HmacMd4,
212    /// Regular Md5 PasswordMaker Pro setting.
213    Md5,
214    /// Md5 as computed by PasswordMaker Pro version 0.6. Encodes input as UTF-16 and discards upper byte and outputs MD5 as hex number.
215    Md5Version06,
216    /// HMAC Md5 PasswordMaker Pro setting. Encodes input as UTF-16 and discards upper byte (just as PasswordMaker Pro does for HMAC).
217    HmacMd5,
218    /// HMAC Md5 as computed by PasswordMaker Pro version 0.6. Encodes input as UTF-16 and discards upper byte and outputs MD5 as hex number.
219    HmacMd5Version06,
220    /// Regular Sha1 PasswordMaker Pro setting.
221    Sha1,
222    /// HAMC Sha1 PasswordMaker Pro setting. Encodes input as UTF-16 and discards upper byte (just as PasswordMaker Pro does for HMAC).
223    HmacSha1,
224    /// Regular Sha256 PasswordMaker Pro setting.
225    Sha256,
226    /// HAMC Sha256 PasswordMaker Pro setting. Encodes input as UTF-16 and discards upper byte (just as PasswordMaker Pro does for HMAC).
227    HmacSha256,
228    /// Regular Ripemd160 PasswordMaker Pro setting.
229    Ripemd160,
230    /// HAMC Ripemd160 PasswordMaker Pro setting. Encodes input as UTF-16 and discards upper byte (just as PasswordMaker Pro does for HMAC).
231    HmacRipemd160,
232}
233
234/// When the Leet replacement as illustrated in [`LeetLevel`] is applied.
235/// 
236/// # Description
237/// If Leet is enabled, the input will be converted to lower case.
238/// It is always applied to each password part when the required password length
239/// is longer than the length obtained by computing a single hash. This is important if the input data or output charset contains certain
240/// characters where the lower case representation depends on context (e.g. 'Σ').
241#[cfg_attr(feature = "strum", derive(strum_macros::EnumDiscriminants, strum_macros::VariantNames), strum_discriminants(derive(strum_macros::EnumString)))]
242#[derive(Debug,Clone, Copy)]
243pub enum UseLeetWhenGenerating {
244    /// Do not apply Leet on input or output.
245    NotAtAll,
246    /// Apply Leet on the input before computing a password part.
247    Before {
248        /// The Leet level to apply to the input.
249        level : LeetLevel,
250    },
251    /// Apply Leet on the generated password-part. Beware that this will force the password to lower-case characters.
252    After {
253        /// The Leet level to apply to the generated password parts.
254        level : LeetLevel,
255    },
256    /// Apply Leet both, to the input for the hasher, and the generated password parts. Beware that this will force the password to lower-case characters.
257    BeforeAndAfter {
258        /// The Leet level to apply to both, input and generated password parts.
259        level : LeetLevel,
260    },
261}
262
263/// Settings for the parsing of the user's input URL.
264/// This is used to generate the `data` parameter for [`PasswordMaker`].
265#[allow(clippy::struct_excessive_bools)]
266#[derive(Debug, Clone)]
267pub struct UrlParsing {
268    use_protocol : ProtocolUsageMode,
269    use_userinfo : bool,
270    use_subdomains : bool,
271    use_domain : bool,
272    use_port_path : bool,
273}
274
275#[allow(clippy::fn_params_excessive_bools)]
276impl UrlParsing {
277    /// Creates a new `UrlParsing` instance with the given settings.
278    #[must_use]
279    pub fn new(
280        use_protocol : ProtocolUsageMode,
281        use_userinfo : bool,
282        use_subdomains : bool,
283        use_domain : bool,
284        use_port_path : bool,
285    ) -> Self{
286        UrlParsing{ use_protocol, use_userinfo, use_subdomains, use_domain, use_port_path, }
287    }
288
289    /// Parses an input string, applying the settings in `self`, and generates a string suitable for
290    /// the `data` parameter of [`PasswordMaker`]
291    #[must_use]
292    pub fn parse(&self, input : &str) -> String{
293        self.make_used_text_from_url(input)
294    }
295}
296
297/// How to handle the URL protocol, or the absence of it, during [`UrlParsing`].
298/// 
299/// # Description
300/// The "Use Protocol" checkbox in PasswordMaker Pro Javascript Edition has some weird behaviour, that's probably a bug.
301/// This enum lets you select how to hande the case that the user wants to use the Protocol, but the input string doesn't contain one.
302#[derive(Debug, Clone, Copy)]
303pub enum ProtocolUsageMode{
304    /// The protocol part of the URI is not used in the output.
305    Ignored,
306    /// The protocol part of the URI is used in the output, if it's non-empty in the input. Otherwise it isn't.
307    Used,
308    /// The protocol part of the URI is used in the output, if it's non-empty in the input. Otherwise the string "undefined" is used in the output.
309    /// This mirrors behaviour of the PasswordMaker Pro Javascript Edition.
310    UsedWithUndefinedIfEmpty,
311}
312
313
314
315/// Error returned if the supplied input did not meet expectations.
316#[derive(Debug, Clone, Copy)]
317pub enum GenerationError {
318    /// Password generation failed, because the user did not supply a master password.
319    MissingMasterPassword,
320    /// Password generation failed, because the user did not supply a text-to-use.
321    MissingTextToUse,
322}
323
324impl Display for GenerationError {
325    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
326        match self {
327            GenerationError::MissingMasterPassword => write!(f, "No master password given."),
328            GenerationError::MissingTextToUse => write!(f, "No text to use. Would just hash the master password."),
329        }
330    }
331}
332impl Error for GenerationError{}
333
334
335/// Error returned if creation of a `PasswordMaker` object failed due to invalid settings.
336/// 
337/// # Description
338/// `InsufficientCharset` means that the output character set does not contain at least two grapheme clusters.
339/// Since the output string is computed by doing a base system conversion from binary to number-of-grapheme-clusters,
340/// any number of grapheme clusters lower than 2 forms a nonsensical input. There simply is no base-1 or base-0 number system.
341#[derive(Debug, Clone, Copy)]
342pub enum SettingsError {
343    /// Password generation failed, because the character set supplied by the user did not contain at least 2 grapheme clusters.
344    InsufficientCharset,
345}
346
347impl Display for SettingsError {
348    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
349        match self {
350            SettingsError::InsufficientCharset => write!(f, "Charset needs to have at least 2 characters."),
351        }
352    }
353}
354impl Error for SettingsError{}