1use core::{fmt, mem};
58
59use alloc::{
60 string::{String, ToString},
61 vec,
62 vec::Vec,
63};
64
65use imap_codec::{
66 AuthenticateDataCodec, CommandCodec,
67 fragmentizer::Fragmentizer,
68 imap_types::{
69 auth::{AuthMechanism, AuthenticateData},
70 command::{Command, CommandBody},
71 core::{IString, NString, TagGenerator},
72 response::{
73 Capability, Code, CommandContinuationRequest, Data, StatusBody, StatusKind, Tagged,
74 },
75 secret::Secret,
76 },
77};
78use io_sasl::{
79 coroutine::*,
80 rfc4616::plain::{SaslPlain, SaslPlainCreds, SaslPlainError},
81};
82use log::{debug, trace};
83use secrecy::SecretString;
84use thiserror::Error;
85
86use crate::{coroutine::*, imap_try, rfc2971::id::*, rfc3501::capability::*, send::*};
87
88#[derive(Clone, Debug, Error)]
90pub enum ImapAuthPlainError {
91 #[error("IMAP AUTHENTICATE PLAIN failed: NO {0}")]
93 No(String),
94 #[error("IMAP AUTHENTICATE PLAIN failed: BAD {0}")]
96 Bad(String),
97 #[error("IMAP AUTHENTICATE PLAIN failed: BYE {0}")]
99 Bye(String),
100 #[error("IMAP AUTHENTICATE PLAIN failed: server did not return a tagged response")]
102 MissingTagged,
103 #[error(
105 "IMAP AUTHENTICATE PLAIN failed: server did not send the expected continuation request"
106 )]
107 ExpectedContinuationRequest,
108 #[error(
110 "IMAP AUTHENTICATE PLAIN failed: server returned OK before the mechanism could complete"
111 )]
112 UnexpectedOk,
113 #[error("IMAP AUTHENTICATE PLAIN failed: {0}")]
119 Mechanism(#[from] SaslPlainError),
120 #[error("IMAP AUTHENTICATE PLAIN failed: {0}")]
122 Send(#[from] ImapSendError),
123 #[error(transparent)]
125 Capability(#[from] ImapCapabilityGetError),
126 #[error(transparent)]
128 ServerId(#[from] ImapServerIdError),
129}
130
131#[derive(Clone, Debug, Default, Eq, PartialEq)]
133pub struct ImapAuthPlainOptions {
134 pub initial_request: bool,
137 pub ensure_capabilities: bool,
140 pub auto_id: Option<Vec<(IString<'static>, NString<'static>)>>,
145}
146
147pub struct ImapAuthPlain {
149 state: State,
150 mechanism: SaslPlain,
151 observed: Vec<Capability<'static>>,
152 opts: ImapAuthPlainOptions,
153}
154
155impl ImapAuthPlain {
156 pub fn new(
165 authzid: Option<impl AsRef<str>>,
166 authcid: impl AsRef<str>,
167 password: impl AsRef<str>,
168 opts: ImapAuthPlainOptions,
169 ) -> Self {
170 let mechanism = SaslPlain::new(SaslPlainCreds {
171 authzid: authzid.map(|zid| zid.as_ref().to_string()),
172 authcid: authcid.as_ref().to_string(),
173 passwd: SecretString::from(password.as_ref().to_string()),
174 });
175
176 Self {
177 state: State::Start,
178 mechanism,
179 observed: Vec::new(),
180 opts,
181 }
182 }
183
184 fn wants_capability(
187 &mut self,
188 code: Option<Code<'static>>,
189 data: Vec<Data<'static>>,
190 untagged: Vec<StatusBody<'static>>,
191 ) -> Option<State> {
192 let mut new_capability = None;
193
194 if let Some(Code::Capability(capability)) = code {
195 new_capability.replace(capability);
196 }
197
198 for data in data {
199 if let Data::Capability(capability) = data {
200 new_capability.replace(capability);
201 }
202 }
203
204 for StatusBody { code, .. } in untagged {
205 if let Some(Code::Capability(capability)) = code {
206 new_capability.replace(capability);
207 }
208 }
209
210 if let Some(capability) = new_capability {
211 self.observed = capability.into_iter().collect();
212 }
213
214 (self.opts.ensure_capabilities && self.observed.is_empty())
215 .then(|| State::Capability(ImapCapabilityGet::new()))
216 }
217
218 fn wants_id(&mut self) -> Option<State> {
220 let params = self.opts.auto_id.take()?;
221 let wire = (!params.is_empty()).then_some(params);
222 Some(State::Id(ImapServerId::new(ImapServerIdOptions {
223 parameters: wire,
224 })))
225 }
226
227 fn wants_continue(payload: Vec<u8>) -> State {
229 let auth = AuthenticateData::r#continue(payload);
230 let codec = AuthenticateDataCodec::new();
231 State::Continue(ImapSend::new(codec, auth))
232 }
233
234 fn resume_sasl(&mut self, arg: SaslArg<'_>) -> Result<Option<Vec<u8>>, ImapAuthPlainError> {
236 match self.mechanism.resume(arg) {
237 SaslCoroutineState::Yielded(SaslYield::WantsWrite(payload)) => Ok(Some(payload)),
238 SaslCoroutineState::Yielded(SaslYield::WantsRead) => Ok(None),
239 SaslCoroutineState::Complete(result) => result.map(|()| None).map_err(Into::into),
240 }
241 }
242}
243
244impl ImapCoroutine for ImapAuthPlain {
245 type Yield = ImapYield;
246 type Return = Result<Vec<Capability<'static>>, ImapAuthPlainError>;
247
248 fn resume(
249 &mut self,
250 fragmentizer: &mut Fragmentizer,
251 arg: Option<&[u8]>,
252 ) -> ImapCoroutineState<Self::Yield, Self::Return> {
253 loop {
254 match &mut self.state {
255 State::Start => {
256 let payload = match self.resume_sasl(SaslArg::None) {
257 Ok(payload) => payload,
258 Err(err) => return ImapCoroutineState::Complete(Err(err)),
259 };
260
261 let (initial_response, pending) = match payload {
266 Some(payload) if self.opts.initial_request => {
267 (Some(Secret::new(payload.into())), None)
268 }
269 payload => (None, payload),
270 };
271
272 let tag = TagGenerator::new().generate();
273 let body = CommandBody::Authenticate {
274 mechanism: AuthMechanism::Plain,
275 initial_response,
276 };
277 let cmd = Command { tag, body };
278 trace!("send IMAP command {cmd:?}");
279
280 self.state = State::Send {
281 send: ImapSend::new(CommandCodec::new(), cmd),
282 pending,
283 };
284 debug!("{}", self.state);
285 }
286 State::Send { send, pending } => {
287 let out = imap_try!(send, fragmentizer, arg);
288
289 if let Some(bye) = out.bye {
290 let err = ImapAuthPlainError::Bye(bye.text.to_string());
291 return ImapCoroutineState::Complete(Err(err));
292 }
293
294 if let Some(cr) = out.continuation_request {
295 let payload = match pending.take() {
300 Some(payload) => payload,
301 None => {
302 match self.resume_sasl(SaslArg::Input(&extract_challenge(cr))) {
303 Ok(payload) => payload.unwrap_or_default(),
304 Err(err) => return ImapCoroutineState::Complete(Err(err)),
305 }
306 }
307 };
308
309 self.state = Self::wants_continue(payload);
310 debug!("{}", self.state);
311 continue;
312 }
313
314 let inlined = pending.is_none();
319
320 let Some(Tagged { body, .. }) = out.tagged else {
321 let err = ImapAuthPlainError::ExpectedContinuationRequest;
322 return ImapCoroutineState::Complete(Err(err));
323 };
324
325 let code = match body.kind {
326 StatusKind::Ok if inlined => body.code,
327 StatusKind::Ok => {
328 let err = ImapAuthPlainError::UnexpectedOk;
329 return ImapCoroutineState::Complete(Err(err));
330 }
331 StatusKind::No => {
332 let err = ImapAuthPlainError::No(body.text.to_string());
333 return ImapCoroutineState::Complete(Err(err));
334 }
335 StatusKind::Bad => {
336 let err = ImapAuthPlainError::Bad(body.text.to_string());
337 return ImapCoroutineState::Complete(Err(err));
338 }
339 };
340
341 if let Err(err) = self.resume_sasl(SaslArg::Done) {
342 return ImapCoroutineState::Complete(Err(err));
343 }
344
345 if let Some(next) = self.wants_capability(code, out.data, out.untagged) {
346 self.state = next;
347 debug!("{}", self.state);
348 continue;
349 }
350
351 if let Some(next) = self.wants_id() {
352 self.state = next;
353 debug!("{}", self.state);
354 continue;
355 }
356
357 let capability = mem::take(&mut self.observed);
358 return ImapCoroutineState::Complete(Ok(capability));
359 }
360 State::Continue(send) => {
361 let out = imap_try!(send, fragmentizer, arg);
362
363 if let Some(bye) = out.bye {
364 let err = ImapAuthPlainError::Bye(bye.text.to_string());
365 return ImapCoroutineState::Complete(Err(err));
366 }
367
368 if let Some(cr) = out.continuation_request {
369 let payload = match self.resume_sasl(SaslArg::Input(&extract_challenge(cr)))
370 {
371 Ok(payload) => payload.unwrap_or_default(),
372 Err(err) => return ImapCoroutineState::Complete(Err(err)),
373 };
374
375 self.state = Self::wants_continue(payload);
376 debug!("{}", self.state);
377 continue;
378 }
379
380 let Some(Tagged { body, .. }) = out.tagged else {
381 let err = ImapAuthPlainError::MissingTagged;
382 return ImapCoroutineState::Complete(Err(err));
383 };
384
385 let code = match body.kind {
386 StatusKind::Ok => body.code,
387 StatusKind::No => {
388 let err = ImapAuthPlainError::No(body.text.to_string());
389 return ImapCoroutineState::Complete(Err(err));
390 }
391 StatusKind::Bad => {
392 let err = ImapAuthPlainError::Bad(body.text.to_string());
393 return ImapCoroutineState::Complete(Err(err));
394 }
395 };
396
397 if let Err(err) = self.resume_sasl(SaslArg::Done) {
403 return ImapCoroutineState::Complete(Err(err));
404 }
405
406 if let Some(next) = self.wants_capability(code, out.data, out.untagged) {
407 self.state = next;
408 debug!("{}", self.state);
409 continue;
410 }
411
412 if let Some(next) = self.wants_id() {
413 self.state = next;
414 debug!("{}", self.state);
415 continue;
416 }
417
418 let capability = mem::take(&mut self.observed);
419 return ImapCoroutineState::Complete(Ok(capability));
420 }
421 State::Capability(capability) => {
422 self.observed = imap_try!(capability, fragmentizer, arg);
423
424 if let Some(next) = self.wants_id() {
425 self.state = next;
426 debug!("{}", self.state);
427 continue;
428 }
429
430 let capability = mem::take(&mut self.observed);
431 return ImapCoroutineState::Complete(Ok(capability));
432 }
433 State::Id(id) => {
434 imap_try!(id, fragmentizer, arg);
435 let capability = mem::take(&mut self.observed);
436 return ImapCoroutineState::Complete(Ok(capability));
437 }
438 }
439 }
440 }
441}
442
443enum State {
444 Start,
445 Send {
446 send: ImapSend<CommandCodec>,
447 pending: Option<Vec<u8>>,
448 },
449 Continue(ImapSend<AuthenticateDataCodec>),
450 Capability(ImapCapabilityGet),
451 Id(ImapServerId),
452}
453
454impl fmt::Display for State {
455 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
456 match self {
457 Self::Start => f.write_str("start mechanism"),
458 Self::Send { pending, .. } if pending.is_some() => f.write_str("send auth"),
459 Self::Send { .. } => f.write_str("send auth with ir"),
460 Self::Continue(_) => f.write_str("send credentials"),
461 Self::Capability(_) => f.write_str("fetch capabilities"),
462 Self::Id(_) => f.write_str("send id"),
463 }
464 }
465}
466
467fn extract_challenge(cr: CommandContinuationRequest<'static>) -> Vec<u8> {
468 match cr {
469 CommandContinuationRequest::Base64(data) => data.as_ref().to_vec(),
470 CommandContinuationRequest::Basic(_) => vec![],
471 }
472}
473
474#[cfg(test)]
475mod tests {
476 use core::str;
477
478 use alloc::format;
479
480 use crate::sasl::auth_plain::*;
481
482 #[test]
483 fn ir_success_returns_ok() {
484 let opts = ImapAuthPlainOptions {
485 initial_request: true,
486 ..Default::default()
487 };
488
489 let mut auth = ImapAuthPlain::new(None::<&str>, "alice", "secret", opts);
490 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
491
492 let bytes = expect_wants_write(&mut auth, &mut frag, None);
493 let line = str::from_utf8(&bytes).expect("utf8 command");
494 let tag = first_word(line);
495 assert!(line.contains("AUTHENTICATE PLAIN "));
496
497 expect_wants_read(&mut auth, &mut frag);
498
499 let reply = format!("{tag} OK AUTHENTICATE completed\r\n");
500 expect_complete_ok(&mut auth, &mut frag, reply.as_bytes());
501 }
502
503 #[test]
504 fn ir_invalid_credentials_returns_no_error() {
505 let opts = ImapAuthPlainOptions {
506 initial_request: true,
507 ..Default::default()
508 };
509
510 let mut auth = ImapAuthPlain::new(None::<&str>, "alice", "wrong", opts);
511 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
512
513 let bytes = expect_wants_write(&mut auth, &mut frag, None);
514 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command"));
515
516 expect_wants_read(&mut auth, &mut frag);
517
518 let reply = format!("{tag} NO authentication failed\r\n");
519 let err = expect_complete_err(&mut auth, &mut frag, reply.as_bytes());
520 let ImapAuthPlainError::No(text) = err else {
521 panic!("expected ImapAuthPlainError::No, got {err:?}");
522 };
523 assert_eq!(text, "authentication failed");
524 }
525
526 #[test]
527 fn ir_tagged_bad_returns_bad_error() {
528 let opts = ImapAuthPlainOptions {
529 initial_request: true,
530 ..Default::default()
531 };
532
533 let mut auth = ImapAuthPlain::new(None::<&str>, "alice", "secret", opts);
534 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
535
536 let bytes = expect_wants_write(&mut auth, &mut frag, None);
537 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command"));
538
539 expect_wants_read(&mut auth, &mut frag);
540
541 let reply = format!("{tag} BAD AUTHENTICATE not enabled\r\n");
542 let err = expect_complete_err(&mut auth, &mut frag, reply.as_bytes());
543 let ImapAuthPlainError::Bad(text) = err else {
544 panic!("expected ImapAuthPlainError::Bad, got {err:?}");
545 };
546 assert_eq!(text, "AUTHENTICATE not enabled");
547 }
548
549 #[test]
550 fn ir_extra_challenge_returns_mechanism_error() {
551 let opts = ImapAuthPlainOptions {
552 initial_request: true,
553 ..Default::default()
554 };
555
556 let mut auth = ImapAuthPlain::new(None::<&str>, "alice", "secret", opts);
557 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
558
559 expect_wants_write(&mut auth, &mut frag, None);
560 expect_wants_read(&mut auth, &mut frag);
561
562 let err = expect_complete_err(&mut auth, &mut frag, b"+ \r\n");
566 let ImapAuthPlainError::Mechanism(SaslPlainError::UnexpectedChallenge) = err else {
567 panic!("expected ImapAuthPlainError::Mechanism, got {err:?}");
568 };
569 }
570
571 #[test]
572 fn non_ir_success_returns_ok() {
573 let opts = ImapAuthPlainOptions::default();
574 let mut auth = ImapAuthPlain::new(None::<&str>, "alice", "secret", opts);
575 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
576
577 let bytes = expect_wants_write(&mut auth, &mut frag, None);
578 let line = str::from_utf8(&bytes).expect("utf8 command");
579 let tag = first_word(line);
580 assert!(line.trim_end().ends_with("AUTHENTICATE PLAIN"));
581
582 expect_wants_read(&mut auth, &mut frag);
583
584 let creds = expect_wants_write(&mut auth, &mut frag, Some(b"+ \r\n"));
585 assert!(creds.ends_with(b"\r\n"));
586
587 expect_wants_read(&mut auth, &mut frag);
588
589 let reply = format!("{tag} OK AUTHENTICATE completed\r\n");
590 expect_complete_ok(&mut auth, &mut frag, reply.as_bytes());
591 }
592
593 #[test]
594 fn non_ir_invalid_credentials_returns_no_error() {
595 let opts = ImapAuthPlainOptions::default();
596 let mut auth = ImapAuthPlain::new(None::<&str>, "alice", "wrong", opts);
597 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
598
599 let bytes = expect_wants_write(&mut auth, &mut frag, None);
600 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command"));
601
602 expect_wants_read(&mut auth, &mut frag);
603 expect_wants_write(&mut auth, &mut frag, Some(b"+ \r\n"));
604 expect_wants_read(&mut auth, &mut frag);
605
606 let reply = format!("{tag} NO authentication failed\r\n");
607 let err = expect_complete_err(&mut auth, &mut frag, reply.as_bytes());
608 let ImapAuthPlainError::No(text) = err else {
609 panic!("expected ImapAuthPlainError::No, got {err:?}");
610 };
611 assert_eq!(text, "authentication failed");
612 }
613
614 #[test]
615 fn non_ir_tagged_ok_before_credentials_returns_unexpected_ok_error() {
616 let opts = ImapAuthPlainOptions::default();
617 let mut auth = ImapAuthPlain::new(None::<&str>, "alice", "secret", opts);
618 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
619
620 let bytes = expect_wants_write(&mut auth, &mut frag, None);
621 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command"));
622
623 expect_wants_read(&mut auth, &mut frag);
624
625 let reply = format!("{tag} OK AUTHENTICATE completed\r\n");
628 let err = expect_complete_err(&mut auth, &mut frag, reply.as_bytes());
629 let ImapAuthPlainError::UnexpectedOk = err else {
630 panic!("expected ImapAuthPlainError::UnexpectedOk, got {err:?}");
631 };
632 }
633
634 fn expect_wants_write(
635 cor: &mut ImapAuthPlain,
636 frag: &mut Fragmentizer,
637 arg: Option<&[u8]>,
638 ) -> Vec<u8> {
639 match cor.resume(frag, arg) {
640 ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
641 state => panic!("expected WantsWrite, got {state:?}"),
642 }
643 }
644
645 fn expect_wants_read(cor: &mut ImapAuthPlain, frag: &mut Fragmentizer) {
646 match cor.resume(frag, None) {
647 ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
648 state => panic!("expected WantsRead, got {state:?}"),
649 }
650 }
651
652 fn expect_complete_ok(cor: &mut ImapAuthPlain, frag: &mut Fragmentizer, reply: &[u8]) {
653 match cor.resume(frag, Some(reply)) {
654 ImapCoroutineState::Complete(Ok(_)) => {}
655 state => panic!("expected Complete(Ok), got {state:?}"),
656 }
657 }
658
659 fn expect_complete_err(
660 cor: &mut ImapAuthPlain,
661 frag: &mut Fragmentizer,
662 reply: &[u8],
663 ) -> ImapAuthPlainError {
664 match cor.resume(frag, Some(reply)) {
665 ImapCoroutineState::Complete(Err(err)) => err,
666 state => panic!("expected Complete(Err), got {state:?}"),
667 }
668 }
669
670 fn first_word(line: &str) -> &str {
671 line.split_whitespace()
672 .next()
673 .expect("first whitespace-separated token")
674 }
675}