use crate::be::dbvalue::{DbCredV1, DbPasswordV1};
use openssl::hash::MessageDigest;
use openssl::pkcs5::pbkdf2_hmac;
use rand::prelude::*;
use std::convert::TryFrom;
use uuid::Uuid;
pub mod totp;
use crate::credential::totp::TOTP;
const PBKDF2_COST: usize = 10000;
const PBKDF2_SALT_LEN: usize = 24;
const PBKDF2_KEY_LEN: usize = 64;
const PBKDF2_IMPORT_MIN_LEN: usize = 32;
#[derive(Clone, Debug)]
enum KDF {
PBKDF2(usize, Vec<u8>, Vec<u8>),
}
#[derive(Clone, Debug)]
pub struct Password {
material: KDF,
}
impl TryFrom<DbPasswordV1> for Password {
type Error = ();
fn try_from(value: DbPasswordV1) -> Result<Self, Self::Error> {
match value {
DbPasswordV1::PBKDF2(c, s, h) => Ok(Password {
material: KDF::PBKDF2(c, s, h),
}),
}
}
}
impl TryFrom<&str> for Password {
type Error = ();
#[allow(clippy::single_match)]
fn try_from(value: &str) -> Result<Self, Self::Error> {
let django_pbkdf: Vec<&str> = value.split('$').collect();
if django_pbkdf.len() == 4 {
let algo = django_pbkdf[0];
let cost = django_pbkdf[1];
let salt = django_pbkdf[2];
let hash = django_pbkdf[3];
match algo {
"pbkdf2_sha256" => {
let c = usize::from_str_radix(cost, 10).map_err(|_| ())?;
let s: Vec<_> = salt.as_bytes().to_vec();
let h = base64::decode(hash).map_err(|_| ())?;
if h.len() < PBKDF2_IMPORT_MIN_LEN {
return Err(());
}
return Ok(Password {
material: KDF::PBKDF2(c, s, h),
});
}
_ => {}
}
}
Err(())
}
}
impl Password {
fn new_pbkdf2(cleartext: &str) -> KDF {
let mut rng = rand::thread_rng();
let salt: Vec<u8> = (0..PBKDF2_SALT_LEN).map(|_| rng.gen()).collect();
let mut key: Vec<u8> = (0..PBKDF2_KEY_LEN).map(|_| 0).collect();
pbkdf2_hmac(
cleartext.as_bytes(),
salt.as_slice(),
PBKDF2_COST,
MessageDigest::sha256(),
key.as_mut_slice(),
)
.expect("PBKDF2 failure");
KDF::PBKDF2(PBKDF2_COST, salt, key)
}
pub fn new(cleartext: &str) -> Self {
Password {
material: Self::new_pbkdf2(cleartext),
}
}
pub fn verify(&self, cleartext: &str) -> bool {
match &self.material {
KDF::PBKDF2(cost, salt, key) => {
let key_len = key.len();
debug_assert!(key_len >= PBKDF2_IMPORT_MIN_LEN);
let mut chal_key: Vec<u8> = (0..key_len).map(|_| 0).collect();
pbkdf2_hmac(
cleartext.as_bytes(),
salt.as_slice(),
*cost,
MessageDigest::sha256(),
chal_key.as_mut_slice(),
)
.expect("PBKDF2 failure");
&chal_key == key
}
}
}
pub fn to_dbpasswordv1(&self) -> DbPasswordV1 {
match &self.material {
KDF::PBKDF2(cost, salt, hash) => {
DbPasswordV1::PBKDF2(*cost, salt.clone(), hash.clone())
}
}
}
}
#[derive(Clone, Debug)]
pub struct Credential {
pub(crate) password: Option<Password>,
pub(crate) totp: Option<TOTP>,
pub(crate) claims: Vec<String>,
pub(crate) uuid: Uuid,
}
impl TryFrom<DbCredV1> for Credential {
type Error = ();
fn try_from(value: DbCredV1) -> Result<Self, Self::Error> {
let DbCredV1 {
password,
totp,
claims,
uuid,
} = value;
let v_password = match password {
Some(dbp) => Some(Password::try_from(dbp)?),
None => None,
};
let v_totp = match totp {
Some(dbt) => Some(TOTP::try_from(dbt)?),
None => None,
};
Ok(Credential {
password: v_password,
totp: v_totp,
claims,
uuid,
})
}
}
impl Credential {
pub fn new_password_only(cleartext: &str) -> Self {
Credential {
password: Some(Password::new(cleartext)),
totp: None,
claims: Vec::new(),
uuid: Uuid::new_v4(),
}
}
pub fn set_password(&self, cleartext: &str) -> Self {
Credential {
password: Some(Password::new(cleartext)),
totp: self.totp.clone(),
claims: self.claims.clone(),
uuid: self.uuid,
}
}
#[cfg(test)]
pub fn verify_password(&self, cleartext: &str) -> bool {
match &self.password {
Some(pw) => pw.verify(cleartext),
None => panic!(),
}
}
pub fn to_db_valuev1(&self) -> DbCredV1 {
DbCredV1 {
password: self.password.as_ref().map(|pw| pw.to_dbpasswordv1()),
totp: self.totp.as_ref().map(|t| t.to_dbtotpv1()),
claims: self.claims.clone(),
uuid: self.uuid,
}
}
pub(crate) fn update_password(&self, pw: Password) -> Self {
Credential {
password: Some(pw),
totp: self.totp.clone(),
claims: self.claims.clone(),
uuid: self.uuid,
}
}
pub(crate) fn update_totp(&self, totp: TOTP) -> Self {
Credential {
password: self.password.clone(),
totp: Some(totp),
claims: self.claims.clone(),
uuid: self.uuid,
}
}
pub(crate) fn new_from_password(pw: Password) -> Self {
Credential {
password: Some(pw),
totp: None,
claims: Vec::new(),
uuid: Uuid::new_v4(),
}
}
}
#[cfg(test)]
mod tests {
use crate::credential::*;
use std::convert::TryFrom;
#[test]
fn test_credential_simple() {
let c = Credential::new_password_only("password");
assert!(c.verify_password("password"));
assert!(!c.verify_password("password1"));
assert!(!c.verify_password("Password1"));
assert!(!c.verify_password("It Works!"));
assert!(!c.verify_password("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
}
#[test]
fn test_password_from_invalid() {
assert!(Password::try_from("password").is_err())
}
#[test]
fn test_password_from_django_pbkdf2_sha256() {
let im_pw = "pbkdf2_sha256$36000$xIEozuZVAoYm$uW1b35DUKyhvQAf1mBqMvoBDcqSD06juzyO/nmyV0+w=";
let password = "eicieY7ahchaoCh0eeTa";
let r = Password::try_from(im_pw).expect("Failed to parse");
assert!(r.verify(password));
}
}