use super::funejson::Value; use super::crypto_constant_time_equal::constant_time_equal; use super::crypto_pbkdf2_sha256::pbkdf2_sha256; use super::encoding_base64_base64_decode::base64_decode; const MALFORMED: &str = "stored password hash is malformed"; fn sextet(b: u8) -> i64 { match b { b'A'..=b'Z' => (b - b'A') as i64, b'a'..=b'z' => (b - b'a' + 26) as i64, b'0'..=b'9' => (b - b'0' + 52) as i64, b'+' => 62, b'/' => 63, _ => -1, } } /// Would base64_decode accept this, and give at least one byte? Checked /// first so a bad field is reported as a malformed hash, with the words the /// other languages use, rather than as base64's own panic. fn is_base64(text: &str) -> bool { let b = text.as_bytes(); if b.is_empty() || b.len() % 4 != 0 { return false; } let padding = b.iter().rev().take_while(|&&c| c == b'=').count(); if padding > 2 || b.len() - padding == 0 { return false; } let body = &b[..b.len() - padding]; if body.iter().any(|&c| sextet(c) < 0) { return false; } let last = sextet(body[body.len() - 1]); match padding { 1 => last & 3 == 0, 2 => last & 15 == 0, _ => true, } } /// (iterations, salt, hash) of a stored pbkdf2_sha256 string. A string this /// code did not write is a data problem to surface, not a wrong password. /// /// # Panics /// Panics when the string is not in that form. pub fn parse_stored_hash(stored: &str) -> (i64, Vec, Vec) { let parts: Vec<&str> = stored.split('$').collect(); if parts[0] != "pbkdf2_sha256" { panic!("stored password hash is not a pbkdf2_sha256 hash"); } if parts.len() != 4 { panic!("{}", MALFORMED); } let count = parts[1].as_bytes(); if count.is_empty() || count.len() > 10 || count[0] == b'0' || !count.iter().all(|c| c.is_ascii_digit()) { panic!("{}", MALFORMED); } let iterations = count.iter().fold(0i64, |n, c| n * 10 + i64::from(c - b'0')); if !is_base64(parts[2]) || !is_base64(parts[3]) { panic!("{}", MALFORMED); } (iterations, base64_decode(parts[2]), base64_decode(parts[3])) } /// Does the password match the stored hash? Re-derived with the stored salt /// and iteration count, compared in constant time. /// /// # Panics /// Panics when the stored string is not one hash_password makes. pub fn verify_password(password: &str, stored: &str) -> bool { let (iterations, salt, expected) = parse_stored_hash(stored); let bytes: Vec = password.bytes().map(|b| b as i64).collect(); constant_time_equal(&pbkdf2_sha256(&bytes, &salt, iterations, expected.len() as i64), &expected) } pub fn fune_vector(args: &[Value]) -> Value { let password = match &args[0] { Value::Str(s) => s.as_str(), _ => panic!("password must be a string"), }; let stored = match &args[1] { Value::Str(s) => s.as_str(), _ => panic!("stored password hash must be a string"), }; Value::Bool(verify_password(password, stored)) }