use base64::prelude::*;
use pbkdf2::pbkdf2_hmac_array;
use sha2::Sha256;
type Result<T> = std::result::Result<T, Error>;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("invalid django-style encoded password: {0}")]
InvalidEncodedPassword(String),
#[error("unsupported algorithm: {0}")]
UnsupportedAlgorithm(String),
#[error("invalid salt: {0}")]
InvalidSalt(String),
}
pub fn django_auth(password: &str, encoded_password: &str) -> Result<bool> {
let parts = encoded_password.split('$');
let parts: Vec<&str> = parts.take(4).collect();
if parts.len() != 4 {
return Err(Error::InvalidEncodedPassword(
"encoded password should have 4 components separated by '$'".to_owned(),
));
}
let (algorithm, iterations, salt) = (parts[0], parts[1], parts[2]);
if algorithm != "pbkdf2_sha256" {
return Err(Error::UnsupportedAlgorithm(algorithm.to_owned()));
}
let iterations: u32 = iterations
.parse()
.expect("invalid iterations in hashed password");
let encoded = django_encode_password(password, salt, iterations)?;
Ok(encoded == encoded_password)
}
pub fn django_encode_password(password: &str, salt: &str, mut iterations: u32) -> Result<String> {
if salt.contains('$') {
return Err(Error::InvalidSalt("salt contains dollar sign ($)".into()));
}
if iterations == 0 {
iterations = 180000;
}
let hash = pbkdf2_hmac_array::<Sha256, 32>(password.as_bytes(), salt.as_bytes(), iterations);
let hash = BASE64_STANDARD.encode(hash);
let res = format!("{}${}${}${}", "pbkdf2_sha256", iterations, salt, hash);
Ok(res)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_django_auth() {
let res = django_auth(
"hello",
"pbkdf2_sha256$180000$btQDcwXF2RoK6Q$D4cC7bgbaIZGHsTdw9TYhRfuLfLGbsZlI4Rp802e7kU=",
)
.unwrap();
assert!(res);
let res = django_auth(
"hello",
"pbkdf2_sha256$180000$btQDcwXF2RoK6Q$D4cC7bgbaIZGHsTdw9TYhRfuLfLGbsZlI4Rp802e7kU",
)
.unwrap();
assert!(!res);
let res = django_auth("world", "abc$edf");
assert!(res.is_err());
}
#[test]
fn test_djaongo_encode_password() {
let password = "hello";
let encoded_password = django_encode_password(password, "btQDcwXF2RoK6Q", 0)
.expect("django_encode_password failed");
assert_eq!(
encoded_password,
"pbkdf2_sha256$180000$btQDcwXF2RoK6Q$D4cC7bgbaIZGHsTdw9TYhRfuLfLGbsZlI4Rp802e7kU="
);
let res = django_auth(password, &encoded_password).expect("auth failed");
assert!(res);
let password = "hello";
let res = django_encode_password(password, "btQDcwXF$2RoK6Q", 0);
assert!(res.is_err());
let password = "hello";
let encoded_password = django_encode_password(password, "btQDcwXF2RoK6Q", 10)
.expect("django_encode_password failed");
assert_ne!(
encoded_password,
"pbkdf2_sha256$180000$btQDcwXF2RoK6Q$D4cC7bgbaIZGHsTdw9TYhRfuLfLGbsZlI4Rp802e7kU="
);
let password = "hello";
let encoded_password = django_encode_password(password, "btQDcwXF2RoK6Qx", 0)
.expect("django_encode_password failed");
assert_ne!(
encoded_password,
"pbkdf2_sha256$180000$btQDcwXF2RoK6Q$D4cC7bgbaIZGHsTdw9TYhRfuLfLGbsZlI4Rp802e7kU="
);
}
}