1use base64::{Alphabet, decode as b64_decode, encode as b64_encode};
17use crypto::{Hasher, hmac::Hmac, random, sha2::Sha256};
18
19use crate::error::Error;
20
21pub const DEFAULT_MAX_SCRAM_ITERATIONS: u32 = 1_000_000;
31
32pub const MAX_SALT_LEN: usize = 1024;
34
35pub const MAX_SERVER_FIRST_LEN: usize = 8 * 1024;
37
38fn hmac_sha256(key: &[u8], data: &[u8]) -> crypto::Hash {
39 Hmac::<Sha256>::mac(key, data)
40}
41
42fn saslprep(password: &str) -> std::result::Result<String, Error> {
56 let mut out = String::with_capacity(password.len());
57 for c in password.chars() {
58 match c {
59 '\u{0000}'..='\u{001F}' | '\u{007F}' => {
61 return Err(Error::Auth(
62 "the password contains a control character that SASLprep prohibits".into(),
63 ));
64 }
65 '\u{00A0}' | '\u{1680}' | '\u{2000}'..='\u{200A}' | '\u{202F}' | '\u{205F}' | '\u{3000}' => {
67 out.push(' ');
68 }
69 '\u{00AD}'
71 | '\u{034F}'
72 | '\u{1806}'
73 | '\u{180B}'..='\u{180D}'
74 | '\u{200B}'..='\u{200D}'
75 | '\u{2060}'
76 | '\u{FE00}'..='\u{FE0F}'
77 | '\u{FEFF}' => {}
78 other => out.push(other),
79 }
80 }
81 Ok(out)
82}
83
84fn hi(password: &[u8], salt: &[u8], iterations: u32, max_iterations: u32) -> std::result::Result<[u8; 32], Error> {
91 if !(1..=max_iterations).contains(&iterations) {
92 return Err(Error::Auth(format!(
93 "server sent an unacceptable SCRAM iteration count ({iterations}, accepted range is 1..={max_iterations})"
94 )));
95 }
96 if salt.len() > MAX_SALT_LEN {
97 return Err(Error::Auth(format!(
98 "server sent an oversized SCRAM salt ({} bytes)",
99 salt.len()
100 )));
101 }
102 let mut salt_input = Vec::with_capacity(salt.len() + 4);
103 salt_input.extend_from_slice(salt);
104 salt_input.extend_from_slice(&1u32.to_be_bytes());
105
106 let mut u = hmac_sha256(password, &salt_input);
107 let mut result = [0u8; 32];
108 result.copy_from_slice(u.as_ref());
109
110 for _ in 1..iterations {
111 u = hmac_sha256(password, u.as_ref());
112 for (a, b) in result.iter_mut().zip(u.as_ref()) {
113 *a ^= b;
114 }
115 }
116 Ok(result)
117}
118
119pub struct ScramClient {
121 password: String,
122 client_nonce: String,
123 client_first_bare: String,
124 server_first: Option<String>,
125 client_final_without_proof: Option<String>,
126 salted_password: Option<[u8; 32]>,
127 max_iterations: u32,
128}
129
130impl ScramClient {
131 pub fn new(password: &str) -> std::result::Result<Self, Error> {
136 Self::with_max_iterations(password, DEFAULT_MAX_SCRAM_ITERATIONS)
137 }
138
139 pub fn with_max_iterations(password: &str, max_iterations: u32) -> std::result::Result<Self, Error> {
147 let raw = random::bytes::<18>().map_err(|e| Error::Auth(format!("random: {e}")))?;
148 let client_nonce = b64_encode(raw, Alphabet::StandardNoPadding);
149 Self::with_nonce_and_max("", password, client_nonce, max_iterations)
150 }
151
152 #[cfg(test)]
153 fn with_nonce(username: &str, password: &str, client_nonce: String) -> Self {
154 Self::with_nonce_and_max(username, password, client_nonce, DEFAULT_MAX_SCRAM_ITERATIONS)
155 .expect("test password is valid")
156 }
157
158 fn with_nonce_and_max(
159 username: &str,
160 password: &str,
161 client_nonce: String,
162 max_iterations: u32,
163 ) -> std::result::Result<Self, Error> {
164 let password = saslprep(password)?;
165 let client_first_bare = format!("n={username},r={client_nonce}");
166 Ok(ScramClient {
167 password,
168 client_nonce,
169 client_first_bare,
170 server_first: None,
171 client_final_without_proof: None,
172 salted_password: None,
173 max_iterations,
174 })
175 }
176
177 pub fn client_first_message(&self) -> String {
180 format!("n,,{}", self.client_first_bare)
181 }
182
183 pub fn parse_server_first_message(&mut self, data: &[u8]) -> std::result::Result<(), Error> {
191 if data.len() > MAX_SERVER_FIRST_LEN {
192 return Err(Error::Auth("server-first message is too long".into()));
193 }
194 let msg = std::str::from_utf8(data).map_err(|_| Error::Auth("invalid UTF-8 in server-first".into()))?;
195 self.server_first = Some(msg.to_string());
196
197 let mut combined_nonce: Option<&str> = None;
198 let mut salt_b64: Option<&str> = None;
199 let mut iterations = None;
200
201 for part in msg.split(',') {
202 if let Some(v) = part.strip_prefix("r=") {
203 if combined_nonce.replace(v).is_some() {
204 return Err(Error::Auth("duplicate nonce attribute".into()));
205 }
206 } else if let Some(v) = part.strip_prefix("s=") {
207 if salt_b64.replace(v).is_some() {
208 return Err(Error::Auth("duplicate salt attribute".into()));
209 }
210 } else if let Some(v) = part.strip_prefix("i=") {
211 if iterations.is_some() {
212 return Err(Error::Auth("duplicate iteration attribute".into()));
213 }
214 iterations = Some(
215 v.parse::<u32>()
216 .map_err(|_| Error::Auth("invalid iteration count".into()))?,
217 );
218 }
219 }
220
221 let combined_nonce = combined_nonce.ok_or_else(|| Error::Auth("missing nonce".into()))?;
222 let salt_b64 = salt_b64.ok_or_else(|| Error::Auth("missing salt".into()))?;
223 let iterations = iterations.ok_or_else(|| Error::Auth("missing iterations".into()))?;
224
225 if !combined_nonce.starts_with(&self.client_nonce) || combined_nonce.len() <= self.client_nonce.len() {
228 return Err(Error::Auth("server nonce does not extend the client nonce".into()));
229 }
230
231 let salt = b64_decode(salt_b64, Alphabet::Standard).map_err(|e| Error::Auth(format!("invalid salt: {e}")))?;
232
233 self.salted_password = Some(hi(self.password.as_bytes(), &salt, iterations, self.max_iterations)?);
234 self.client_final_without_proof = Some(format!("c=biws,r={combined_nonce}"));
235 Ok(())
236 }
237
238 fn auth_message(&self) -> std::result::Result<String, Error> {
239 let server_first = self
240 .server_first
241 .as_deref()
242 .ok_or_else(|| Error::Auth("server-first message has not been parsed".into()))?;
243 let client_final = self
244 .client_final_without_proof
245 .as_deref()
246 .ok_or_else(|| Error::Auth("server-first message has not been parsed".into()))?;
247 Ok(format!("{},{},{}", self.client_first_bare, server_first, client_final))
248 }
249
250 pub fn build_client_final_message(&self) -> std::result::Result<Vec<u8>, Error> {
257 let sp = self
258 .salted_password
259 .as_ref()
260 .ok_or_else(|| Error::Auth("server-first message has not been parsed".into()))?;
261 let client_key = hmac_sha256(sp, b"Client Key");
262 let stored_key = Sha256::hash(client_key.as_ref());
263 let client_signature = hmac_sha256(stored_key.as_ref(), self.auth_message()?.as_bytes());
264
265 let mut proof = [0u8; 32];
266 proof.copy_from_slice(client_key.as_ref());
267 for (a, b) in proof.iter_mut().zip(client_signature.as_ref()) {
268 *a ^= b;
269 }
270
271 Ok(format!(
272 "{},p={}",
273 self.client_final_without_proof
274 .as_deref()
275 .ok_or_else(|| Error::Auth("server-first message has not been parsed".into()))?,
276 b64_encode(proof, Alphabet::Standard)
277 )
278 .into_bytes())
279 }
280
281 pub fn parse_server_final_message(&self, data: &[u8]) -> std::result::Result<(), Error> {
289 if data.len() > MAX_SERVER_FIRST_LEN {
290 return Err(Error::Auth("server-final message is too long".into()));
291 }
292 let msg = std::str::from_utf8(data).map_err(|_| Error::Auth("invalid UTF-8 in server-final".into()))?;
293
294 let mut server_sig_b64 = None;
295 for part in msg.split(',') {
296 if let Some(v) = part.strip_prefix("v=") {
297 if server_sig_b64.replace(v).is_some() {
298 return Err(Error::Auth("duplicate server signature".into()));
299 }
300 } else if let Some(e) = part.strip_prefix("e=") {
301 return Err(Error::Auth(format!("server rejected authentication: {e}")));
302 }
303 }
304
305 let server_sig_b64 = server_sig_b64.ok_or_else(|| Error::Auth("missing server signature".into()))?;
306 let server_sig = b64_decode(server_sig_b64, Alphabet::Standard)
307 .map_err(|_| Error::Auth("server signature is not base64".into()))?;
308
309 let sp = self
310 .salted_password
311 .as_ref()
312 .ok_or_else(|| Error::Auth("server-first message has not been parsed".into()))?;
313 let server_key = hmac_sha256(sp, b"Server Key");
314 let expected = hmac_sha256(server_key.as_ref(), self.auth_message()?.as_bytes());
315
316 if !constant_time_eq::constant_time_eq(expected.as_ref(), &server_sig) {
318 return Err(Error::Auth("server signature mismatch".into()));
319 }
320 Ok(())
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 #[test]
330 fn rfc7677_vector() {
331 let mut client = ScramClient::with_nonce("user", "pencil", "rOprNGfwEbeRWgbNEkqO".to_string());
332 assert_eq!(client.client_first_message(), "n,,n=user,r=rOprNGfwEbeRWgbNEkqO");
333
334 client
335 .parse_server_first_message(
336 b"r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096",
337 )
338 .unwrap();
339
340 let final_msg = client.build_client_final_message().unwrap();
341 assert_eq!(
342 String::from_utf8(final_msg).unwrap(),
343 "c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ="
344 );
345
346 client
347 .parse_server_final_message(b"v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=")
348 .unwrap();
349 }
350
351 #[test]
352 fn rejects_wrong_nonce() {
353 let mut client = ScramClient::with_nonce("user", "pencil", "abc".to_string());
354 let err = client
355 .parse_server_first_message(b"r=xyz,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096")
356 .unwrap_err();
357 assert!(matches!(err, Error::Auth(_)));
358 }
359
360 #[test]
361 fn rejects_unextended_nonce() {
362 let mut client = ScramClient::with_nonce("user", "pencil", "abc".to_string());
364 let err = client
365 .parse_server_first_message(b"r=abc,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096")
366 .unwrap_err();
367 assert!(matches!(err, Error::Auth(_)));
368 }
369
370 #[test]
371 fn rejects_unbounded_iterations() {
372 for i in ["0", "2000001", "4294967295"] {
373 let mut client = ScramClient::with_nonce("user", "pencil", "abc".to_string());
374 let msg = format!("r=abcd,s=W22ZaJ0SNY7soEsUEjb6gQ==,i={i}");
375 let err = client.parse_server_first_message(msg.as_bytes()).unwrap_err();
376 assert!(matches!(err, Error::Auth(_)), "i={i} must be rejected");
377 }
378 }
379
380 #[test]
381 fn accepts_bounded_iterations() {
382 for i in ["1", "4096", "100000"] {
383 let mut client = ScramClient::with_nonce("user", "pencil", "abc".to_string());
384 let msg = format!("r=abcd,s=W22ZaJ0SNY7soEsUEjb6gQ==,i={i}");
385 client.parse_server_first_message(msg.as_bytes()).unwrap();
386 }
387 }
388
389 #[test]
390 fn custom_iteration_limit_is_honoured() {
391 let mut client = ScramClient::with_nonce_and_max("user", "pencil", "abc".to_string(), 1000).unwrap();
393 let msg = "r=abcd,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096";
394 assert!(client.parse_server_first_message(msg.as_bytes()).is_err());
395
396 let mut client = ScramClient::with_nonce_and_max("user", "pencil", "abc".to_string(), 4096).unwrap();
397 client.parse_server_first_message(msg.as_bytes()).unwrap();
398 }
399
400 #[test]
401 fn rejects_oversized_salt() {
402 let salt = "A".repeat(MAX_SALT_LEN * 2);
403 let mut client = ScramClient::with_nonce("user", "pencil", "abc".to_string());
404 let msg = format!("r=abcd,s={salt},i=4096");
405 assert!(client.parse_server_first_message(msg.as_bytes()).is_err());
406 }
407
408 #[test]
409 fn rejects_oversized_messages() {
410 let mut client = ScramClient::with_nonce("user", "pencil", "abc".to_string());
411 let msg = format!("r=abcd,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096,{}", "x".repeat(MAX_SERVER_FIRST_LEN));
412 assert!(client.parse_server_first_message(msg.as_bytes()).is_err());
413 assert!(
414 client
415 .parse_server_final_message(&vec![b'v'; MAX_SERVER_FIRST_LEN + 1])
416 .is_err()
417 );
418 }
419
420 #[test]
421 fn rejects_duplicate_attributes() {
422 let mut client = ScramClient::with_nonce("user", "pencil", "abc".to_string());
423 assert!(
424 client
425 .parse_server_first_message(b"r=abcd,r=abcd,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096")
426 .is_err()
427 );
428 }
429
430 #[test]
431 fn out_of_order_calls_error_instead_of_panicking() {
432 let client = ScramClient::with_nonce("user", "pencil", "abc".to_string());
433 assert!(client.build_client_final_message().is_err());
434 assert!(client.parse_server_final_message(b"v=whatever").is_err());
435 }
436
437 #[test]
438 fn rejects_bad_server_signature() {
439 let mut client = ScramClient::with_nonce("user", "pencil", "abc".to_string());
440 client
441 .parse_server_first_message(b"r=abcdef,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096")
442 .unwrap();
443 let _ = client.build_client_final_message().unwrap();
444 assert!(
445 client
446 .parse_server_final_message(b"v=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
447 .is_err()
448 );
449 assert!(client.parse_server_final_message(b"v=not base64 !!").is_err());
450 assert!(client.parse_server_final_message(b"e=server-error").is_err());
451 }
452
453 #[test]
454 fn saslprep_maps_and_prohibits() {
455 assert_eq!(saslprep("pencil").unwrap(), "pencil");
457 assert_eq!(saslprep("a\u{00A0}b").unwrap(), "a b");
459 assert_eq!(saslprep("a\u{3000}b").unwrap(), "a b");
460 assert_eq!(saslprep("a\u{00AD}b").unwrap(), "ab");
462 assert_eq!(saslprep("a\u{200B}b").unwrap(), "ab");
463 for c in ['\u{0000}', '\u{001F}', '\u{007F}'] {
465 assert!(saslprep(&format!("a{c}b")).is_err(), "control {c:?} must be rejected");
466 }
467 }
468
469 #[test]
470 fn passwords_are_saslprep_before_derivation() {
471 let mut with_nbsp = ScramClient::with_nonce("user", "pi\u{00A0}ncil", "abc".to_string());
474 let mut ascii = ScramClient::with_nonce("user", "pi ncil", "abc".to_string());
475 let server_first = b"r=abcd,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096";
476 with_nbsp.parse_server_first_message(server_first).unwrap();
477 ascii.parse_server_first_message(server_first).unwrap();
478 assert_eq!(
479 with_nbsp.build_client_final_message().unwrap(),
480 ascii.build_client_final_message().unwrap()
481 );
482 }
483}