django_auth/
lib.rs

1use base64::prelude::*;
2use pbkdf2::pbkdf2_hmac_array;
3use sha2::Sha256;
4
5type Result<T> = std::result::Result<T, Error>;
6
7#[derive(thiserror::Error, Debug)]
8pub enum Error {
9    #[error("invalid django-style encoded password: {0}")]
10    InvalidEncodedPassword(String),
11
12    #[error("unsupported algorithm: {0}")]
13    UnsupportedAlgorithm(String),
14
15    #[error("invalid salt: {0}")]
16    InvalidSalt(String),
17}
18
19/// Verify `password` based on `encoded_password` which is managed by Django,
20/// return Ok(true) if verification is successful, otherwise return false.
21///
22/// Currently only the default pbkdf2_sha256 algorithm is supported.
23///
24/// # Usage
25///
26/// ```rust
27/// use django_auth::*;
28///
29/// let res = django_auth(
30///     "hello",
31///     "pbkdf2_sha256$180000$btQDcwXF2RoK6Q$D4cC7bgbaIZGHsTdw9TYhRfuLfLGbsZlI4Rp802e7kU=",
32/// ).expect("django_auth error");
33///
34/// assert!(res);
35/// ```
36///
37pub fn django_auth(password: &str, encoded_password: &str) -> Result<bool> {
38    // split hashed_password into 4 parts: algorithm, iterations, salt, hash
39    let parts = encoded_password.split('$');
40
41    let parts: Vec<&str> = parts.take(4).collect();
42    if parts.len() != 4 {
43        return Err(Error::InvalidEncodedPassword(
44            "encoded password should have 4 components separated by '$'".to_owned(),
45        ));
46    }
47
48    let (algorithm, iterations, salt) = (parts[0], parts[1], parts[2]);
49
50    if algorithm != "pbkdf2_sha256" {
51        return Err(Error::UnsupportedAlgorithm(algorithm.to_owned()));
52    }
53
54    let iterations: u32 = iterations
55        .parse()
56        .expect("invalid iterations in hashed password");
57
58    let encoded = django_encode_password(password, salt, iterations)?;
59    Ok(encoded == encoded_password)
60}
61
62/// Encode `password` in [Django way][1].
63///
64/// # Usage
65///
66/// ```rust
67/// use django_auth::*;
68/// let password = "hello";
69/// let encoded_password = django_encode_password(password, "btQDcwXF2RoK6Q", 0)
70///     .expect("django_encode_password error");
71
72/// assert_eq!(
73///     encoded_password,
74///     "pbkdf2_sha256$180000$btQDcwXF2RoK6Q$D4cC7bgbaIZGHsTdw9TYhRfuLfLGbsZlI4Rp802e7kU="
75/// );
76/// let res = django_auth(password, &encoded_password).expect("auth failed");
77/// assert!(res);
78/// ```
79///
80/// [1]: https://docs.djangoproject.com/en/5.0/topics/auth/passwords/
81///
82pub fn django_encode_password(password: &str, salt: &str, mut iterations: u32) -> Result<String> {
83    if salt.contains('$') {
84        return Err(Error::InvalidSalt("salt contains dollar sign ($)".into()));
85    }
86
87    if iterations == 0 {
88        iterations = 180000;
89    }
90
91    let hash = pbkdf2_hmac_array::<Sha256, 32>(password.as_bytes(), salt.as_bytes(), iterations);
92    let hash = BASE64_STANDARD.encode(hash);
93    let res = format!("{}${}${}${}", "pbkdf2_sha256", iterations, salt, hash);
94
95    Ok(res)
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn test_django_auth() {
104        let res = django_auth(
105            "hello",
106            "pbkdf2_sha256$180000$btQDcwXF2RoK6Q$D4cC7bgbaIZGHsTdw9TYhRfuLfLGbsZlI4Rp802e7kU=",
107        )
108        .unwrap();
109
110        assert!(res);
111
112        let res = django_auth(
113            "hello",
114            "pbkdf2_sha256$180000$btQDcwXF2RoK6Q$D4cC7bgbaIZGHsTdw9TYhRfuLfLGbsZlI4Rp802e7kU",
115        )
116        .unwrap();
117        assert!(!res);
118
119        let res = django_auth("world", "abc$edf");
120        assert!(res.is_err());
121    }
122
123    #[test]
124    fn test_djaongo_encode_password() {
125        let password = "hello";
126        let encoded_password = django_encode_password(password, "btQDcwXF2RoK6Q", 0)
127            .expect("django_encode_password failed");
128        assert_eq!(
129            encoded_password,
130            "pbkdf2_sha256$180000$btQDcwXF2RoK6Q$D4cC7bgbaIZGHsTdw9TYhRfuLfLGbsZlI4Rp802e7kU="
131        );
132        let res = django_auth(password, &encoded_password).expect("auth failed");
133        assert!(res);
134
135        let password = "hello";
136        let res = django_encode_password(password, "btQDcwXF$2RoK6Q", 0);
137        assert!(res.is_err());
138
139        let password = "hello";
140        let encoded_password = django_encode_password(password, "btQDcwXF2RoK6Q", 10)
141            .expect("django_encode_password failed");
142        assert_ne!(
143            encoded_password,
144            "pbkdf2_sha256$180000$btQDcwXF2RoK6Q$D4cC7bgbaIZGHsTdw9TYhRfuLfLGbsZlI4Rp802e7kU="
145        );
146
147        let password = "hello";
148        let encoded_password = django_encode_password(password, "btQDcwXF2RoK6Qx", 0)
149            .expect("django_encode_password failed");
150        assert_ne!(
151            encoded_password,
152            "pbkdf2_sha256$180000$btQDcwXF2RoK6Q$D4cC7bgbaIZGHsTdw9TYhRfuLfLGbsZlI4Rp802e7kU="
153        );
154    }
155}