revolt_database/util/
password.rs1use 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
22pub 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 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}