use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use hmac::{Hmac, Mac};
use rand::Rng;
use sha2::{Digest, Sha256};
use zeroize::Zeroize;
type HmacSha256 = Hmac<Sha256>;
pub const DEFAULT_ITERATIONS: u32 = 4096;
pub fn compute_verifier(password: &str, iterations: u32) -> String {
let mut salt = [0u8; 16];
rand::rng().fill(&mut salt);
compute_verifier_with_salt(password, iterations, &salt)
}
fn compute_verifier_with_salt(password: &str, iterations: u32, salt: &[u8]) -> String {
let mut salted_password = [0u8; 32];
pbkdf2::pbkdf2_hmac::<Sha256>(password.as_bytes(), salt, iterations, &mut salted_password);
let mut client_key = hmac_sha256(&salted_password, b"Client Key");
let stored_key = Sha256::digest(&client_key);
let mut server_key = hmac_sha256(&salted_password, b"Server Key");
let verifier = format!(
"SCRAM-SHA-256${iterations}:{salt}${stored_key}:{server_key}",
salt = BASE64.encode(salt),
stored_key = BASE64.encode(stored_key),
server_key = BASE64.encode(&server_key),
);
salted_password.zeroize();
client_key.zeroize();
server_key.zeroize();
verifier
}
fn hmac_sha256(key: &[u8], message: &[u8]) -> Vec<u8> {
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC-SHA256 accepts any key length");
mac.update(message);
mac.finalize().into_bytes().to_vec()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verifier_has_correct_prefix() {
let verifier = compute_verifier("hunter2", DEFAULT_ITERATIONS);
assert!(
verifier.starts_with("SCRAM-SHA-256$"),
"verifier should start with SCRAM-SHA-256$, got: {verifier}"
);
}
#[test]
fn verifier_has_correct_structure() {
let verifier = compute_verifier("test-password", DEFAULT_ITERATIONS);
let rest = verifier.strip_prefix("SCRAM-SHA-256$").unwrap();
let (iter_salt, keys) = rest.split_once('$').expect("should have $ separator");
let (iter_str, salt_b64) = iter_salt
.split_once(':')
.expect("should have : in iter:salt");
let (stored_key_b64, server_key_b64) = keys.split_once(':').expect("should have : in keys");
assert_eq!(iter_str, "4096");
let salt = BASE64
.decode(salt_b64)
.expect("salt should be valid base64");
assert_eq!(salt.len(), 16, "salt should be 16 bytes");
let stored_key = BASE64
.decode(stored_key_b64)
.expect("StoredKey should be valid base64");
assert_eq!(
stored_key.len(),
32,
"StoredKey should be 32 bytes (SHA-256)"
);
let server_key = BASE64
.decode(server_key_b64)
.expect("ServerKey should be valid base64");
assert_eq!(
server_key.len(),
32,
"ServerKey should be 32 bytes (SHA-256)"
);
}
#[test]
fn different_passwords_produce_different_verifiers() {
let salt = [1u8; 16];
let v1 = compute_verifier_with_salt("password-a", DEFAULT_ITERATIONS, &salt);
let v2 = compute_verifier_with_salt("password-b", DEFAULT_ITERATIONS, &salt);
assert_ne!(v1, v2);
}
#[test]
fn same_password_different_salt_produces_different_verifiers() {
let v1 = compute_verifier_with_salt("same-password", DEFAULT_ITERATIONS, &[1u8; 16]);
let v2 = compute_verifier_with_salt("same-password", DEFAULT_ITERATIONS, &[2u8; 16]);
assert_ne!(v1, v2);
}
#[test]
fn deterministic_with_fixed_salt() {
let v1 = compute_verifier_with_salt("deterministic", 4096, &[42u8; 16]);
let v2 = compute_verifier_with_salt("deterministic", 4096, &[42u8; 16]);
assert_eq!(v1, v2);
}
#[test]
fn known_vector_matches_rfc7677() {
let salt = BASE64.decode("W22ZaJ0SNY7soEsUEjb6gQ==").unwrap();
let verifier = compute_verifier_with_salt("pencil", 4096, &salt);
assert_eq!(
verifier,
"SCRAM-SHA-256$4096:W22ZaJ0SNY7soEsUEjb6gQ==\
$WG5d8oPm3OtcPnkdi4Uo7BkeZkBFzpcXkuLmtbsT4qY=\
:wfPLwcE6nTWhTAmQ7tl2KeoiWGPlZqQxSrmfPwDl2dU="
);
}
}