use base64::Engine;
use base64::engine::general_purpose::STANDARD_NO_PAD;
use nom::bytes::complete::{tag, take_while, take_while1};
use nom::combinator::{map_res, opt};
use nom::multi::fold_many0;
use nom::sequence::{preceded, separated_pair, terminated};
use nom::{IResult, Parser};
use std::collections::HashMap;
fn from_b64(data: &str) -> Result<Option<Vec<u8>>, ()> {
Ok(match data.len() {
0 => None,
_ => STANDARD_NO_PAD.decode(data.as_bytes()).ok(),
})
}
fn to_b64(data: &[u8]) -> String {
STANDARD_NO_PAD.encode(data)
}
fn is_b64(chr: char) -> bool {
chr.is_ascii_alphanumeric() || chr == '+' || chr == '/'
}
fn is_id_char(chr: char) -> bool {
(chr.is_ascii_alphabetic() && chr.is_ascii_lowercase()) || chr.is_ascii_digit() || chr == '-'
}
fn is_param_name_char(chr: char) -> bool {
(chr.is_ascii_alphabetic() && chr.is_ascii_lowercase()) || chr.is_ascii_digit() || chr == '-'
}
fn is_param_value_char(chr: char) -> bool {
chr.is_ascii_alphanumeric() || chr == '+' || chr == '-' || chr == '.' || chr == '/'
}
fn get_id(input: &str) -> IResult<&str, &str> {
preceded(tag("$"), take_while1(is_id_char)).parse(input)
}
fn get_phc_part(input: &str) -> IResult<&str, Option<Vec<u8>>> {
if input.is_empty() {
return Ok((input, None));
}
map_res(preceded(tag("$"), take_while(is_b64)), from_b64).parse(input)
}
fn get_phc_part_if(input: &str, cond: bool) -> IResult<&str, Option<Vec<u8>>> {
if cond {
get_phc_part(input)
} else {
Ok((input, None))
}
}
fn get_param_elem(input: &str) -> IResult<&str, (&str, &str)> {
terminated(
separated_pair(
take_while1(is_param_name_char),
tag("="),
take_while1(is_param_value_char),
),
opt(tag(",")),
)
.parse(input)
}
fn get_params(input: &str) -> IResult<&str, HashMap<String, String>> {
fold_many0(
get_param_elem,
HashMap::new,
|mut hm: HashMap<_, _>, (k, v)| {
hm.insert(k.to_string(), v.to_string());
hm
},
)
.parse(input)
}
fn parse_params(input: &str) -> IResult<&str, HashMap<String, String>> {
preceded(tag("$"), get_params).parse(input)
}
fn get_phc(input: &str) -> IResult<&str, PHCData> {
let (input, id) = get_id(input)?;
let (input, parameters) = opt(parse_params).parse(input)?;
let (input, salt) = get_phc_part_if(input, parameters.is_some())?;
let (input, hash) = get_phc_part_if(input, salt.is_some())?;
let parameters = parameters.unwrap_or_default();
let data = PHCData {
id: id.to_string(),
parameters,
salt,
hash,
};
Ok((input, data))
}
pub struct PHCData {
pub id: String,
pub parameters: HashMap<String, String>,
pub salt: Option<Vec<u8>>,
pub hash: Option<Vec<u8>>,
}
impl PHCData {
pub fn from_str(s: &str) -> Result<PHCData, ()> {
match get_phc(s) {
Ok((r, v)) => match r.len() {
0 => Ok(v),
_ => Err(()),
},
Err(_) => Err(()),
}
}
pub fn to_string(&self) -> Result<String, ()> {
if self.id.is_empty() {
return Err(());
}
let mut res = String::from("$");
res += self.id.as_str();
if self.parameters.is_empty() && self.salt.is_none() {
return Ok(res);
}
res += "$";
for (i, (k, v)) in self.parameters.iter().enumerate() {
res += &match i {
0 => format!("{}={}", k, v),
_ => format!(",{}={}", k, v),
};
}
match self.salt {
Some(ref s) => {
res += "$";
res += to_b64(s).as_str();
match self.hash {
Some(ref h) => {
res += "$";
res += to_b64(h).as_str();
Ok(res)
}
None => Ok(res),
}
}
None => Ok(res),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_to_string_same() {
let data = [
"$test",
"$test$i=42",
"$test$$YXN1cmUu",
"$test$i=42$YXN1cmUu",
"$test$i=42$YXN1cmUu$YW55IGNhcm5hbCBwbGVhc3Vy",
"$test$$YXN1cmUu$YW55IGNhcm5hbCBwbGVhc3Vy",
"$pbkdf2$i=1000$RSF4Aw$xvdfA4H7QJQ1w/4jGcjBEIjCvsc",
"$pbkdf2-sha256$t-y=./42+a-1$RSF4Aw$xvdfA4H7QJQ1w/4jGcjBEIjCvsc",
"$pbkdf2$$RSF4Aw",
"$pbkdf2$i=21000$RSF4Aw$LwCbGeQoBZIraYoDZ8Oe/PxdJHc",
];
for ref_str in data.iter() {
let phc = PHCData::from_str(ref_str);
assert!(phc.is_ok());
let phc_str = phc.unwrap().to_string();
assert!(phc_str.is_ok());
assert_eq!(phc_str.unwrap(), ref_str.to_string());
}
}
#[test]
fn test_to_string_diff() {
let data = [
("$test$", "$test"),
("$test$$", "$test"),
("$test$$YXN1cmUu$", "$test$$YXN1cmUu"),
("$test$i=42$YXN1cmUu$", "$test$i=42$YXN1cmUu"),
];
for &(str_extra, ref_str) in data.iter() {
let phc = PHCData::from_str(str_extra);
assert!(phc.is_ok());
let phc_str = phc.unwrap().to_string();
assert!(phc_str.is_ok());
assert_eq!(phc_str.unwrap(), ref_str.to_string());
}
}
#[test]
fn test_valid_data_id() {
let phc = PHCData::from_str("$dummy");
assert!(phc.is_ok());
let phc = phc.unwrap();
assert_eq!(phc.id, "dummy".to_string());
assert!(phc.parameters.is_empty());
assert_eq!(phc.salt, None);
assert_eq!(phc.hash, None);
}
#[test]
fn test_valid_data_params() {
let phc = PHCData::from_str("$dummy$i=42");
assert!(phc.is_ok());
let phc = phc.unwrap();
assert_eq!(phc.id, "dummy".to_string());
assert_eq!(phc.parameters.len(), 1);
match phc.parameters.get("i") {
Some(v) => assert_eq!(v, "42"),
None => assert!(false),
}
assert_eq!(phc.salt, None);
assert_eq!(phc.hash, None);
}
#[test]
fn test_valid_data_salt() {
let phc = PHCData::from_str("$dummy$i=42$YXN1cmUu");
assert!(phc.is_ok());
let phc = phc.unwrap();
assert_eq!(phc.id, "dummy".to_string());
assert_eq!(phc.parameters.len(), 1);
match phc.parameters.get("i") {
Some(v) => assert_eq!(v, "42"),
None => assert!(false),
}
match phc.salt {
Some(p) => assert_eq!(p, vec![0x61, 0x73, 0x75, 0x72, 0x65, 0x2e]),
None => assert!(false),
};
assert_eq!(phc.hash, None);
}
#[test]
fn test_valid_data_full() {
let phc = PHCData::from_str("$dummy$i=42$YXN1cmUu$YW55IGNhcm5hbCBwbGVhc3Vy");
assert!(phc.is_ok());
let phc = phc.unwrap();
assert_eq!(phc.id, "dummy".to_string());
assert_eq!(phc.parameters.len(), 1);
match phc.parameters.get("i") {
Some(v) => assert_eq!(v, "42"),
None => assert!(false),
}
match phc.salt {
Some(p) => assert_eq!(p, vec![0x61, 0x73, 0x75, 0x72, 0x65, 0x2e]),
None => assert!(false),
};
match phc.hash {
Some(p) => assert_eq!(
p,
vec![
0x61, 0x6e, 0x79, 0x20, 0x63, 0x61, 0x72, 0x6e, 0x61, 0x6c, 0x20, 0x70, 0x6c,
0x65, 0x61, 0x73, 0x75, 0x72,
]
),
None => assert!(false),
};
}
#[test]
fn test_multiple_params() {
let phc = PHCData::from_str("$dummy$i=42,plop=asdfg,21=abcd12efg$YXN1cmUu");
assert!(phc.is_ok());
let phc = phc.unwrap();
assert_eq!(phc.parameters.len(), 3);
match phc.parameters.get("i") {
Some(v) => assert_eq!(v, "42"),
None => assert!(false),
}
match phc.parameters.get("plop") {
Some(v) => assert_eq!(v, "asdfg"),
None => assert!(false),
}
match phc.parameters.get("21") {
Some(v) => assert_eq!(v, "abcd12efg"),
None => assert!(false),
}
}
#[test]
fn test_invalid_data() {
let data = [
"", "$", "$@zerty", "$test$YXN1cmUu", "$test$=42", "$test$i@=42", "$test$i=?", "$test$i", "$test$i=", "$test$i=$YXN1cmUu", "$test$i=42$YXN1cmUr%w", "$test$i=42$YXN1cmUr%w$YW55IGNhcm5hbCBwbGVhc3Vy", "$test$i=42$YXN1cmUu$YW55IGNhcm5hbCBwbGVhc3V=", "$test$i=42$$YW55IGNhcm5hbCBwbGVhc3Vy", ];
for s in data.iter() {
let phc = PHCData::from_str(s);
assert!(phc.is_err());
}
}
}