use phf::phf_map;
use std::collections::BTreeMap;
use crate::crypto;
use crate::crypto::CryptoPolicy;
use crate::error::{Error, Result};
use crate::etree;
use crate::utils;
pub static BOTAN_PBKDF_PARAM_MAP: phf::Map<&'static str, &[&[&str; 3]; 2]> = phf_map! {
"argon2" => &[&["t", "p", "m"], &["m", "t", "p"]],
"scrypt" => &[&["r", "p", "ln"], &["ln", "r", "p"]],
"pbkdf2-sha256" => &[&["i", "", ""], &["i", "", ""]],
"pbkdf2-sha512" => &[&["i", "", ""], &["i", "", ""]],
};
pub struct PBKDFCacheEntry {
pub password: String,
pub alg: String,
pub msec: u32,
pub salt: Vec<u8>,
pub key: Vec<u8>,
pub params: BTreeMap<String, usize>,
}
pub type PBKDFCache = Vec<PBKDFCacheEntry>;
fn pbkdf_legacy(password: &str, key_len: usize, policy: &dyn CryptoPolicy) -> Result<Vec<u8>> {
policy
.check_pbkdf("sha3-512", key_len, password, &[], &BTreeMap::new())
.map_err(Error::Policy)?;
let mut result = crypto::digest("sha3-512", password.as_bytes(), policy)?;
result.truncate(key_len);
Ok(result)
}
fn pbkdf_timed(
alg: &str,
botan_param_order: &[&[&str; 3]; 2],
password: &str,
salt: &[u8],
msec: u32,
key_len: usize,
policy: &dyn CryptoPolicy,
) -> Result<(Vec<u8>, BTreeMap<String, usize>)> {
crypto::derive_key_from_password_timed(
alg,
botan_param_order,
key_len,
password,
salt,
msec,
policy,
)
}
fn pbkdf_manual(
alg: &str,
botan_param_order: &[&[&str; 3]; 2],
password: &str,
salt: &[u8],
params_map: BTreeMap<String, usize>,
key_len: usize,
policy: &dyn CryptoPolicy,
) -> Result<Vec<u8>> {
crypto::derive_key_from_password(
alg,
botan_param_order,
key_len,
password,
salt,
params_map,
policy,
)
}
pub fn format_phc(alg: &str, params: &BTreeMap<String, usize>, salt: &[u8]) -> Result<String> {
let body = params
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<String>>()
.join(",");
Ok(format!("${}${}${}", alg, body, utils::base64_encode(salt)?))
}
pub fn parse_phc(s: &str) -> Result<(String, BTreeMap<String, usize>, Vec<u8>)> {
let body = s
.strip_prefix('$')
.ok_or_else(|| Error::Phc("PHC string must start with '$'".into()))?;
let mut parts = body.splitn(3, '$');
let id = parts.next().unwrap_or("");
if id.is_empty() {
return Err(Error::Phc("PHC string is missing the algorithm id".into()));
}
let raw_params = parts.next().unwrap_or("");
let raw_salt = parts
.next()
.ok_or_else(|| Error::Phc("PHC string is missing the salt segment".into()))?;
let mut params = BTreeMap::new();
for kv in raw_params.split(',').filter(|s| !s.is_empty()) {
let (k, v) = kv
.split_once('=')
.ok_or_else(|| Error::Phc(format!("PHC param '{}' is not key=value", kv)))?;
let n: usize = v
.parse()
.map_err(|_| Error::Phc(format!("PHC param '{}' has non-numeric value", kv)))?;
params.insert(k.to_string(), n);
}
let salt = utils::base64_decode(raw_salt)?;
Ok((id.to_string(), params, salt))
}
pub fn derive_key(
password: &str,
key_len: usize,
rng: &mut Option<botan::RandomNumberGenerator>,
opts: &etree::PBKDFOptions,
cache: &mut Option<PBKDFCache>,
policy: &dyn CryptoPolicy,
) -> Result<(Vec<u8>, Option<String>)> {
if opts.alg == "legacy" {
return Ok((pbkdf_legacy(password, key_len, policy)?, None));
}
let mut salt = opts.salt.clone().unwrap_or_else(|| {
rng.as_mut()
.unwrap()
.read(opts.saltlen)
.expect("RNG failure")
});
let botan_param_order = BOTAN_PBKDF_PARAM_MAP
.get(opts.alg.as_str())
.ok_or_else(|| Error::Pbkdf(format!("Missing PBKDF param mapping for '{}'", opts.alg)))?;
if let Some(params) = opts.params.as_ref() {
let key = if let Some(entry) = cache.as_ref().unwrap_or(&Vec::new()).iter().find(|e| {
e.password == password
&& e.alg == opts.alg
&& e.key.len() == key_len
&& e.msec == 0
&& e.params == *params
}) {
entry.key.clone()
} else {
let k = pbkdf_manual(
&opts.alg,
botan_param_order,
password,
&salt,
opts.params.clone().unwrap(),
key_len,
policy,
)?;
if let Some(c) = cache.as_mut() {
c.push(PBKDFCacheEntry {
password: password.to_string(),
alg: opts.alg.clone(),
msec: 0,
salt: salt.clone(),
key: k.clone(),
params: params.clone(),
});
}
k
};
return Ok((
key,
Some(format_phc(&opts.alg, opts.params.as_ref().unwrap(), &salt)?),
));
}
let msec = opts
.msec
.ok_or_else(|| Error::Pbkdf("Missing PBKDF msec".into()))?;
let (key, params) = if let Some(entry) =
cache.as_ref().unwrap_or(&Vec::new()).iter().find(|e| {
e.password == password && e.alg == opts.alg && e.key.len() == key_len && e.msec == msec
}) {
salt = entry.salt.clone();
(entry.key.clone(), entry.params.clone())
} else {
let (k, p) = pbkdf_timed(
&opts.alg,
botan_param_order,
password,
&salt,
msec,
key_len,
policy,
)?;
if let Some(c) = cache.as_mut() {
c.push(PBKDFCacheEntry {
password: password.to_string(),
alg: opts.alg.clone(),
msec,
salt: salt.clone(),
key: k.clone(),
params: p.clone(),
});
}
(k, p)
};
Ok((key, Some(format_phc(&opts.alg, ¶ms, &salt)?)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn phc_roundtrip_argon2() {
let mut params = BTreeMap::new();
params.insert("m".to_string(), 65536);
params.insert("t".to_string(), 3);
params.insert("p".to_string(), 4);
let s = format_phc("argon2", ¶ms, b"0123456789abcdef").unwrap();
assert!(s.starts_with("$argon2$m=65536,p=4,t=3$"));
}
#[test]
fn phc_roundtrip_pbkdf2() {
let mut params = BTreeMap::new();
params.insert("i".to_string(), 100_000);
let s = format_phc("pbkdf2-sha512", ¶ms, b"0123456789abcdef").unwrap();
assert_eq!(s, "$pbkdf2-sha512$i=100000$MDEyMzQ1Njc4OWFiY2RlZg==");
}
#[test]
fn phc_parse_accepts_padded_salt() {
let (alg, params, salt) = parse_phc("$pbkdf2-sha256$i=1$AQIDBAUGBwg=").unwrap();
assert_eq!(alg, "pbkdf2-sha256");
assert_eq!(params.get("i").copied(), Some(1));
assert_eq!(salt, b"\x01\x02\x03\x04\x05\x06\x07\x08");
}
#[test]
fn phc_parse_rejects_missing_prefix() {
assert!(parse_phc("pbkdf2-sha256$i=1$AQIDBAUGBwg=").is_err());
}
#[test]
fn phc_parse_rejects_non_numeric_value() {
assert!(parse_phc("$pbkdf2-sha256$i=abc$AQIDBAUGBwg=").is_err());
}
}