Skip to main content

io_imap/rfc7677/
auth_scram_sha_256.rs

1//! IMAP SASL SCRAM-SHA-256 coroutine; supports both the non-IR and
2//! SASL-IR (RFC 4959) flows.
3//!
4//! SCRAM: <https://www.rfc-editor.org/rfc/rfc5802>
5//! SASL-IR: <https://www.rfc-editor.org/rfc/rfc4959>
6//!
7//! # Example
8//!
9//! ```rust,no_run
10//! use std::{
11//!     io::{Read, Write},
12//!     net::TcpStream,
13//! };
14//!
15//! use io_imap::{
16//!     codec::fragmentizer::Fragmentizer,
17//!     coroutine::{ImapCoroutine, ImapCoroutineState, ImapYield},
18//!     rfc7677::auth_scram_sha_256::{ImapAuthScramSha256, ImapAuthScramSha256Options},
19//! };
20//!
21//! // Ready stream needed (TCP-connected, TLS-negociated)
22//! let mut stream = TcpStream::connect("localhost:143").unwrap();
23//!
24//! let mut fragmentizer = Fragmentizer::new(50 * 1024 * 1024);
25//! let mut buf = [0u8; 4096];
26//!
27//! let opts = ImapAuthScramSha256Options::default();
28//! let mut coroutine = ImapAuthScramSha256::new("alice", "secret", opts);
29//! let mut arg = None;
30//!
31//! let capability = loop {
32//!     match coroutine.resume(&mut fragmentizer, arg.take()) {
33//!         ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
34//!             stream.write_all(&bytes).unwrap();
35//!         }
36//!         ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
37//!             let n = stream.read(&mut buf).unwrap();
38//!             arg = Some(&buf[..n]);
39//!         }
40//!         ImapCoroutineState::Complete(Ok(capability)) => break capability,
41//!         ImapCoroutineState::Complete(Err(err)) => panic!("{err}"),
42//!     }
43//! };
44//!
45//! println!("{capability:?}");
46//! ```
47
48use 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/// Failure causes during the SASL SCRAM-SHA-256 flow.
80#[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/// Options for [`ImapAuthScramSha256::new`].
132#[derive(Clone, Debug, Default, Eq, PartialEq)]
133pub struct ImapAuthScramSha256Options {
134    /// `true` selects SASL-IR (RFC 4959, inline client-first-message);
135    /// `false` selects the non-IR upload-after-challenge flow.
136    pub initial_request: bool,
137    pub ensure_capabilities: bool,
138    pub auto_id: Option<Vec<(IString<'static>, NString<'static>)>>,
139}
140
141/// I/O-free SASL SCRAM-SHA-256 coroutine.
142pub 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        // NOTE: c=biws is base64("n,,"), the GS2 header for no channel binding.
248        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                    // NOTE: some servers piggyback the server-final on the
429                    // tagged OK instead of sending it as a continuation.
430                    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    // SaltedPassword = PBKDF2(SHA-256, password, salt, iterations).
608    let mut salted_password = [0u8; 32];
609    pbkdf2::pbkdf2_hmac::<Sha256>(password, salt, iterations, &mut salted_password);
610
611    // ClientKey = HMAC(SaltedPassword, "Client Key").
612    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    // StoredKey = H(ClientKey).
617    let stored_key = Sha256::digest(&client_key);
618
619    // ClientSignature = HMAC(StoredKey, AuthMessage).
620    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    // ClientProof = ClientKey XOR ClientSignature.
625    let client_proof: Vec<u8> = client_key
626        .iter()
627        .zip(client_signature.iter())
628        .map(|(a, b)| a ^ b)
629        .collect();
630
631    // ServerKey = HMAC(SaltedPassword, "Server Key").
632    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    // ServerSignature = HMAC(ServerKey, AuthMessage).
637    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    // --- utils
833
834    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}