#[cfg(feature = "alloc")]
use crate::alloc_prelude::*;
#[cfg(feature = "std")]
use std::collections::BTreeMap;
#[cfg(feature = "std")]
use std::string::String;
#[cfg(feature = "std")]
use std::vec::Vec;
#[cfg(all(feature = "alloc", not(feature = "std")))]
use alloc::collections::BTreeMap;
#[cfg(all(feature = "alloc", not(feature = "std")))]
use alloc::string::String;
#[cfg(all(feature = "alloc", not(feature = "std")))]
use alloc::vec::Vec;
use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine as _};
use core::fmt;
use core::str::FromStr;
use dcrypt_internal::zeroing::Zeroize;
use crate::error::{Error, Result};
pub trait ParamProvider {
type Params: Clone;
fn with_params(params: Self::Params) -> Self;
fn params(&self) -> &Self::Params;
fn set_params(&mut self, params: Self::Params);
}
#[derive(Clone, PartialEq, Eq)]
pub struct PasswordHash {
pub algorithm: String,
pub params: BTreeMap<String, String>,
pub salt: Vec<u8>,
pub hash: Vec<u8>,
}
impl Zeroize for PasswordHash {
fn zeroize(&mut self) {
self.algorithm.zeroize();
for (mut key, mut value) in core::mem::take(&mut self.params) {
key.zeroize();
value.zeroize();
}
self.salt.zeroize();
self.hash.zeroize();
}
}
impl PasswordHash {
pub fn new(
algorithm: String,
params: BTreeMap<String, String>,
salt: Vec<u8>,
hash: Vec<u8>,
) -> Self {
Self {
algorithm,
params,
salt,
hash,
}
}
pub fn param(&self, key: &str) -> Option<&String> {
self.params.get(key)
}
pub fn param_as_u32(&self, key: &str) -> Result<u32> {
match self.param(key) {
Some(value) => value.parse::<u32>().map_err(|_| {
Error::param(
key.to_string(), "Invalid parameter value - not a valid u32",
)
}),
None => Err(Error::param(
key.to_string(), "Missing required parameter",
)),
}
}
}
impl fmt::Display for PasswordHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "${}", self.algorithm)?;
if !self.params.is_empty() {
write!(f, "$")?;
let mut first = true;
for (key, value) in &self.params {
if !first {
write!(f, ",")?;
}
write!(f, "{}={}", key, value)?;
first = false;
}
}
let salt_b64 = STANDARD_NO_PAD.encode(self.salt.as_slice());
let hash_b64 = STANDARD_NO_PAD.encode(self.hash.as_slice());
write!(f, "${}${}", salt_b64, hash_b64)
}
}
impl FromStr for PasswordHash {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
if !s.starts_with('$') {
return Err(Error::param(
"password_hash",
"Invalid password hash format - must start with '$'",
));
}
let parts: Vec<&str> = s.split('$').skip(1).collect();
if parts.len() < 3 {
return Err(Error::param(
"password_hash",
"Invalid password hash format - insufficient components",
));
}
let algorithm = parts[0].to_string();
let mut params = BTreeMap::new();
if parts.len() > 3 {
for param_str in parts[1].split(',') {
if param_str.is_empty() {
continue;
}
let param_parts: Vec<&str> = param_str.split('=').collect();
if param_parts.len() != 2 {
return Err(Error::param(
"param",
"Invalid parameter format - must be key=value",
));
}
params.insert(param_parts[0].to_string(), param_parts[1].to_string());
}
}
let salt_idx = if parts.len() > 3 { 2 } else { 1 };
let hash_idx = if parts.len() > 3 { 3 } else { 2 };
let salt = STANDARD_NO_PAD
.decode(parts[salt_idx])
.map_err(|_| Error::param("salt", "Invalid salt encoding - not valid base64"))?;
let hash = STANDARD_NO_PAD
.decode(parts[hash_idx])
.map_err(|_| Error::param("hash", "Invalid hash encoding - not valid base64"))?;
Ok(PasswordHash {
algorithm,
params,
salt,
hash,
})
}
}