1use hmac::{Hmac, KeyInit, Mac};
7use sha2::{Digest, Sha256};
8
9use super::handshake::{
10 base64_std, base64_std_decode, build_scram_server_first, expect_auth_response_payload,
11 parse_scram_client_final, parse_scram_client_first, AuthFail, AuthOk,
12};
13use super::MessageKind;
14
15pub const DEFAULT_ITER: u32 = 16_384;
17
18pub const MIN_ITER: u32 = 4096;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct ScramVerifier {
24 pub salt: Vec<u8>,
25 pub iter: u32,
26 pub stored_key: [u8; 32],
27 pub server_key: [u8; 32],
28}
29
30impl ScramVerifier {
31 pub fn from_password(password: &str, salt: Vec<u8>, iter: u32) -> Self {
32 let salted = salted_password(password.as_bytes(), &salt, iter);
33 let client_key = hmac_sha256(&salted, b"Client Key");
34 let stored_key = sha256(&client_key);
35 let server_key = hmac_sha256(&salted, b"Server Key");
36 Self {
37 salt,
38 iter,
39 stored_key,
40 server_key,
41 }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum ScramClientOutput {
48 Response(Vec<u8>),
49 Authenticated(AuthOk),
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum ScramClientError {
55 Refused(String),
56 Protocol(String),
57}
58
59impl std::fmt::Display for ScramClientError {
60 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 match self {
62 Self::Refused(reason) => write!(formatter, "SCRAM authentication refused: {reason}"),
63 Self::Protocol(reason) => formatter.write_str(reason),
64 }
65 }
66}
67
68impl std::error::Error for ScramClientError {}
69
70pub struct ScramClientHandshake {
72 state: ScramClientState,
73}
74
75enum ScramClientState {
76 Ready {
77 username: String,
78 password: String,
79 client_nonce: String,
80 },
81 AwaitServerFirst {
82 password: String,
83 client_nonce: String,
84 client_first_bare: String,
85 },
86 AwaitServerFinal {
87 expected_server_signature: [u8; 32],
88 },
89 Complete,
90}
91
92impl ScramClientHandshake {
93 pub fn new(
95 username: impl Into<String>,
96 password: impl Into<String>,
97 client_nonce: impl Into<String>,
98 ) -> Result<Self, ScramClientError> {
99 let username = username.into();
100 if username.is_empty() || username.contains([',', '=']) {
101 return Err(ScramClientError::Protocol(
102 "SCRAM username must be non-empty and contain neither ',' nor '='".to_string(),
103 ));
104 }
105 let client_nonce = client_nonce.into();
106 if client_nonce.is_empty() || client_nonce.contains(',') {
107 return Err(ScramClientError::Protocol(
108 "SCRAM client nonce must be non-empty and contain no ','".to_string(),
109 ));
110 }
111 Ok(Self {
112 state: ScramClientState::Ready {
113 username,
114 password: password.into(),
115 client_nonce,
116 },
117 })
118 }
119
120 pub fn start(&mut self) -> Result<ScramClientOutput, ScramClientError> {
122 let state = std::mem::replace(&mut self.state, ScramClientState::Complete);
123 let ScramClientState::Ready {
124 username,
125 password,
126 client_nonce,
127 } = state
128 else {
129 return Err(unexpected_client_state());
130 };
131 let client_first_bare = format!("n={username},r={client_nonce}");
132 let response = format!("n,,{client_first_bare}").into_bytes();
133 self.state = ScramClientState::AwaitServerFirst {
134 password,
135 client_nonce,
136 client_first_bare,
137 };
138 Ok(ScramClientOutput::Response(response))
139 }
140
141 pub fn step(
143 &mut self,
144 kind: MessageKind,
145 payload: &[u8],
146 ) -> Result<ScramClientOutput, ScramClientError> {
147 let state = std::mem::replace(&mut self.state, ScramClientState::Complete);
148 match state {
149 ScramClientState::AwaitServerFirst {
150 password,
151 client_nonce,
152 client_first_bare,
153 } => {
154 self.receive_server_first(kind, payload, password, client_nonce, client_first_bare)
155 }
156 ScramClientState::AwaitServerFinal {
157 expected_server_signature,
158 } => Self::receive_server_final(kind, payload, &expected_server_signature),
159 ScramClientState::Ready { .. } | ScramClientState::Complete => {
160 Err(unexpected_client_state())
161 }
162 }
163 }
164
165 fn receive_server_first(
166 &mut self,
167 kind: MessageKind,
168 payload: &[u8],
169 password: String,
170 client_nonce: String,
171 client_first_bare: String,
172 ) -> Result<ScramClientOutput, ScramClientError> {
173 if kind == MessageKind::AuthFail {
174 return Err(auth_refused(payload));
175 }
176 if kind != MessageKind::AuthRequest {
177 return Err(ScramClientError::Protocol(format!(
178 "expected AuthRequest(server-first-message), got {kind:?}"
179 )));
180 }
181 let server_first = std::str::from_utf8(payload).map_err(|_| {
182 ScramClientError::Protocol("SCRAM server-first is not UTF-8".to_string())
183 })?;
184 let (combined_nonce, salt, iter) = parse_server_first(server_first, &client_nonce)?;
185 let client_final_no_proof = format!("c=biws,r={combined_nonce}");
186 let message = auth_message(&client_first_bare, server_first, &client_final_no_proof);
187 let salted = salted_password(password.as_bytes(), &salt, iter);
188 let client_key = hmac_sha256(&salted, b"Client Key");
189 let stored_key = sha256(&client_key);
190 let proof = client_proof(&stored_key, &message, &client_key);
191 let expected_server_signature =
192 server_signature(&hmac_sha256(&salted, b"Server Key"), &message);
193 let response = format!("{client_final_no_proof},p={}", base64_std(&proof)).into_bytes();
194 self.state = ScramClientState::AwaitServerFinal {
195 expected_server_signature,
196 };
197 Ok(ScramClientOutput::Response(response))
198 }
199
200 fn receive_server_final(
201 kind: MessageKind,
202 payload: &[u8],
203 expected_server_signature: &[u8; 32],
204 ) -> Result<ScramClientOutput, ScramClientError> {
205 if kind == MessageKind::AuthFail {
206 return Err(auth_refused(payload));
207 }
208 if kind != MessageKind::AuthOk {
209 return Err(ScramClientError::Protocol(format!(
210 "expected AuthOk/AuthFail, got {kind:?}"
211 )));
212 }
213 let auth_ok = AuthOk::from_payload(payload)
214 .map_err(|error| ScramClientError::Protocol(format!("decode SCRAM AuthOk: {error}")))?;
215 let encoded_signature = auth_ok.server_signature.as_deref().ok_or_else(|| {
216 ScramClientError::Protocol("SCRAM AuthOk omitted server signature".to_string())
217 })?;
218 let presented_signature = base64_std_decode(encoded_signature).ok_or_else(|| {
219 ScramClientError::Protocol(
220 "SCRAM AuthOk server signature is not valid base64".to_string(),
221 )
222 })?;
223 if !constant_time_eq(expected_server_signature, &presented_signature) {
224 return Err(ScramClientError::Protocol(
225 "SCRAM server signature did not verify".to_string(),
226 ));
227 }
228 Ok(ScramClientOutput::Authenticated(auth_ok))
229 }
230}
231
232fn parse_server_first(
233 server_first: &str,
234 client_nonce: &str,
235) -> Result<(String, Vec<u8>, u32), ScramClientError> {
236 let mut combined_nonce = None;
237 let mut encoded_salt = None;
238 let mut iterations = None;
239 for field in server_first.split(',') {
240 if let Some(value) = field.strip_prefix("r=") {
241 set_once(&mut combined_nonce, value, "r")?;
242 } else if let Some(value) = field.strip_prefix("s=") {
243 set_once(&mut encoded_salt, value, "s")?;
244 } else if let Some(value) = field.strip_prefix("i=") {
245 set_once(&mut iterations, value, "i")?;
246 }
247 }
248
249 let combined_nonce = combined_nonce.ok_or_else(|| {
250 ScramClientError::Protocol("SCRAM server-first omitted r=<nonce>".to_string())
251 })?;
252 if !combined_nonce.starts_with(client_nonce) || combined_nonce == client_nonce {
253 return Err(ScramClientError::Protocol(
254 "server nonce does not extend client nonce".to_string(),
255 ));
256 }
257 let encoded_salt = encoded_salt.ok_or_else(|| {
258 ScramClientError::Protocol("SCRAM server-first omitted s=<salt>".to_string())
259 })?;
260 let salt = base64_std_decode(encoded_salt)
261 .ok_or_else(|| ScramClientError::Protocol("server salt is not valid base64".to_string()))?;
262 if salt.is_empty() {
263 return Err(ScramClientError::Protocol(
264 "server salt is empty".to_string(),
265 ));
266 }
267 let iterations = iterations
268 .ok_or_else(|| {
269 ScramClientError::Protocol("SCRAM server-first omitted i=<iterations>".to_string())
270 })?
271 .parse::<u32>()
272 .map_err(|_| {
273 ScramClientError::Protocol("SCRAM iteration count is not an integer".to_string())
274 })?;
275 if iterations < MIN_ITER {
276 return Err(ScramClientError::Protocol(format!(
277 "SCRAM iteration count {iterations} is below minimum {MIN_ITER}"
278 )));
279 }
280 Ok((combined_nonce.to_string(), salt, iterations))
281}
282
283fn set_once<'a>(
284 destination: &mut Option<&'a str>,
285 value: &'a str,
286 name: &str,
287) -> Result<(), ScramClientError> {
288 if destination.replace(value).is_some() {
289 return Err(ScramClientError::Protocol(format!(
290 "SCRAM server-first repeated {name}= field"
291 )));
292 }
293 Ok(())
294}
295
296fn auth_refused(payload: &[u8]) -> ScramClientError {
297 match AuthFail::from_payload(payload) {
298 Ok(failure) => ScramClientError::Refused(failure.reason),
299 Err(error) => ScramClientError::Protocol(format!("decode SCRAM AuthFail: {error}")),
300 }
301}
302
303fn unexpected_client_state() -> ScramClientError {
304 ScramClientError::Protocol("unexpected SCRAM client message".to_string())
305}
306
307#[derive(Debug, Clone, PartialEq, Eq)]
309pub enum ScramServerInput {
310 ClientMessage {
311 correlation_id: u64,
312 kind: MessageKind,
313 payload: Vec<u8>,
314 },
315 Verifier(Option<ScramVerifier>),
316}
317
318#[derive(Debug, Clone, PartialEq, Eq)]
320pub enum ScramServerOutput {
321 NeedVerifier {
322 username: String,
323 },
324 Challenge {
325 correlation_id: u64,
326 payload: Vec<u8>,
327 },
328 Authenticated {
329 correlation_id: u64,
330 username: String,
331 server_signature: [u8; 32],
332 },
333 Failed {
334 correlation_id: u64,
335 reason: String,
336 },
337}
338
339#[derive(Debug)]
341pub struct ScramServerHandshake {
342 server_nonce: String,
343 unknown_user_salt: Vec<u8>,
344 state: ScramServerState,
345}
346
347#[derive(Debug)]
348enum ScramServerState {
349 AwaitClientFirst,
350 AwaitVerifier {
351 correlation_id: u64,
352 username: String,
353 client_nonce: String,
354 client_first_bare: String,
355 },
356 AwaitClientFinal(ScramClientFinalState),
357 Complete,
358}
359
360#[derive(Debug)]
361struct ScramClientFinalState {
362 username: String,
363 combined_nonce: String,
364 client_first_bare: String,
365 server_first: String,
366 verifier: ScramVerifier,
367 user_known: bool,
368}
369
370impl ScramServerHandshake {
371 pub fn new(server_nonce: impl Into<String>, unknown_user_salt: Vec<u8>) -> Self {
376 Self {
377 server_nonce: server_nonce.into(),
378 unknown_user_salt,
379 state: ScramServerState::AwaitClientFirst,
380 }
381 }
382
383 pub fn step(&mut self, input: ScramServerInput) -> ScramServerOutput {
384 let state = std::mem::replace(&mut self.state, ScramServerState::Complete);
385 match (state, input) {
386 (
387 ScramServerState::AwaitClientFirst,
388 ScramServerInput::ClientMessage {
389 correlation_id,
390 kind,
391 payload,
392 },
393 ) => self.step_receive_client_first(correlation_id, kind, &payload),
394 (
395 ScramServerState::AwaitVerifier {
396 correlation_id,
397 username,
398 client_nonce,
399 client_first_bare,
400 },
401 ScramServerInput::Verifier(verifier),
402 ) => self.step_receive_verifier(
403 correlation_id,
404 username,
405 client_nonce,
406 client_first_bare,
407 verifier,
408 ),
409 (
410 ScramServerState::AwaitClientFinal(final_state),
411 ScramServerInput::ClientMessage {
412 correlation_id,
413 kind,
414 payload,
415 },
416 ) => Self::step_receive_client_final(correlation_id, kind, &payload, final_state),
417 (_, ScramServerInput::ClientMessage { correlation_id, .. }) => {
418 ScramServerOutput::Failed {
419 correlation_id,
420 reason: "unexpected SCRAM client message".to_string(),
421 }
422 }
423 (_, ScramServerInput::Verifier(_)) => ScramServerOutput::Failed {
424 correlation_id: 0,
425 reason: "unexpected SCRAM verifier".to_string(),
426 },
427 }
428 }
429
430 fn step_receive_client_first(
431 &mut self,
432 correlation_id: u64,
433 kind: MessageKind,
434 payload: &[u8],
435 ) -> ScramServerOutput {
436 let payload =
437 match expect_auth_response_payload(kind, payload, "AuthResponse(client-first-message)")
438 {
439 Ok(payload) => payload,
440 Err(error) => return failure(correlation_id, error.to_string()),
441 };
442 let (username, client_nonce, client_first_bare) = match parse_scram_client_first(payload) {
443 Ok(parsed) => parsed,
444 Err(error) => return failure(correlation_id, format!("scram client-first: {error}")),
445 };
446 self.state = ScramServerState::AwaitVerifier {
447 correlation_id,
448 username: username.clone(),
449 client_nonce,
450 client_first_bare,
451 };
452 ScramServerOutput::NeedVerifier { username }
453 }
454
455 fn step_receive_verifier(
456 &mut self,
457 correlation_id: u64,
458 username: String,
459 client_nonce: String,
460 client_first_bare: String,
461 verifier: Option<ScramVerifier>,
462 ) -> ScramServerOutput {
463 let user_known = verifier.is_some();
464 let verifier = verifier.unwrap_or_else(|| ScramVerifier {
465 salt: self.unknown_user_salt.clone(),
466 iter: DEFAULT_ITER,
467 stored_key: [0; 32],
468 server_key: [0; 32],
469 });
470 let server_first = build_scram_server_first(
471 &client_nonce,
472 &self.server_nonce,
473 &verifier.salt,
474 verifier.iter,
475 );
476 let combined_nonce = format!("{client_nonce}{}", self.server_nonce);
477 self.state = ScramServerState::AwaitClientFinal(ScramClientFinalState {
478 username,
479 combined_nonce,
480 client_first_bare,
481 server_first: server_first.clone(),
482 verifier,
483 user_known,
484 });
485 ScramServerOutput::Challenge {
486 correlation_id,
487 payload: server_first.into_bytes(),
488 }
489 }
490
491 fn step_receive_client_final(
492 correlation_id: u64,
493 kind: MessageKind,
494 payload: &[u8],
495 state: ScramClientFinalState,
496 ) -> ScramServerOutput {
497 let payload =
498 match expect_auth_response_payload(kind, payload, "AuthResponse(client-final-message)")
499 {
500 Ok(payload) => payload,
501 Err(error) => return failure(correlation_id, error.to_string()),
502 };
503 let (combined_nonce, presented_proof, client_final_no_proof) =
504 match parse_scram_client_final(payload) {
505 Ok(parsed) => parsed,
506 Err(error) => {
507 return failure(correlation_id, format!("scram client-final: {error}"))
508 }
509 };
510 if combined_nonce != state.combined_nonce {
511 return failure(
512 correlation_id,
513 "scram nonce mismatch — replay protection failed".to_string(),
514 );
515 }
516 let auth_message = auth_message(
517 &state.client_first_bare,
518 &state.server_first,
519 &client_final_no_proof,
520 );
521 if !state.user_known
522 || !verify_client_proof(&state.verifier, &auth_message, &presented_proof)
523 {
524 return failure(correlation_id, "invalid SCRAM proof".to_string());
525 }
526 ScramServerOutput::Authenticated {
527 correlation_id,
528 username: state.username,
529 server_signature: server_signature(&state.verifier.server_key, &auth_message),
530 }
531 }
532}
533
534fn failure(correlation_id: u64, reason: String) -> ScramServerOutput {
535 ScramServerOutput::Failed {
536 correlation_id,
537 reason,
538 }
539}
540
541pub fn salted_password(password: &[u8], salt: &[u8], iter: u32) -> [u8; 32] {
543 let mut salt_with_block = Vec::with_capacity(salt.len() + 4);
544 salt_with_block.extend_from_slice(salt);
545 salt_with_block.extend_from_slice(&1u32.to_be_bytes());
546
547 let mut round = hmac_sha256(password, &salt_with_block);
548 let mut result = round;
549 for _ in 1..iter {
550 round = hmac_sha256(password, &round);
551 for (result_byte, round_byte) in result.iter_mut().zip(round) {
552 *result_byte ^= round_byte;
553 }
554 }
555 result
556}
557
558pub fn hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32] {
559 type HmacSha256 = Hmac<Sha256>;
560 let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts keys of any length");
561 mac.update(data);
562 mac.finalize().into_bytes().into()
563}
564
565pub fn sha256(data: &[u8]) -> [u8; 32] {
566 Sha256::digest(data).into()
567}
568
569pub fn xor(left: &[u8], right: &[u8]) -> Vec<u8> {
570 assert_eq!(
571 left.len(),
572 right.len(),
573 "SCRAM XOR operands must have equal lengths"
574 );
575 left.iter()
576 .zip(right)
577 .map(|(left_byte, right_byte)| left_byte ^ right_byte)
578 .collect()
579}
580
581pub fn auth_message(
582 client_first_bare: &str,
583 server_first: &str,
584 client_final_no_proof: &str,
585) -> Vec<u8> {
586 let mut message = Vec::with_capacity(
587 client_first_bare.len() + server_first.len() + client_final_no_proof.len() + 2,
588 );
589 message.extend_from_slice(client_first_bare.as_bytes());
590 message.push(b',');
591 message.extend_from_slice(server_first.as_bytes());
592 message.push(b',');
593 message.extend_from_slice(client_final_no_proof.as_bytes());
594 message
595}
596
597pub fn client_proof(stored_key: &[u8], auth_message: &[u8], client_key: &[u8]) -> Vec<u8> {
598 xor(client_key, &hmac_sha256(stored_key, auth_message))
599}
600
601pub fn verify_client_proof(
602 verifier: &ScramVerifier,
603 auth_message: &[u8],
604 presented_proof: &[u8],
605) -> bool {
606 if presented_proof.len() != 32 {
607 return false;
608 }
609 let client_signature = hmac_sha256(&verifier.stored_key, auth_message);
610 let client_key = xor(presented_proof, &client_signature);
611 constant_time_eq(&sha256(&client_key), &verifier.stored_key)
612}
613
614pub fn server_signature(server_key: &[u8], auth_message: &[u8]) -> [u8; 32] {
615 hmac_sha256(server_key, auth_message)
616}
617
618pub fn verify_server_signature(
619 server_key: &[u8],
620 auth_message: &[u8],
621 presented_signature: &[u8],
622) -> bool {
623 presented_signature.len() == 32
624 && constant_time_eq(
625 &server_signature(server_key, auth_message),
626 presented_signature,
627 )
628}
629
630fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
631 let len = left.len().max(right.len());
634 let mut difference = u8::from(left.len() != right.len());
635 for i in 0..len {
636 let l = left.get(i).copied().unwrap_or(0);
637 let r = right.get(i).copied().unwrap_or(0);
638 difference |= l ^ r;
639 }
640 std::hint::black_box(difference) == 0
641}