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-negotiated)
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    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/// Failure causes during the SASL SCRAM-SHA-256 flow.
82#[derive(Clone, Debug, Error)]
83pub enum ImapAuthScramSha256Error {
84    /// The server rejected authentication with a tagged NO.
85    #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: NO {0}")]
86    No(String),
87    /// The server rejected the AUTHENTICATE command with a tagged BAD.
88    #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: BAD {0}")]
89    Bad(String),
90    /// The server closed the connection with an untagged BYE.
91    #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: BYE {0}")]
92    Bye(String),
93    /// The server never returned the final tagged response.
94    #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: server did not return a tagged response")]
95    MissingTagged,
96    /// The server never sent the expected continuation request.
97    #[error(
98        "IMAP AUTHENTICATE SCRAM-SHA-256 failed: server did not send the expected continuation request"
99    )]
100    ExpectedContinuationRequest,
101    /// The server returned OK before the mechanism could complete.
102    #[error(
103        "IMAP AUTHENTICATE SCRAM-SHA-256 failed: server returned OK before the mechanism could complete"
104    )]
105    UnexpectedOk,
106    /// A server challenge was not valid UTF-8.
107    #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: invalid server message encoding")]
108    InvalidEncoding,
109    /// The server-first-message carried no r= nonce.
110    #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: server-first-message missing nonce")]
111    MissingNonce,
112    /// The server-first-message carried no s= salt.
113    #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: server-first-message missing salt")]
114    MissingSalt,
115    /// The server-first-message carried no i= iteration count.
116    #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: server-first-message missing iteration count")]
117    MissingIterations,
118    /// A base64 value in a server message failed to decode.
119    #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: invalid base64 in server message")]
120    InvalidBase64,
121    /// The i= iteration count of the server-first-message did not
122    /// parse as an integer.
123    #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: invalid iteration count")]
124    InvalidIterationCount,
125    /// The server nonce did not start with the client nonce from the
126    /// client-first-message.
127    #[error(
128        "IMAP AUTHENTICATE SCRAM-SHA-256 failed: server nonce does not start with client nonce"
129    )]
130    NonceMismatch,
131    /// The v= signature of the server-final-message did not match the
132    /// locally computed one.
133    #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: server signature verification failed")]
134    ServerSignatureMismatch,
135    /// The server-final-message reported an e= error.
136    #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: server error: {0}")]
137    ServerError(String),
138    /// The server-final-message carried neither v= nor e=.
139    #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: invalid server-final-message")]
140    InvalidServerFinal,
141    /// The underlying send coroutine failed.
142    #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: {0}")]
143    Send(#[from] ImapSendError),
144    /// The follow-up CAPABILITY command failed.
145    #[error(transparent)]
146    Capability(#[from] ImapCapabilityGetError),
147    /// The follow-up ID command failed.
148    #[error(transparent)]
149    ServerId(#[from] ImapServerIdError),
150}
151
152/// Options for [`ImapAuthScramSha256::new`].
153#[derive(Clone, Debug, Default, Eq, PartialEq)]
154pub struct ImapAuthScramSha256Options {
155    /// `true` selects SASL-IR (RFC 4959, inline client-first-message);
156    /// `false` selects the non-IR upload-after-challenge flow.
157    pub initial_request: bool,
158    /// Fetch CAPABILITY after authentication when the tagged response
159    /// carries no capability data. Defaults to `false`.
160    pub ensure_capabilities: bool,
161    /// Chain an RFC 2971 ID round-trip right after authentication, as
162    /// required by some providers.
163    ///
164    /// Defaults to `None` (no ID); an empty list sends ID NIL.
165    pub auto_id: Option<Vec<(IString<'static>, NString<'static>)>>,
166}
167
168/// I/O-free SASL SCRAM-SHA-256 coroutine.
169pub 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    /// Builds a SASL SCRAM-SHA-256 coroutine authenticating `user`
181    /// with `password`, generating a fresh client nonce.
182    ///
183    /// Depending on `opts.initial_request`, the client-first-message
184    /// goes inline with the AUTHENTICATE command (SASL-IR) or is
185    /// uploaded after the server challenge.
186    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        // NOTE: c=biws is base64("n,,"), the GS2 header for no channel binding.
281        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                    // NOTE: some servers piggyback the server-final on the
465                    // tagged OK instead of sending it as a continuation.
466                    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    // NOTE: the labels below map each step to its RFC 5802 ยง3 formula.
649
650    // NOTE: SaltedPassword = PBKDF2(SHA-256, password, salt, iterations).
651    let mut salted_password = [0u8; 32];
652    pbkdf2::pbkdf2_hmac::<Sha256>(password, salt, iterations, &mut salted_password);
653
654    // NOTE: ClientKey = HMAC(SaltedPassword, "Client Key").
655    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    // NOTE: StoredKey = H(ClientKey).
660    let stored_key = Sha256::digest(client_key);
661
662    // NOTE: ClientSignature = HMAC(StoredKey, AuthMessage).
663    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    // NOTE: ClientProof = ClientKey XOR ClientSignature.
668    let client_proof: Vec<u8> = client_key
669        .iter()
670        .zip(client_signature.iter())
671        .map(|(a, b)| a ^ b)
672        .collect();
673
674    // NOTE: ServerKey = HMAC(SaltedPassword, "Server Key").
675    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    // NOTE: ServerSignature = HMAC(ServerKey, AuthMessage).
680    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}