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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
//! `passablewords` is a password validation library which checks a password against a million of
//! the most common as well as it's ability to be cracked.
//!
//! If you're asking why use `passablewords` over `zxcvbn`, it's because `passablewords` checks
//! a password against 1,000,000 of the most common passwords. `zxcvbn` only checks 30,000.
//! `zxcvbn` is a great tool, however, and `passablewords` uses it to check the entropy of a given
//! password to make sure it's random enough on top of being unique enough. If you are ok with the
//! top 30,000 most common passwords, then you should probably use `zxcvbn`. If you want a little
//! extra, consider `passablewords`.
//!
//! While you're free to use any of the public methods, using the `check_password` function is
//! recommended since that checks for length, uniqueness, and entropy all within a single call.
//!
//! It's also important to note that this is provided as-is and doesn't prevent an attacker from
//! gaining access to, decrypting, or guessing your user's passwords. It just makes it a little
//! harder.
//!
//! # Example
//!
//! ```
//! extern crate passablewords;
//!
//! use passablewords::{check_password, PassablewordResult};
//!
//! fn main() {
//!     match check_password(password) {
//!         Ok() => println!("That password is probably pretty good!")
//!         Err(err) => match(err) {
//!             PassablewordResult::TooShort => println!("Your password should be longer than 8 characters"),
//!             PassablewordResult::TooCommon => println!("Your should be more unique"),
//!             PassablewordResult::TooSimple => println!("Your should be more random"),
//!             PassablewordResult::InternalError => println!
//!         }
//!     }
//! }
//! ```

#[macro_use]
extern crate lazy_static;
extern crate zxcvbn;

use std::fs::File;
use std::io::prelude::*;
use std::collections::HashSet;
use std::iter::FromIterator;
use zxcvbn::{zxcvbn, ZxcvbnError};

lazy_static! {
    static ref FILE_CONTENTS: String = {
        let mut f = File::open("src/common-passwords.txt")
            .expect("There was a problem opening the list of passwords");
        let mut file_contents = String::new();

        f.read_to_string(&mut file_contents)
            .expect("There was a problem reading the common passwords file");

        file_contents
    };
    static ref PASSWORDS: HashSet<&'static str> = {
        HashSet::from_iter(FILE_CONTENTS.lines())
    };
}

/// The suite of possible errors returned from passablewords. These represent the three checks made
/// for length, uniqueness, and entropy. If something goes wrong during the request, an
/// `InternalError` error is returned.
#[derive(Debug, PartialEq)]
pub enum PasswordError {
    /// The password is less than 8 characters and is therefore too short.
    TooShort,
    /// The password is within the list of 1,000,000 most common passwords and should not be used.
    TooCommon,
    /// The entropy of the password is too low, which means it could be easily guessable/crackable.
    /// A more random password should be used instead.
    TooSimple,
    /// The password isn't using ascii characters, a requirement that zxcvbn has
    NonAsciiPassword,
    /// Something went wrong during the password checks and a normal error couldn't be returned.
    InternalError,
}

/// The result type that will be returned from all public functions. It's simply a `Result` type
/// that either returns `Ok` or a `PasswordError`.
pub type PassablewordResult = Result<(), PasswordError>;

/// Check a password to make sure it's at least 8 characters long. While this shouldn't be used as
/// the only password check, it's a good baseline to start from.
///
/// # Example (using rocket.rs)
///
/// ```
/// match check_length(password) {
///     Ok() => status::Ok
///     Err(err) => match(err) {
///         PassablewordResult::TooShort => status::BadRequest("Your password should be longer than 8 characters")
///     }
/// }
/// ```
pub fn check_length(password: &str) -> PassablewordResult {
    if password.len() >= 8 {
        Ok(())
    } else {
        Err(PasswordError::TooShort)
    }
}

/// Check a password to make sure it's not within the top million most common passwords.
///
/// # Example (using rocket.rs)
///
/// ```
/// match check_uniqueness(password) {
///     Ok() => status::Ok
///     Err(err) => match(err) {
///         PassablewordResult::TooCommon => status::BadRequest("Your should be more unique")
///     }
/// }
/// ```
pub fn check_uniqueness(password: &str) -> PassablewordResult {
    if PASSWORDS.contains(password) {
        Err(PasswordError::TooCommon)
    } else {
        Ok(())
    }
}

/// Check a password to make sure random enough that it would take a lot of effort to crack/guess.
/// This uses the awesome zxcvbn library behind the scenes.
///
/// # Example (using rocket.rs)
///
/// ```
/// match check_entropy(password) {
///     Ok() => status::Ok
///     Err(err) => match(err) {
///         PassablewordResult::TooSimple => status::BadRequest("Your should be more random")
///     }
/// }
/// ```
pub fn check_entropy(password: &str) -> PassablewordResult {
    match zxcvbn(password, &[]) {
        Ok(result) => {
            if result.score >= 3 {
                Ok(())
            } else {
                Err(PasswordError::TooSimple)
            }
        }
        Err(zxcvbn_error) => {
            match zxcvbn_error {
                ZxcvbnError::NonAsciiPassword => Err(PasswordError::NonAsciiPassword),
                _ => Err(PasswordError::InternalError),
            }
        }
    }
}

/// Check a password's length, uniqueness, and entropy all in a single call. This is a convenience
/// method and simply calls `check_length`, `check_uniqueness`, and `check_entropy` with the
/// password supplied.
///
/// # Example (using rocket.rs)
///
/// ```
/// match check_password(password) {
///     Ok() => status::Ok
///     Err(err) => match(err) {
///         PassablewordResult::TooShort => status::BadRequest("Your password should be longer than 8 characters"),
///         PassablewordResult::TooCommon => status::BadRequest("Your should be more unique"),
///         PassablewordResult::TooSimple => status::BadRequest("Your should be more random"),
///         PassablewordResult::InternalError => status::InternalServerError
///     }
/// }
/// ```
pub fn check_password(password: &str) -> PassablewordResult {
    check_length(password).and(check_uniqueness(password)).and(
        check_entropy(password),
    )
}

#[cfg(test)]
mod tests {
    use super::{check_entropy, check_length, check_password, check_uniqueness, PasswordError};

    #[test]
    fn it_validates_length() {
        let too_short = check_length("short");
        let long_enough = check_length("this is a long password");

        assert_eq!(too_short, Err(PasswordError::TooShort));
        assert_eq!(long_enough, Ok(()));
    }

    #[test]
    fn it_validates_uniqueness() {
        let too_common = check_uniqueness("password");
        let unique_enough = check_uniqueness("this is a unique password");

        assert_eq!(too_common, Err(PasswordError::TooCommon));
        assert_eq!(unique_enough, Ok(()));
    }

    #[test]
    fn it_validates_entropy() {
        let too_simple = check_entropy("NotTooRandom");
        let random_enough = check_entropy("Th1s iS a Sup3rR4ndom PassW0rd!");

        assert_eq!(too_simple, Err(PasswordError::TooSimple));
        assert_eq!(random_enough, Ok(()));
    }

    #[test]
    fn it_validates_a_password() {
        let too_short = check_password("short");
        let too_common = check_password("password");
        let too_simple = check_password("NotTooRandom");
        let ok_password = check_password("Th1s iS a Sup3rR4ndom PassW0rd!");

        assert_eq!(too_short, Err(PasswordError::TooShort));
        assert_eq!(too_common, Err(PasswordError::TooCommon));
        assert_eq!(too_simple, Err(PasswordError::TooSimple));
        assert_eq!(ok_password, Ok(()));
    }
}