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