use base64::Engine;
use hmac::{Hmac, Mac};
use sha2::Sha256;
pub fn account_password(secret: &str, label: &str) -> String {
let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).expect("HMAC takes a key of any length");
mac.update(label.as_bytes());
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn account_password_matches_the_golden_vectors() {
let file: serde_json::Value =
serde_json::from_str(include_str!("../testdata/account_password_vectors.json")).expect("vectors parse");
let vectors = file["vectors"].as_array().expect("a vectors array");
assert!(!vectors.is_empty());
for v in vectors {
let (secret, label) = (v["secret"].as_str().unwrap(), v["label"].as_str().unwrap());
assert!(!label.contains('.'), "a vector's message is a label, never a handle: {label}");
assert_eq!(account_password(secret, label), v["password"].as_str().unwrap(), "{secret:?} {label}");
}
}
#[test]
fn the_password_depends_on_the_whole_label_and_the_secret() {
let p = account_password("s", "rule-features");
assert_eq!(p.len(), 43, "32 bytes, base64url without padding");
assert!(p.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_'));
assert_ne!(p, account_password("s", "rule-feature"));
assert_ne!(p, account_password("t", "rule-features"));
}
}