1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
use indexmap::IndexSet;
use num_bigint::BigUint;
use num_traits::ToPrimitive;
use rand::Rng;

pub mod cli;
pub mod config;

/// Collection of unique chars
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Pool(IndexSet<char>);

impl Pool {
    /// Create new empty pool
    pub fn new() -> Self {
        Pool(IndexSet::new())
    }

    /// Create new pool from [`std::string::String`]
    pub fn from_string(s: String) -> Self {
        Pool(s.chars().collect::<IndexSet<char>>())
    }

    /// Return number of chars in the pool
    ///
    /// # Examples
    /// ```
    /// # use indexmap::IndexSet;
    /// let pool = "0123456789".chars().collect::<IndexSet<char>>();
    ///
    /// assert_eq!(pool.len(), 10)
    /// ```
    pub fn len(&self) -> usize {
        self.0.len()
    }

    /// Extracts all chars from the string and adds them to the pool
    pub fn extend_from_string(&mut self, s: String) -> &mut Self {
        self.0.extend(s.chars().collect::<IndexSet<char>>());

        self
    }

    /// Returns true if pool contains no elements
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    /// Get char by index
    pub fn get(&self, index: usize) -> Option<&char> {
        self.0.get_index(index)
    }

    /// Returns true if pool contains char `element`
    pub fn contains(&self, element: char) -> bool {
        self.0.contains(&element)
    }

    /// Returns true if pool contains each char from the string `elements`
    pub fn contains_all(&self, elements: String) -> bool {
        self.0
            .is_superset(&elements.chars().collect::<IndexSet<char>>())
    }
}

/// Generate random password.
///
/// # Examples
/// ```
/// # use indexmap::IndexSet;
/// use upwd::Pool;
/// let pool = "0123456789".to_owned();
/// let password = upwd::generate_password(&Pool::from_string(pool), 15);
///
/// assert_eq!(password.len(), 15);
/// ```
///
/// # Panics
/// Panics if `pool` is empty.
pub fn generate_password(pool: &Pool, length: usize) -> String {
    assert!(!pool.is_empty(), "Pool contains no elements!");

    let mut rng = rand::thread_rng();

    (0..length)
        .map(|_| {
            let idx = rng.gen_range(0, pool.len());
            *pool.get(idx).unwrap()
        })
        .collect()
}

/// Calculates entropy.
///
/// # Examples
/// ```
/// let entropy = upwd::calculate_entropy(12, 64);
///
/// assert_eq!(entropy, 72.0);
/// ```
///
/// # Panics
/// Panics if `pool_size` is zero
pub fn calculate_entropy(length: usize, pool_size: usize) -> f64 {
    assert!(pool_size > 0, "Pool size must be greater than zero!");

    BigUint::from(pool_size)
        .pow(length as u32)
        .to_f64()
        .unwrap_or(f64::MAX)
        .log2()
}

/// Calculates the minimum password length required to obtain a given entropy.
///
/// # Examples
/// ```
/// let length = upwd::calculate_length(128.0, 64.0);
///
/// assert_eq!(length.ceil(), 22.0);
/// ```
pub fn calculate_length(entropy: f64, pool_size: f64) -> f64 {
    entropy / pool_size.log2()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn generate_password_assert_len() {
        let pool = "0123456789".chars().collect::<IndexSet<char>>();
        let password = generate_password(&Pool(pool), 15);

        assert_eq!(password.len(), 15);
    }

    #[test]
    #[should_panic(expected = "Pool contains no elements!")]
    fn generate_password_passed_empty_pool() {
        let pool = "".chars().collect::<IndexSet<char>>();

        generate_password(&Pool(pool), 15);
    }

    #[test]
    fn calculate_entropy_assert_true() {
        let entropy = calculate_entropy(12, 64);

        assert_eq!(entropy, 72.0);
    }

    #[test]
    fn calculate_entropy_passed_length_is_0() {
        let entropy = calculate_entropy(0, 64);

        assert_eq!(entropy, 0.0)
    }

    #[test]
    #[should_panic(expected = "Pool size must be greater than zero!")]
    fn calculate_entropy_passed_pool_size_is_0() {
        calculate_entropy(12, 0);
    }

    #[test]
    fn calculate_entropy_passed_pool_size_is_1() {
        let entropy = calculate_entropy(12, 1);

        assert_eq!(entropy, 0.0)
    }

    #[test]
    fn calculate_length_assert_true() {
        let length = calculate_length(128.0, 64.0);

        assert_eq!(length.ceil(), 22.0);
    }

    #[test]
    fn calculate_length_entropy_is_0() {
        let length = calculate_length(0.0, 64.0);

        assert_eq!(length, 0.0);
    }

    #[test]
    fn calculate_length_pool_size_is_0() {
        let length = calculate_length(128.0, 0.0);

        assert_eq!(length, 0.0);
    }
}