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
#![warn(missing_docs)] //! pwchecker_rs //! //! pwchecker_rs allows you to conveniently query the [haveibeenpwned.com](https://haveibeenpwned.com) //! api so you can check whether or not a password has been involved in a data breach. //! //! # Examples //! ``` //! # use std::error::Error; //! # //! # fn main() -> Result<(), Box<dyn Error>> { //! let res = pwchecker_rs::check_for_pwnage("helloworld")?; //! //! assert!(res.times_pwned > 0); //! # //! # Ok(()) //! # } //! ``` use std::error::Error; use crypto::digest::Digest; use crypto::sha1::Sha1; use reqwest::blocking; /// haveibeenpwned api url specifically for the type of request /// we are making, which reduces risk when sending a password /// that may not be pwned. const API_URL: &str = "https://api.pwnedpasswords.com/range/"; /// Passwd contains two fields, the password checked for pwnage and the number of times /// that password has been pwned. /// /// # Examples /// ``` /// # use std::error::Error; /// # /// # fn main() -> Result<(), Box<dyn Error>> { /// let pass = pwchecker_rs::check_for_pwnage("helloworld")?; /// assert_eq!(pass.text, "helloworld"); /// /// // The password "helloworld" has been involved in over 10,000 breaches. /// assert!(pass.times_pwned != 0); /// # /// # Ok(()) /// # } /// ``` #[derive(Debug)] pub struct Passwd { /// The password that was passed into [`check_for_pwnage`]. pub text: String, /// The number of times the password was pwned. /// /// This field will be 0 if the password hasn't been involved in a data breach /// (at least one that haveibeenpwned is aware of). pub times_pwned: i32, } /// check_for_pwnage checks the given password against the haveibeenpwned breach database. /// /// The pwned passwords api uses k-anonymity to protect the privacy of the users of the api. /// Only a 5 character prefix of the sha-1 hash of the password is sent to the api, all hashes /// in the database that begin with that prefix are sent back, and in this function are locally /// checked to see if they match the original full-length sha-1 hash. As of this writing, the /// smallest number of returned hashes is 381. More info can be found /// [here](https://www.troyhunt.com/ive-just-launched-pwned-passwords-version-2/#cloudflareprivacyandkanonymity). /// /// # Examples /// ``` /// # use std::error::Error; /// # /// # fn main() -> Result<(), Box<dyn Error>> { /// let res = pwchecker_rs::check_for_pwnage("helloworld")?; /// /// assert!(res.times_pwned > 0); /// # /// # Ok(()) /// # } /// ``` pub fn check_for_pwnage(pass: &str) -> Result<Passwd, Box<dyn Error>> { if pass.len() <= 0 { return Err("Password can't be length 0")?; } let hash = get_hash(pass); let res = blocking::get(format!("{}{}", API_URL, &hash[..5]))?.text()?; for line in res.lines() { let values = line.split(':').collect::<Vec<&str>>(); let (hash_suffix, num) = (values[0], values[1]); if format!("{}{}", &hash[..5], hash_suffix).eq(&hash) { return Ok(Passwd { text: pass.to_string(), times_pwned: num.parse()?, }); } } Ok(Passwd { text: pass.to_string(), times_pwned: 0, }) } fn get_hash(pass: &str) -> String { let mut hasher = Sha1::new(); hasher.input_str(pass); hasher.result_str().to_ascii_uppercase() } #[cfg(test)] mod tests { use super::*; #[test] #[should_panic] fn test_zero_len() { check_for_pwnage("").unwrap(); } #[test] fn check_hello_world() { assert!(check_for_pwnage("helloworld").unwrap().times_pwned > 0); } }