Skip to main content

revolt_database/util/
password.rs

1use reqwest::Client;
2use sha1::Digest;
3use std::{collections::HashSet, sync::LazyLock};
4
5use revolt_config::config;
6use revolt_result::{Result, ToRevoltError};
7
8static CLIENT: LazyLock<Client> = LazyLock::new(Client::new);
9static ARGON_CONFIG: LazyLock<argon2::Config<'static>> = LazyLock::new(argon2::Config::default);
10static TOP_100K_COMPROMISED: LazyLock<HashSet<String>> = LazyLock::new(|| {
11    include_str!("../../assets/pwned100k.txt")
12        .split('\n')
13        .map(|x| x.into())
14        .collect()
15});
16
17#[derive(Deserialize)]
18struct EasyPwnedResult {
19    secure: bool,
20}
21
22/// Hash a password using argon2
23pub fn hash_password(plaintext_password: String) -> Result<String> {
24    argon2::hash_encoded(
25        plaintext_password.as_bytes(),
26        nanoid::nanoid!(24).as_bytes(),
27        &ARGON_CONFIG,
28    )
29    .to_internal_error()
30}
31
32pub async fn assert_safe(password: &str) -> Result<()> {
33    // Make sure the password is long enough.
34    if password.len() < 8 {
35        return Err(create_error!(ShortPassword));
36    }
37
38    let config = config().await;
39
40    if !config.api.security.easypwned.is_empty() {
41        let mut hasher = sha1::Sha1::new();
42        hasher.update(password);
43        let pwd_hash = hasher.finalize();
44
45        let result = match CLIENT
46            .get(format!(
47                "{}/hash/{pwd_hash:#02x}",
48                &config.api.security.easypwned
49            ))
50            .send()
51            .await
52        {
53            Ok(response) => match response.json::<EasyPwnedResult>().await {
54                Ok(result) => Ok(result.secure),
55                Err(e) => Err(e),
56            },
57            Err(e) => Err(e),
58        };
59
60        if let Err(e) = &result {
61            revolt_config::capture_error(e);
62        } else if result.is_ok_and(|b| b) {
63            return Ok(());
64        }
65    };
66
67    if TOP_100K_COMPROMISED.contains(password) {
68        return Err(create_error!(CompromisedPassword));
69    };
70
71    Ok(())
72}