1use core::{fmt, mem};
49
50use alloc::{
51 format,
52 string::{String, ToString},
53 vec,
54 vec::Vec,
55};
56
57use base64::{Engine, engine::general_purpose::STANDARD};
58use hmac::{Hmac, KeyInit, Mac};
59use imap_codec::{
60 AuthenticateDataCodec, CommandCodec,
61 fragmentizer::Fragmentizer,
62 imap_types::{
63 auth::{AuthMechanism, AuthenticateData},
64 command::{Command, CommandBody},
65 core::{IString, NString, TagGenerator},
66 response::{
67 Capability, Code, CommandContinuationRequest, Data, StatusBody, StatusKind, Tagged,
68 },
69 secret::Secret,
70 },
71};
72use log::{debug, trace};
73use rand::{RngExt, distr::Alphanumeric};
74use sha2::{Digest, Sha256};
75use thiserror::Error;
76
77use crate::{coroutine::*, imap_try, rfc2971::id::*, rfc3501::capability::*, send::*};
78
79type HmacSha256 = Hmac<Sha256>;
80
81#[derive(Clone, Debug, Error)]
83pub enum ImapAuthScramSha256Error {
84 #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: NO {0}")]
86 No(String),
87 #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: BAD {0}")]
89 Bad(String),
90 #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: BYE {0}")]
92 Bye(String),
93 #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: server did not return a tagged response")]
95 MissingTagged,
96 #[error(
98 "IMAP AUTHENTICATE SCRAM-SHA-256 failed: server did not send the expected continuation request"
99 )]
100 ExpectedContinuationRequest,
101 #[error(
103 "IMAP AUTHENTICATE SCRAM-SHA-256 failed: server returned OK before the mechanism could complete"
104 )]
105 UnexpectedOk,
106 #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: invalid server message encoding")]
108 InvalidEncoding,
109 #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: server-first-message missing nonce")]
111 MissingNonce,
112 #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: server-first-message missing salt")]
114 MissingSalt,
115 #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: server-first-message missing iteration count")]
117 MissingIterations,
118 #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: invalid base64 in server message")]
120 InvalidBase64,
121 #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: invalid iteration count")]
124 InvalidIterationCount,
125 #[error(
128 "IMAP AUTHENTICATE SCRAM-SHA-256 failed: server nonce does not start with client nonce"
129 )]
130 NonceMismatch,
131 #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: server signature verification failed")]
134 ServerSignatureMismatch,
135 #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: server error: {0}")]
137 ServerError(String),
138 #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: invalid server-final-message")]
140 InvalidServerFinal,
141 #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: {0}")]
143 Send(#[from] ImapSendError),
144 #[error(transparent)]
146 Capability(#[from] ImapCapabilityGetError),
147 #[error(transparent)]
149 ServerId(#[from] ImapServerIdError),
150}
151
152#[derive(Clone, Debug, Default, Eq, PartialEq)]
154pub struct ImapAuthScramSha256Options {
155 pub initial_request: bool,
158 pub ensure_capabilities: bool,
161 pub auto_id: Option<Vec<(IString<'static>, NString<'static>)>>,
166}
167
168pub struct ImapAuthScramSha256 {
170 state: State,
171 password: Vec<u8>,
172 client_first_bare: String,
173 client_nonce: String,
174 observed: Vec<Capability<'static>>,
175 expected_server_signature: Option<Vec<u8>>,
176 opts: ImapAuthScramSha256Options,
177}
178
179impl ImapAuthScramSha256 {
180 pub fn new(
187 user: impl AsRef<str>,
188 password: impl AsRef<str>,
189 opts: ImapAuthScramSha256Options,
190 ) -> Self {
191 let user = user.as_ref();
192 let password = password.as_ref().as_bytes().to_vec();
193 let client_nonce = generate_nonce();
194 let escaped = escape_username(user);
195 let client_first_bare = format!("n={escaped},r={client_nonce}");
196 let client_first_message = format!("n,,{client_first_bare}");
197 let tag = TagGenerator::new().generate();
198
199 let state = if opts.initial_request {
200 let body = CommandBody::Authenticate {
201 mechanism: AuthMechanism::ScramSha256,
202 initial_response: Some(Secret::new(client_first_message.into_bytes().into())),
203 };
204 let cmd = Command { tag, body };
205 trace!("send IMAP command {cmd:?}");
206 State::SendIr(ImapSend::new(CommandCodec::new(), cmd))
207 } else {
208 let body = CommandBody::Authenticate {
209 mechanism: AuthMechanism::ScramSha256,
210 initial_response: None,
211 };
212 let cmd = Command { tag, body };
213 trace!("send IMAP command {cmd:?}");
214 State::Send {
215 send: ImapSend::new(CommandCodec::new(), cmd),
216 client_first_message,
217 }
218 };
219
220 Self {
221 state,
222 password,
223 client_first_bare,
224 client_nonce,
225 observed: Vec::new(),
226 expected_server_signature: None,
227 opts,
228 }
229 }
230
231 fn wants_capability(
232 &mut self,
233 code: Option<Code<'static>>,
234 data: Vec<Data<'static>>,
235 untagged: Vec<StatusBody<'static>>,
236 ) -> Option<State> {
237 let mut new_capability = None;
238
239 if let Some(Code::Capability(capability)) = code {
240 new_capability.replace(capability);
241 }
242
243 for data in data {
244 if let Data::Capability(capability) = data {
245 new_capability.replace(capability);
246 }
247 }
248
249 for StatusBody { code, .. } in untagged {
250 if let Some(Code::Capability(capability)) = code {
251 new_capability.replace(capability);
252 }
253 }
254
255 if let Some(capability) = new_capability {
256 self.observed = capability.into_iter().collect();
257 }
258
259 (self.opts.ensure_capabilities && self.observed.is_empty())
260 .then(|| State::Capability(ImapCapabilityGet::new()))
261 }
262
263 fn wants_id(&mut self) -> Option<State> {
264 let params = self.opts.auto_id.take()?;
265 let wire = (!params.is_empty()).then_some(params);
266 Some(State::Id(ImapServerId::new(ImapServerIdOptions {
267 parameters: wire,
268 })))
269 }
270
271 fn build_client_final(
272 &mut self,
273 server_first_bytes: &[u8],
274 ) -> Result<ImapSend<AuthenticateDataCodec>, ImapAuthScramSha256Error> {
275 let server_first = String::from_utf8(server_first_bytes.to_vec())
276 .map_err(|_| ImapAuthScramSha256Error::InvalidEncoding)?;
277
278 let (nonce, salt, iterations) = parse_server_first(&server_first, &self.client_nonce)?;
279
280 let client_final_without_proof = format!("c=biws,r={nonce}");
282
283 let auth_message = format!(
284 "{},{},{}",
285 self.client_first_bare, server_first, client_final_without_proof,
286 );
287
288 let (client_proof, server_signature) =
289 compute_scram_sha256(&self.password, &salt, iterations, auth_message.as_bytes());
290
291 self.expected_server_signature = Some(server_signature);
292
293 let client_final = format!(
294 "{},p={}",
295 client_final_without_proof,
296 STANDARD.encode(&client_proof),
297 );
298
299 let auth = AuthenticateData::r#continue(client_final.into_bytes());
300 Ok(ImapSend::new(AuthenticateDataCodec::new(), auth))
301 }
302
303 fn verify_server_final(
304 &self,
305 server_final_bytes: &[u8],
306 ) -> Result<(), ImapAuthScramSha256Error> {
307 let server_final = String::from_utf8(server_final_bytes.to_vec())
308 .map_err(|_| ImapAuthScramSha256Error::InvalidEncoding)?;
309
310 if let Some(e) = server_final.strip_prefix("e=") {
311 return Err(ImapAuthScramSha256Error::ServerError(e.to_string()));
312 }
313
314 let v = server_final
315 .strip_prefix("v=")
316 .ok_or(ImapAuthScramSha256Error::InvalidServerFinal)?;
317
318 let server_sig = STANDARD
319 .decode(v)
320 .map_err(|_| ImapAuthScramSha256Error::InvalidBase64)?;
321
322 let expected = self
323 .expected_server_signature
324 .as_ref()
325 .ok_or(ImapAuthScramSha256Error::InvalidServerFinal)?;
326
327 if server_sig != *expected {
328 return Err(ImapAuthScramSha256Error::ServerSignatureMismatch);
329 }
330
331 Ok(())
332 }
333}
334
335impl ImapCoroutine for ImapAuthScramSha256 {
336 type Yield = ImapYield;
337 type Return = Result<Vec<Capability<'static>>, ImapAuthScramSha256Error>;
338
339 fn resume(
340 &mut self,
341 fragmentizer: &mut Fragmentizer,
342 arg: Option<&[u8]>,
343 ) -> ImapCoroutineState<Self::Yield, Self::Return> {
344 loop {
345 match &mut self.state {
346 State::Send {
347 send,
348 client_first_message,
349 } => {
350 let out = imap_try!(send, fragmentizer, arg);
351
352 if let Some(bye) = out.bye {
353 let err = ImapAuthScramSha256Error::Bye(bye.text.to_string());
354 return ImapCoroutineState::Complete(Err(err));
355 }
356
357 if out.continuation_request.is_some() {
358 let payload = mem::take(client_first_message).into_bytes();
359 let auth = AuthenticateData::r#continue(payload);
360 let codec = AuthenticateDataCodec::new();
361 self.state = State::SendClientFirst(ImapSend::new(codec, auth));
362 debug!("{}", self.state);
363 continue;
364 }
365
366 if let Some(Tagged { body, .. }) = out.tagged {
367 let err = match body.kind {
368 StatusKind::Ok => ImapAuthScramSha256Error::UnexpectedOk,
369 StatusKind::No => ImapAuthScramSha256Error::No(body.text.to_string()),
370 StatusKind::Bad => ImapAuthScramSha256Error::Bad(body.text.to_string()),
371 };
372
373 return ImapCoroutineState::Complete(Err(err));
374 }
375
376 let err = ImapAuthScramSha256Error::ExpectedContinuationRequest;
377 return ImapCoroutineState::Complete(Err(err));
378 }
379 State::SendIr(send) => {
380 let out = imap_try!(send, fragmentizer, arg);
381
382 if let Some(bye) = out.bye {
383 let err = ImapAuthScramSha256Error::Bye(bye.text.to_string());
384 return ImapCoroutineState::Complete(Err(err));
385 }
386
387 if let Some(cr) = out.continuation_request {
388 let challenge = extract_challenge(cr);
389 let send = match self.build_client_final(&challenge) {
390 Ok(s) => s,
391 Err(err) => return ImapCoroutineState::Complete(Err(err)),
392 };
393 self.state = State::SendClientFinal(send);
394 debug!("{}", self.state);
395 continue;
396 }
397
398 if let Some(Tagged { body, .. }) = out.tagged {
399 let err = match body.kind {
400 StatusKind::Ok => ImapAuthScramSha256Error::UnexpectedOk,
401 StatusKind::No => ImapAuthScramSha256Error::No(body.text.to_string()),
402 StatusKind::Bad => ImapAuthScramSha256Error::Bad(body.text.to_string()),
403 };
404
405 return ImapCoroutineState::Complete(Err(err));
406 }
407
408 let err = ImapAuthScramSha256Error::ExpectedContinuationRequest;
409 return ImapCoroutineState::Complete(Err(err));
410 }
411 State::SendClientFirst(send) => {
412 let out = imap_try!(send, fragmentizer, arg);
413
414 if let Some(bye) = out.bye {
415 let err = ImapAuthScramSha256Error::Bye(bye.text.to_string());
416 return ImapCoroutineState::Complete(Err(err));
417 }
418
419 if let Some(cr) = out.continuation_request {
420 let challenge = extract_challenge(cr);
421 let send = match self.build_client_final(&challenge) {
422 Ok(s) => s,
423 Err(err) => return ImapCoroutineState::Complete(Err(err)),
424 };
425 self.state = State::SendClientFinal(send);
426 debug!("{}", self.state);
427 continue;
428 }
429
430 if let Some(Tagged { body, .. }) = out.tagged {
431 let err = match body.kind {
432 StatusKind::Ok => ImapAuthScramSha256Error::UnexpectedOk,
433 StatusKind::No => ImapAuthScramSha256Error::No(body.text.to_string()),
434 StatusKind::Bad => ImapAuthScramSha256Error::Bad(body.text.to_string()),
435 };
436
437 return ImapCoroutineState::Complete(Err(err));
438 }
439
440 let err = ImapAuthScramSha256Error::ExpectedContinuationRequest;
441 return ImapCoroutineState::Complete(Err(err));
442 }
443 State::SendClientFinal(send) => {
444 let out = imap_try!(send, fragmentizer, arg);
445
446 if let Some(bye) = out.bye {
447 let err = ImapAuthScramSha256Error::Bye(bye.text.to_string());
448 return ImapCoroutineState::Complete(Err(err));
449 }
450
451 if let Some(cr) = out.continuation_request {
452 let challenge = extract_challenge(cr);
453 if let Err(err) = self.verify_server_final(&challenge) {
454 return ImapCoroutineState::Complete(Err(err));
455 }
456
457 let auth = AuthenticateData::r#continue(vec![]);
458 let codec = AuthenticateDataCodec::new();
459 self.state = State::Acknowledge(ImapSend::new(codec, auth));
460 debug!("{}", self.state);
461 continue;
462 }
463
464 let Some(Tagged { body, .. }) = out.tagged else {
467 let err = ImapAuthScramSha256Error::MissingTagged;
468 return ImapCoroutineState::Complete(Err(err));
469 };
470
471 let code = match body.kind {
472 StatusKind::Ok => body.code,
473 StatusKind::No => {
474 let err = ImapAuthScramSha256Error::No(body.text.to_string());
475 return ImapCoroutineState::Complete(Err(err));
476 }
477 StatusKind::Bad => {
478 let err = ImapAuthScramSha256Error::Bad(body.text.to_string());
479 return ImapCoroutineState::Complete(Err(err));
480 }
481 };
482
483 if let Some(next) = self.wants_capability(code, out.data, out.untagged) {
484 self.state = next;
485 debug!("{}", self.state);
486 continue;
487 }
488
489 if let Some(next) = self.wants_id() {
490 self.state = next;
491 debug!("{}", self.state);
492 continue;
493 }
494
495 let capability = mem::take(&mut self.observed);
496 return ImapCoroutineState::Complete(Ok(capability));
497 }
498 State::Acknowledge(send) => {
499 let out = imap_try!(send, fragmentizer, arg);
500
501 if let Some(bye) = out.bye {
502 let err = ImapAuthScramSha256Error::Bye(bye.text.to_string());
503 return ImapCoroutineState::Complete(Err(err));
504 }
505
506 let Some(Tagged { body, .. }) = out.tagged else {
507 let err = ImapAuthScramSha256Error::MissingTagged;
508 return ImapCoroutineState::Complete(Err(err));
509 };
510
511 let code = match body.kind {
512 StatusKind::Ok => body.code,
513 StatusKind::No => {
514 let err = ImapAuthScramSha256Error::No(body.text.to_string());
515 return ImapCoroutineState::Complete(Err(err));
516 }
517 StatusKind::Bad => {
518 let err = ImapAuthScramSha256Error::Bad(body.text.to_string());
519 return ImapCoroutineState::Complete(Err(err));
520 }
521 };
522
523 if let Some(next) = self.wants_capability(code, out.data, out.untagged) {
524 self.state = next;
525 debug!("{}", self.state);
526 continue;
527 }
528
529 if let Some(next) = self.wants_id() {
530 self.state = next;
531 debug!("{}", self.state);
532 continue;
533 }
534
535 let capability = mem::take(&mut self.observed);
536 return ImapCoroutineState::Complete(Ok(capability));
537 }
538 State::Capability(capability) => {
539 self.observed = imap_try!(capability, fragmentizer, arg);
540
541 if let Some(next) = self.wants_id() {
542 self.state = next;
543 debug!("{}", self.state);
544 continue;
545 }
546
547 let capability = mem::take(&mut self.observed);
548 return ImapCoroutineState::Complete(Ok(capability));
549 }
550 State::Id(id) => {
551 imap_try!(id, fragmentizer, arg);
552 let capability = mem::take(&mut self.observed);
553 return ImapCoroutineState::Complete(Ok(capability));
554 }
555 }
556 }
557 }
558}
559
560enum State {
561 Send {
562 send: ImapSend<CommandCodec>,
563 client_first_message: String,
564 },
565 SendIr(ImapSend<CommandCodec>),
566 SendClientFirst(ImapSend<AuthenticateDataCodec>),
567 SendClientFinal(ImapSend<AuthenticateDataCodec>),
568 Acknowledge(ImapSend<AuthenticateDataCodec>),
569 Capability(ImapCapabilityGet),
570 Id(ImapServerId),
571}
572
573impl fmt::Display for State {
574 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
575 match self {
576 Self::Send { .. } => f.write_str("send auth"),
577 Self::SendIr(_) => f.write_str("send auth with ir"),
578 Self::SendClientFirst(_) => f.write_str("send client-first"),
579 Self::SendClientFinal(_) => f.write_str("send client-final"),
580 Self::Acknowledge(_) => f.write_str("acknowledge server-final"),
581 Self::Capability(_) => f.write_str("fetch capabilities"),
582 Self::Id(_) => f.write_str("send id"),
583 }
584 }
585}
586
587fn escape_username(username: &str) -> String {
588 username.replace('=', "=3D").replace(',', "=2C")
589}
590
591fn generate_nonce() -> String {
592 rand::rng()
593 .sample_iter(&Alphanumeric)
594 .take(24)
595 .map(char::from)
596 .collect()
597}
598
599fn extract_challenge(cr: CommandContinuationRequest<'static>) -> Vec<u8> {
600 match cr {
601 CommandContinuationRequest::Base64(data) => data.as_ref().to_vec(),
602 CommandContinuationRequest::Basic(_) => vec![],
603 }
604}
605
606fn parse_server_first(
607 msg: &str,
608 client_nonce: &str,
609) -> Result<(String, Vec<u8>, u32), ImapAuthScramSha256Error> {
610 let mut nonce = None;
611 let mut salt = None;
612 let mut iterations = None;
613
614 for part in msg.split(',') {
615 if let Some(r) = part.strip_prefix("r=") {
616 nonce = Some(r.to_string());
617 } else if let Some(s) = part.strip_prefix("s=") {
618 salt = Some(
619 STANDARD
620 .decode(s)
621 .map_err(|_| ImapAuthScramSha256Error::InvalidBase64)?,
622 );
623 } else if let Some(i) = part.strip_prefix("i=") {
624 iterations = Some(
625 i.parse::<u32>()
626 .map_err(|_| ImapAuthScramSha256Error::InvalidIterationCount)?,
627 );
628 }
629 }
630
631 let nonce = nonce.ok_or(ImapAuthScramSha256Error::MissingNonce)?;
632 let salt = salt.ok_or(ImapAuthScramSha256Error::MissingSalt)?;
633 let iterations = iterations.ok_or(ImapAuthScramSha256Error::MissingIterations)?;
634
635 if !nonce.starts_with(client_nonce) {
636 return Err(ImapAuthScramSha256Error::NonceMismatch);
637 }
638
639 Ok((nonce, salt, iterations))
640}
641
642fn compute_scram_sha256(
643 password: &[u8],
644 salt: &[u8],
645 iterations: u32,
646 auth_message: &[u8],
647) -> (Vec<u8>, Vec<u8>) {
648 let mut salted_password = [0u8; 32];
652 pbkdf2::pbkdf2_hmac::<Sha256>(password, salt, iterations, &mut salted_password);
653
654 let mut mac = HmacSha256::new_from_slice(&salted_password).unwrap();
656 mac.update(b"Client Key");
657 let client_key = mac.finalize().into_bytes();
658
659 let stored_key = Sha256::digest(client_key);
661
662 let mut mac = HmacSha256::new_from_slice(&stored_key).unwrap();
664 mac.update(auth_message);
665 let client_signature = mac.finalize().into_bytes();
666
667 let client_proof: Vec<u8> = client_key
669 .iter()
670 .zip(client_signature.iter())
671 .map(|(a, b)| a ^ b)
672 .collect();
673
674 let mut mac = HmacSha256::new_from_slice(&salted_password).unwrap();
676 mac.update(b"Server Key");
677 let server_key = mac.finalize().into_bytes();
678
679 let mut mac = HmacSha256::new_from_slice(&server_key).unwrap();
681 mac.update(auth_message);
682 let server_signature = mac.finalize().into_bytes();
683
684 (client_proof, server_signature.to_vec())
685}
686
687#[cfg(test)]
688mod tests {
689 use core::str;
690
691 use alloc::borrow::ToOwned;
692
693 use crate::rfc7677::auth_scram_sha_256::*;
694
695 #[test]
696 fn ir_success_returns_ok() {
697 let opts = ImapAuthScramSha256Options {
698 initial_request: true,
699 ..Default::default()
700 };
701
702 let mut auth = ImapAuthScramSha256::new("alice", "secret", opts);
703 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
704
705 let bytes = expect_wants_write(&mut auth, &mut frag, None);
706 let line = str::from_utf8(&bytes).expect("utf8 command");
707 let tag = first_word(line).to_owned();
708 let client_first = decode_last_base64_token(line);
709 let client_nonce = extract_client_nonce(&client_first);
710
711 expect_wants_read(&mut auth, &mut frag);
712
713 let server_first = format!("r={client_nonce}ServerExtra,s={SALT_B64},i={ITERATIONS}");
714 let challenge = format!("+ {}\r\n", STANDARD.encode(&server_first));
715 let client_final_bytes =
716 expect_wants_write(&mut auth, &mut frag, Some(challenge.as_bytes()));
717 let client_final_line = str::from_utf8(&client_final_bytes).expect("utf8");
718 let client_final = decode_last_base64_token(client_final_line.trim_end());
719
720 expect_wants_read(&mut auth, &mut frag);
721
722 let server_final = build_server_final(&client_first, &server_first, &client_final);
723 let challenge2 = format!("+ {}\r\n", STANDARD.encode(&server_final));
724 let ack = expect_wants_write(&mut auth, &mut frag, Some(challenge2.as_bytes()));
725 assert_eq!(b"\r\n", &*ack);
726
727 expect_wants_read(&mut auth, &mut frag);
728
729 let reply = format!("{tag} OK AUTHENTICATE completed\r\n");
730 expect_complete_ok(&mut auth, &mut frag, reply.as_bytes());
731 }
732
733 #[test]
734 fn ir_server_error_returns_server_error() {
735 let opts = ImapAuthScramSha256Options {
736 initial_request: true,
737 ..Default::default()
738 };
739
740 let mut auth = ImapAuthScramSha256::new("alice", "secret", opts);
741 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
742
743 let bytes = expect_wants_write(&mut auth, &mut frag, None);
744 let client_first = decode_last_base64_token(str::from_utf8(&bytes).expect("utf8"));
745 let client_nonce = extract_client_nonce(&client_first);
746
747 expect_wants_read(&mut auth, &mut frag);
748
749 let server_first = format!("r={client_nonce}ServerExtra,s={SALT_B64},i={ITERATIONS}");
750 let challenge = format!("+ {}\r\n", STANDARD.encode(&server_first));
751 let _client_final = expect_wants_write(&mut auth, &mut frag, Some(challenge.as_bytes()));
752
753 expect_wants_read(&mut auth, &mut frag);
754
755 let server_final = "e=invalid-proof";
756 let challenge2 = format!("+ {}\r\n", STANDARD.encode(server_final));
757 let err = match auth.resume(&mut frag, Some(challenge2.as_bytes())) {
758 ImapCoroutineState::Complete(Err(err)) => err,
759 state => panic!("expected Complete(Err), got {state:?}"),
760 };
761 let ImapAuthScramSha256Error::ServerError(text) = err else {
762 panic!("expected ImapAuthScramSha256Error::ServerError, got {err:?}");
763 };
764 assert_eq!(text, "invalid-proof");
765 }
766
767 #[test]
768 fn ir_tagged_bad_returns_bad_error() {
769 let opts = ImapAuthScramSha256Options {
770 initial_request: true,
771 ..Default::default()
772 };
773
774 let mut auth = ImapAuthScramSha256::new("alice", "secret", opts);
775 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
776
777 let bytes = expect_wants_write(&mut auth, &mut frag, None);
778 let tag = first_word(str::from_utf8(&bytes).expect("utf8"));
779
780 expect_wants_read(&mut auth, &mut frag);
781
782 let reply = format!("{tag} BAD AUTHENTICATE not enabled\r\n");
783 let err = expect_complete_err(&mut auth, &mut frag, reply.as_bytes());
784 let ImapAuthScramSha256Error::Bad(text) = err else {
785 panic!("expected ImapAuthScramSha256Error::Bad, got {err:?}");
786 };
787 assert_eq!(text, "AUTHENTICATE not enabled");
788 }
789
790 #[test]
791 fn non_ir_success_returns_ok() {
792 let opts = ImapAuthScramSha256Options::default();
793 let mut auth = ImapAuthScramSha256::new("alice", "secret", opts);
794 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
795
796 let bytes = expect_wants_write(&mut auth, &mut frag, None);
797 let line = str::from_utf8(&bytes).expect("utf8 command");
798 let tag = first_word(line).to_owned();
799 assert!(line.trim_end().ends_with("AUTHENTICATE SCRAM-SHA-256"));
800
801 expect_wants_read(&mut auth, &mut frag);
802
803 let client_first_bytes = expect_wants_write(&mut auth, &mut frag, Some(b"+ \r\n"));
804 let client_first = decode_last_base64_token(
805 str::from_utf8(&client_first_bytes)
806 .expect("utf8")
807 .trim_end(),
808 );
809 let client_nonce = extract_client_nonce(&client_first);
810
811 expect_wants_read(&mut auth, &mut frag);
812
813 let server_first = format!("r={client_nonce}ServerExtra,s={SALT_B64},i={ITERATIONS}");
814 let challenge = format!("+ {}\r\n", STANDARD.encode(&server_first));
815 let client_final_bytes =
816 expect_wants_write(&mut auth, &mut frag, Some(challenge.as_bytes()));
817 let client_final = decode_last_base64_token(
818 str::from_utf8(&client_final_bytes)
819 .expect("utf8")
820 .trim_end(),
821 );
822
823 expect_wants_read(&mut auth, &mut frag);
824
825 let server_final = build_server_final(&client_first, &server_first, &client_final);
826 let challenge2 = format!("+ {}\r\n", STANDARD.encode(&server_final));
827 let ack = expect_wants_write(&mut auth, &mut frag, Some(challenge2.as_bytes()));
828 assert_eq!(b"\r\n", &*ack);
829
830 expect_wants_read(&mut auth, &mut frag);
831
832 let reply = format!("{tag} OK AUTHENTICATE completed\r\n");
833 expect_complete_ok(&mut auth, &mut frag, reply.as_bytes());
834 }
835
836 #[test]
837 fn non_ir_server_error_returns_server_error() {
838 let opts = ImapAuthScramSha256Options::default();
839 let mut auth = ImapAuthScramSha256::new("alice", "secret", opts);
840 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
841
842 let bytes = expect_wants_write(&mut auth, &mut frag, None);
843 let _tag = first_word(str::from_utf8(&bytes).expect("utf8"));
844
845 expect_wants_read(&mut auth, &mut frag);
846
847 let client_first_bytes = expect_wants_write(&mut auth, &mut frag, Some(b"+ \r\n"));
848 let client_first = decode_last_base64_token(
849 str::from_utf8(&client_first_bytes)
850 .expect("utf8")
851 .trim_end(),
852 );
853 let client_nonce = extract_client_nonce(&client_first);
854
855 expect_wants_read(&mut auth, &mut frag);
856
857 let server_first = format!("r={client_nonce}ServerExtra,s={SALT_B64},i={ITERATIONS}");
858 let challenge = format!("+ {}\r\n", STANDARD.encode(&server_first));
859 let _client_final = expect_wants_write(&mut auth, &mut frag, Some(challenge.as_bytes()));
860
861 expect_wants_read(&mut auth, &mut frag);
862
863 let server_final = "e=invalid-proof";
864 let challenge2 = format!("+ {}\r\n", STANDARD.encode(server_final));
865 let err = match auth.resume(&mut frag, Some(challenge2.as_bytes())) {
866 ImapCoroutineState::Complete(Err(err)) => err,
867 state => panic!("expected Complete(Err), got {state:?}"),
868 };
869 let ImapAuthScramSha256Error::ServerError(text) = err else {
870 panic!("expected ImapAuthScramSha256Error::ServerError, got {err:?}");
871 };
872 assert_eq!(text, "invalid-proof");
873 }
874
875 const SALT_B64: &str = "QSXCR+Q6sek8bf92";
876 const ITERATIONS: u32 = 4096;
877
878 fn expect_wants_write(
879 cor: &mut ImapAuthScramSha256,
880 frag: &mut Fragmentizer,
881 arg: Option<&[u8]>,
882 ) -> Vec<u8> {
883 match cor.resume(frag, arg) {
884 ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
885 state => panic!("expected WantsWrite, got {state:?}"),
886 }
887 }
888
889 fn expect_wants_read(cor: &mut ImapAuthScramSha256, frag: &mut Fragmentizer) {
890 match cor.resume(frag, None) {
891 ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
892 state => panic!("expected WantsRead, got {state:?}"),
893 }
894 }
895
896 fn expect_complete_ok(cor: &mut ImapAuthScramSha256, frag: &mut Fragmentizer, reply: &[u8]) {
897 match cor.resume(frag, Some(reply)) {
898 ImapCoroutineState::Complete(Ok(_)) => {}
899 state => panic!("expected Complete(Ok), got {state:?}"),
900 }
901 }
902
903 fn expect_complete_err(
904 cor: &mut ImapAuthScramSha256,
905 frag: &mut Fragmentizer,
906 reply: &[u8],
907 ) -> ImapAuthScramSha256Error {
908 match cor.resume(frag, Some(reply)) {
909 ImapCoroutineState::Complete(Err(err)) => err,
910 state => panic!("expected Complete(Err), got {state:?}"),
911 }
912 }
913
914 fn first_word(line: &str) -> &str {
915 line.split_whitespace()
916 .next()
917 .expect("first whitespace-separated token")
918 }
919
920 fn decode_last_base64_token(line: &str) -> String {
921 let b64 = line
922 .trim_end()
923 .rsplit_terminator(char::is_whitespace)
924 .next()
925 .expect("token");
926 let bytes = STANDARD.decode(b64).expect("valid base64");
927 String::from_utf8(bytes).expect("valid utf8")
928 }
929
930 fn extract_client_nonce(client_first: &str) -> &str {
931 client_first
932 .rsplit_once("r=")
933 .expect("client-first has r=")
934 .1
935 }
936
937 fn build_server_final(client_first: &str, server_first: &str, client_final: &str) -> String {
938 let client_first_bare = client_first.strip_prefix("n,,").expect("gs2 header");
939 let client_final_without_proof = client_final
940 .rsplit_once(",p=")
941 .expect("client-final has p=")
942 .0;
943 let auth_message =
944 format!("{client_first_bare},{server_first},{client_final_without_proof}");
945 let salt = STANDARD.decode(SALT_B64).expect("valid salt");
946 let (_, server_sig) =
947 compute_scram_sha256(b"secret", &salt, ITERATIONS, auth_message.as_bytes());
948 format!("v={}", STANDARD.encode(server_sig))
949 }
950}