#[derive(Debug, PartialEq)]
pub enum CheckHashResult {
Valid,
PasswordTooLong,
InvalidHash(InvalidHash),
Invalid,
}
#[derive(Debug, PartialEq)]
pub enum InvalidHash {
BadLength,
UnsupportedHashType,
InvalidRounds,
InvalidBase64(base64::DecodeError),
}
#[derive(Debug)]
pub struct PhpbbHash<'a> {
hash_type: &'a str,
rounds: usize,
salt: &'a str,
hashed: &'a str,
}
static ALPHABET: &str = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
pub fn parse_hash(salted_hash: &str) -> Result<PhpbbHash, InvalidHash> {
if salted_hash.len() != 34 {
return Err(InvalidHash::BadLength);
}
let hash_type = &salted_hash[0..3];
if hash_type != "$H$" {
return Err(InvalidHash::UnsupportedHashType);
};
let rounds = match ALPHABET.find(salted_hash.chars().nth(3).unwrap()) {
None => return Err(InvalidHash::InvalidRounds),
Some(offset) if offset < 7 || offset > 30 => return Err(InvalidHash::InvalidRounds),
Some(offset) => 1 << offset,
};
let salt = &salted_hash[4..12];
let hashed = &salted_hash[12..];
Ok(PhpbbHash {
hash_type,
rounds,
salt,
hashed,
})
}
fn decode64(val: &[u8]) -> Result<Vec<u8>, base64::DecodeError> {
let len = val.len();
let bytes = base64::decode_config(
std::iter::repeat(b'.')
.take(3 - len % 3)
.chain(val.iter().cloned().rev())
.collect::<Vec<_>>(),
base64::CRYPT,
)?
.iter()
.rev()
.take(16)
.copied()
.collect::<Vec<_>>();
Ok(bytes)
}
pub fn check_hash(salted_hash: &str, password: &str) -> CheckHashResult {
if password.len() > 4096 {
return CheckHashResult::PasswordTooLong;
}
let password_bytes = password.as_bytes();
let password_bytes_len = password_bytes.len();
let parsed = match parse_hash(salted_hash) {
Ok(p) => p,
Err(e) => return CheckHashResult::InvalidHash(e),
};
let decoded_hashed = match decode64(parsed.hashed.as_bytes()) {
Ok(d) => d,
Err(e) => return CheckHashResult::InvalidHash(InvalidHash::InvalidBase64(e)),
};
let mut buf: Vec<u8> = Vec::with_capacity(8 + password_bytes_len);
buf.extend_from_slice(parsed.salt.as_bytes());
buf.extend_from_slice(password.as_bytes());
let mut hash = md5::compute(&buf);
for _ in 0..parsed.rounds {
let mut buf: Vec<u8> = Vec::with_capacity(16 + password_bytes_len);
buf.extend_from_slice(&hash.0);
buf.extend_from_slice(password_bytes);
hash = md5::compute(&buf);
}
if hash.0.as_ref() == decoded_hashed {
CheckHashResult::Valid
} else {
CheckHashResult::Invalid
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug)]
struct TestCase {
encoded_hash: &'static str,
password: &'static str,
result: CheckHashResult,
}
#[test]
fn test_validation() {
let test_cases = [
TestCase {
encoded_hash: "$H$9/O41.qQjQNlleivjbckbSNpfS4xgh0",
password: "pass1234",
result: CheckHashResult::Valid,
},
TestCase {
encoded_hash: "$H$9PoEptdBNUJZuamBBKOr/KPdi1ZmSw1",
password: "pass1234",
result: CheckHashResult::Valid,
},
TestCase {
encoded_hash: "$H$94VS2e40wcTQ38TK2P2yBc0TnmMfLC1",
password: "pass1234",
result: CheckHashResult::Valid,
},
TestCase {
encoded_hash: "$H$9/O41.qQjQNlleivjbckbSNpfS4xgh0",
password: "pass1235",
result: CheckHashResult::Invalid,
},
TestCase {
encoded_hash: "$H$9/O41.qQjQNlleivjbckbSNpfS4xgh012",
password: "pass1234",
result: CheckHashResult::InvalidHash(InvalidHash::BadLength),
},
TestCase {
encoded_hash: "$X$9/O41.qQjQNlleivjbckbSNpfS4xgh0",
password: "pass1234",
result: CheckHashResult::InvalidHash(InvalidHash::UnsupportedHashType),
},
TestCase {
encoded_hash: "$H$1/O41.qQjQNlleivjbckbSNpfS4xgh0",
password: "pass1234",
result: CheckHashResult::InvalidHash(InvalidHash::InvalidRounds),
},
];
for case in &test_cases {
let result = check_hash(case.encoded_hash, case.password);
assert_eq!(result, case.result, "{:?}", case);
}
}
}