use base64::engine::general_purpose::STANDARD as B64;
use base64::Engine as _;
use hmac::{Hmac, Mac};
use rand::RngCore as _;
use sha2::{Digest, Sha256};
use subtle::ConstantTimeEq as _;
use crate::error::{Error, Result};
type HmacSha256 = Hmac<Sha256>;
const GS2_HEADER: &str = "n,,";
fn hmac_sha256(key: &[u8], msg: &[u8]) -> [u8; 32] {
let mut mac = <HmacSha256 as Mac>::new_from_slice(key).expect("HMAC accepts any key length");
mac.update(msg);
mac.finalize().into_bytes().into()
}
pub struct Scram {
user: String,
password: String,
client_first_bare: String,
expected_server_sig: Vec<u8>,
}
impl Scram {
pub fn start(user: &str, password: &str) -> Result<(Self, String)> {
let mut raw = [0u8; 18];
rand::rngs::OsRng.fill_bytes(&mut raw);
let nonce = B64.encode(raw);
let client_first_bare = format!("n={user},r={nonce}");
let client_first = format!("{GS2_HEADER}{client_first_bare}");
Ok((
Self {
user: user.to_owned(),
password: password.to_owned(),
client_first_bare,
expected_server_sig: Vec::new(),
},
client_first,
))
}
pub fn client_final(&mut self, server_first: &str) -> Result<String> {
let (combined_nonce, salt, iterations) = parse_server_first(server_first)?;
let client_nonce = self
.client_first_bare
.strip_prefix(&format!("n={},r=", self.user))
.ok_or_else(|| Error::Auth("malformed client-first state".to_owned()))?;
if !combined_nonce.starts_with(client_nonce) {
return Err(Error::Auth(
"server nonce does not extend client nonce".to_owned(),
));
}
let mut salted = [0u8; 32];
pbkdf2::pbkdf2_hmac::<Sha256>(self.password.as_bytes(), &salt, iterations, &mut salted);
let client_key = hmac_sha256(&salted, b"Client Key");
let stored_key: [u8; 32] = Sha256::digest(client_key).into();
let channel_binding = B64.encode(GS2_HEADER.as_bytes()); let without_proof = format!("c={channel_binding},r={combined_nonce}");
let auth_message = format!(
"{},{},{}",
self.client_first_bare, server_first, without_proof
);
let client_sig = hmac_sha256(&stored_key, auth_message.as_bytes());
let proof: Vec<u8> = client_key
.iter()
.zip(client_sig.iter())
.map(|(k, s)| k ^ s)
.collect();
let server_key = hmac_sha256(&salted, b"Server Key");
self.expected_server_sig = hmac_sha256(&server_key, auth_message.as_bytes()).to_vec();
Ok(format!("{without_proof},p={}", B64.encode(proof)))
}
#[must_use]
pub fn verify(&self, server_final: &str) -> bool {
for field in server_final.split(',') {
if let Some(value) = field.strip_prefix("v=") {
if let Ok(sig) = B64.decode(value) {
return sig.ct_eq(&self.expected_server_sig).into();
}
}
}
false
}
}
fn parse_server_first(message: &str) -> Result<(String, Vec<u8>, u32)> {
let mut nonce = None;
let mut salt = None;
let mut iters = None;
for field in message.split(',') {
if let Some((key, value)) = field.split_once('=') {
match key {
"r" => nonce = Some(value.to_owned()),
"s" => {
salt = Some(
B64.decode(value)
.map_err(|_| Error::Auth("bad salt in server-first".to_owned()))?,
);
}
"i" => {
iters = Some(
value
.parse::<u32>()
.map_err(|_| Error::Auth("bad iteration count".to_owned()))?,
);
}
_ => {}
}
}
}
match (nonce, salt, iters) {
(Some(n), Some(s), Some(i)) if !n.is_empty() && !s.is_empty() && i > 0 => Ok((n, s, i)),
_ => Err(Error::Auth("malformed server-first message".to_owned())),
}
}