fraiseql_wire/auth/scram/
mod.rs1use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
7use hmac::{Hmac, KeyInit, Mac};
8use pbkdf2::pbkdf2;
9use rand::Rng;
10use sha2::{Digest, Sha256};
11use std::fmt;
12use zeroize::Zeroizing;
13
14type HmacSha256 = Hmac<Sha256>;
15
16pub(crate) const MAX_SCRAM_ITERATIONS: u32 = 1_000_000;
23
24#[derive(Debug, Clone)]
26#[non_exhaustive]
27pub enum ScramError {
28 InvalidServerProof(String),
30 InvalidServerMessage(String),
32 Utf8Error(String),
34 Base64Error(String),
36 KeyDerivation(String),
38}
39
40impl fmt::Display for ScramError {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 match self {
43 ScramError::InvalidServerProof(msg) => write!(f, "invalid server proof: {}", msg),
44 ScramError::InvalidServerMessage(msg) => write!(f, "invalid server message: {}", msg),
45 ScramError::Utf8Error(msg) => write!(f, "UTF-8 error: {}", msg),
46 ScramError::Base64Error(msg) => write!(f, "Base64 error: {}", msg),
47 ScramError::KeyDerivation(msg) => write!(f, "key derivation error: {}", msg),
48 }
49 }
50}
51
52impl std::error::Error for ScramError {}
53
54#[derive(Clone, Debug)]
56pub struct ScramState {
57 auth_message: Vec<u8>,
59 server_key: Vec<u8>,
61}
62
63pub struct ScramClient {
65 username: String,
66 password: Zeroizing<String>,
69 nonce: String,
70}
71
72impl ScramClient {
73 #[must_use]
75 pub fn new(username: String, password: String) -> Self {
76 let mut rng = rand::rng();
78 let nonce_bytes: Vec<u8> = (0..24).map(|_| rng.random()).collect();
79 let nonce = BASE64.encode(&nonce_bytes);
80
81 Self {
82 username,
83 password: Zeroizing::new(password),
84 nonce,
85 }
86 }
87
88 #[must_use]
90 pub fn client_first(&self) -> String {
91 let escaped_username = self.username.replace('=', "=3D").replace(',', "=2C");
96 format!("n,,n={},r={}", escaped_username, self.nonce)
97 }
98
99 pub fn client_final(&mut self, server_first: &str) -> Result<(String, ScramState), ScramError> {
110 let (server_nonce, salt, iterations) = parse_server_first(server_first)?;
112
113 if !server_nonce.starts_with(&self.nonce) {
115 return Err(ScramError::InvalidServerMessage(
116 "server nonce doesn't contain client nonce".to_string(),
117 ));
118 }
119
120 let salt_bytes = BASE64
122 .decode(&salt)
123 .map_err(|_| ScramError::Base64Error("invalid salt encoding".to_string()))?;
124 let iterations = iterations
125 .parse::<u32>()
126 .map_err(|_| ScramError::InvalidServerMessage("invalid iteration count".to_string()))?;
127
128 if iterations > MAX_SCRAM_ITERATIONS {
131 return Err(ScramError::InvalidServerMessage(format!(
132 "server iteration count {iterations} exceeds maximum of {MAX_SCRAM_ITERATIONS}"
133 )));
134 }
135
136 let channel_binding = BASE64.encode(b"n,,");
138
139 let client_final_without_proof = format!("c={},r={}", channel_binding, server_nonce);
141
142 let escaped_username = self.username.replace('=', "=3D").replace(',', "=2C");
148 let client_first_bare = format!("n={},r={}", escaped_username, self.nonce);
149 let auth_message = format!(
150 "{},{},{}",
151 client_first_bare, server_first, client_final_without_proof
152 );
153
154 let proof = calculate_client_proof(
156 &self.password,
157 &salt_bytes,
158 iterations,
159 auth_message.as_bytes(),
160 )?;
161
162 let server_key = calculate_server_key(&self.password, &salt_bytes, iterations)?;
164
165 let client_final = format!("{},p={}", client_final_without_proof, BASE64.encode(&proof));
167
168 let state = ScramState {
169 auth_message: auth_message.into_bytes(),
170 server_key,
171 };
172
173 Ok((client_final, state))
174 }
175
176 pub fn verify_server_final(
184 &self,
185 server_final: &str,
186 state: &ScramState,
187 ) -> Result<(), ScramError> {
188 let server_sig_encoded = server_final
190 .strip_prefix("v=")
191 .ok_or_else(|| ScramError::InvalidServerMessage("missing 'v=' prefix".to_string()))?;
192
193 let server_signature = BASE64.decode(server_sig_encoded).map_err(|_| {
194 ScramError::Base64Error("invalid server signature encoding".to_string())
195 })?;
196
197 let expected_signature =
199 calculate_server_signature(&state.server_key, &state.auth_message)?;
200
201 if constant_time_compare(&server_signature, &expected_signature) {
203 Ok(())
204 } else {
205 Err(ScramError::InvalidServerProof(
206 "server signature verification failed".to_string(),
207 ))
208 }
209 }
210}
211
212pub(crate) fn parse_server_first(msg: &str) -> Result<(String, String, String), ScramError> {
214 let mut nonce = String::new();
215 let mut salt = String::new();
216 let mut iterations = String::new();
217
218 for part in msg.split(',') {
219 if let Some(value) = part.strip_prefix("r=") {
220 nonce = value.to_string();
221 } else if let Some(value) = part.strip_prefix("s=") {
222 salt = value.to_string();
223 } else if let Some(value) = part.strip_prefix("i=") {
224 iterations = value.to_string();
225 }
226 }
227
228 if nonce.is_empty() || salt.is_empty() || iterations.is_empty() {
229 return Err(ScramError::InvalidServerMessage(
230 "missing required fields in server first message".to_string(),
231 ));
232 }
233
234 Ok((nonce, salt, iterations))
235}
236
237fn calculate_client_proof(
239 password: &str,
240 salt: &[u8],
241 iterations: u32,
242 auth_message: &[u8],
243) -> Result<Vec<u8>, ScramError> {
244 let password_bytes = password.as_bytes();
246 let mut salted_password = vec![0u8; 32]; pbkdf2::<HmacSha256>(password_bytes, salt, iterations, &mut salted_password).map_err(|_| {
251 ScramError::KeyDerivation("PBKDF2 produced an invalid output length".into())
252 })?;
253
254 let mut client_key_hmac = HmacSha256::new_from_slice(&salted_password)
256 .map_err(|_| ScramError::Utf8Error("HMAC key error".to_string()))?;
257 client_key_hmac.update(b"Client Key");
258 let client_key = client_key_hmac.finalize().into_bytes();
259
260 let stored_key = Sha256::digest(client_key.to_vec().as_slice());
262
263 let mut client_sig_hmac = HmacSha256::new_from_slice(&stored_key)
265 .map_err(|_| ScramError::Utf8Error("HMAC key error".to_string()))?;
266 client_sig_hmac.update(auth_message);
267 let client_signature = client_sig_hmac.finalize().into_bytes();
268
269 let mut proof = client_key.to_vec();
271 for (proof_byte, sig_byte) in proof.iter_mut().zip(client_signature.iter()) {
272 *proof_byte ^= sig_byte;
273 }
274
275 Ok(proof.clone())
276}
277
278fn calculate_server_key(
280 password: &str,
281 salt: &[u8],
282 iterations: u32,
283) -> Result<Vec<u8>, ScramError> {
284 let password_bytes = password.as_bytes();
286 let mut salted_password = vec![0u8; 32];
287 pbkdf2::<HmacSha256>(password_bytes, salt, iterations, &mut salted_password).map_err(|_| {
291 ScramError::KeyDerivation("PBKDF2 produced an invalid output length".into())
292 })?;
293
294 let mut server_key_hmac = HmacSha256::new_from_slice(&salted_password)
296 .map_err(|_| ScramError::Utf8Error("HMAC key error".to_string()))?;
297 server_key_hmac.update(b"Server Key");
298
299 Ok(server_key_hmac.finalize().into_bytes().to_vec())
300}
301
302fn calculate_server_signature(
304 server_key: &[u8],
305 auth_message: &[u8],
306) -> Result<Vec<u8>, ScramError> {
307 let mut hmac = HmacSha256::new_from_slice(server_key)
308 .map_err(|_| ScramError::Utf8Error("invalid HMAC key for server signature".to_string()))?;
309 hmac.update(auth_message);
310 Ok(hmac.finalize().into_bytes().to_vec())
311}
312
313pub(crate) fn constant_time_compare(a: &[u8], b: &[u8]) -> bool {
317 use subtle::ConstantTimeEq;
318 a.ct_eq(b).into()
319}
320
321#[cfg(test)]
322mod tests;