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
19pub fn django_auth(password: &str, encoded_password: &str) -> Result<bool> {
38 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
62pub 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}