#![cfg_attr(not(feature = "std"), no_std)]
#[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 core::fmt;
use core::str::FromStr;
use zeroize::{Zeroize, Zeroizing};
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);
}
pub trait StringEncodable {
fn to_string(&self) -> String;
fn from_string(s: &str) -> Result<Self>
where
Self: Sized;
}
#[derive(Clone, PartialEq, Eq)]
pub struct PasswordHash {
pub algorithm: String,
pub params: BTreeMap<String, String>,
pub salt: Zeroizing<Vec<u8>>,
pub hash: Zeroizing<Vec<u8>>,
}
impl Zeroize for PasswordHash {
fn zeroize(&mut self) {
self.algorithm.zeroize();
}
}
impl PasswordHash {
pub fn new(
algorithm: String,
params: BTreeMap<String, String>,
salt: Vec<u8>,
hash: Vec<u8>,
) -> Self {
Self {
algorithm,
params,
salt: Zeroizing::new(salt),
hash: Zeroizing::new(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 = base64_encode(&self.salt);
let hash_b64 = base64_encode(&self.hash);
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 = base64_decode(parts[salt_idx])
.map_err(|_| Error::param("salt", "Invalid salt encoding - not valid base64"))?;
let hash = base64_decode(parts[hash_idx])
.map_err(|_| Error::param("hash", "Invalid hash encoding - not valid base64"))?;
Ok(PasswordHash {
algorithm,
params,
salt: Zeroizing::new(salt),
hash: Zeroizing::new(hash),
})
}
}
fn base64_encode(data: &[u8]) -> String {
let encoded = data
.iter()
.map(|b| format!("{:02x}", b))
.collect::<String>();
encoded
}
fn base64_decode(s: &str) -> Result<Vec<u8>> {
let mut result = Vec::new();
let mut chars = s.chars().peekable();
while chars.peek().is_some() {
let high = chars.next().ok_or_else(|| {
Error::param(
"hex_string",
"Invalid hex encoding - unexpected end of string",
)
})?;
let low = chars
.next()
.ok_or_else(|| Error::param("hex_string", "Invalid hex encoding - odd length"))?;
let byte = u8::from_str_radix(&format!("{}{}", high, low), 16)
.map_err(|_| Error::param("hex_string", "Invalid hex encoding - non-hex character"))?;
result.push(byte);
}
Ok(result)
}