1use core::{fmt, mem};
74
75use alloc::{
76 string::{String, ToString},
77 vec::Vec,
78};
79
80use imap_codec::{
81 AuthenticateDataCodec, CommandCodec,
82 fragmentizer::Fragmentizer,
83 imap_types::{
84 auth::{AuthMechanism, AuthenticateData},
85 command::{Command, CommandBody},
86 core::{IString, NString, TagGenerator},
87 response::{
88 Capability, Code, CommandContinuationRequest, Data, StatusBody, StatusKind, Tagged,
89 },
90 secret::Secret,
91 },
92};
93use io_sasl::{
94 coroutine::*,
95 rfc5802::{SaslScramCreds, SaslScramError},
96 rfc7677::scram_sha_256::SaslScramSha256,
97};
98use log::{debug, trace};
99use thiserror::Error;
100
101use crate::{coroutine::*, imap_try, rfc2971::id::*, rfc3501::capability::*, send::*};
102
103#[derive(Clone, Debug, Error)]
105pub enum ImapAuthScramSha256Error {
106 #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: NO {0}")]
108 No(String),
109 #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: BAD {0}")]
111 Bad(String),
112 #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: BYE {0}")]
114 Bye(String),
115 #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: server did not return a tagged response")]
117 MissingTagged,
118 #[error(
120 "IMAP AUTHENTICATE SCRAM-SHA-256 failed: server did not send the expected continuation request"
121 )]
122 ExpectedContinuationRequest,
123 #[error(
125 "IMAP AUTHENTICATE SCRAM-SHA-256 failed: server returned OK before the mechanism could complete"
126 )]
127 UnexpectedOk,
128 #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: {0}")]
136 Mechanism(#[from] SaslScramError),
137 #[error("IMAP AUTHENTICATE SCRAM-SHA-256 failed: {0}")]
139 Send(#[from] ImapSendError),
140 #[error(transparent)]
142 Capability(#[from] ImapCapabilityGetError),
143 #[error(transparent)]
145 ServerId(#[from] ImapServerIdError),
146}
147
148#[derive(Clone, Debug, Default, Eq, PartialEq)]
150pub struct ImapAuthScramSha256Options {
151 pub initial_request: bool,
154 pub ensure_capabilities: bool,
157 pub auto_id: Option<Vec<(IString<'static>, NString<'static>)>>,
162}
163
164pub struct ImapAuthScramSha256 {
166 state: State,
167 mechanism: SaslScramSha256,
168 observed: Vec<Capability<'static>>,
169 opts: ImapAuthScramSha256Options,
170}
171
172impl ImapAuthScramSha256 {
173 pub fn new(creds: SaslScramCreds, opts: ImapAuthScramSha256Options) -> Self {
191 Self {
192 state: State::Start,
193 mechanism: SaslScramSha256::new(creds),
194 observed: Vec::new(),
195 opts,
196 }
197 }
198
199 fn wants_capability(
202 &mut self,
203 code: Option<Code<'static>>,
204 data: Vec<Data<'static>>,
205 untagged: Vec<StatusBody<'static>>,
206 ) -> Option<State> {
207 let mut new_capability = None;
208
209 if let Some(Code::Capability(capability)) = code {
210 new_capability.replace(capability);
211 }
212
213 for data in data {
214 if let Data::Capability(capability) = data {
215 new_capability.replace(capability);
216 }
217 }
218
219 for StatusBody { code, .. } in untagged {
220 if let Some(Code::Capability(capability)) = code {
221 new_capability.replace(capability);
222 }
223 }
224
225 if let Some(capability) = new_capability {
226 self.observed = capability.into_iter().collect();
227 }
228
229 (self.opts.ensure_capabilities && self.observed.is_empty())
230 .then(|| State::Capability(ImapCapabilityGet::new()))
231 }
232
233 fn wants_id(&mut self) -> Option<State> {
235 let params = self.opts.auto_id.take()?;
236 let wire = (!params.is_empty()).then_some(params);
237 Some(State::Id(ImapServerId::new(ImapServerIdOptions {
238 parameters: wire,
239 })))
240 }
241
242 fn wants_continue(payload: Vec<u8>) -> State {
244 let auth = AuthenticateData::r#continue(payload);
245 let codec = AuthenticateDataCodec::new();
246 State::Continue(ImapSend::new(codec, auth))
247 }
248
249 fn resume_sasl(
251 &mut self,
252 arg: SaslArg<'_>,
253 ) -> Result<Option<Vec<u8>>, ImapAuthScramSha256Error> {
254 match self.mechanism.resume(arg) {
255 SaslCoroutineState::Yielded(SaslYield::WantsWrite(payload)) => Ok(Some(payload)),
256 SaslCoroutineState::Yielded(SaslYield::WantsRead) => Ok(None),
257 SaslCoroutineState::Complete(result) => result.map(|()| None).map_err(Into::into),
258 }
259 }
260}
261
262impl ImapCoroutine for ImapAuthScramSha256 {
263 type Yield = ImapYield;
264 type Return = Result<Vec<Capability<'static>>, ImapAuthScramSha256Error>;
265
266 fn resume(
267 &mut self,
268 fragmentizer: &mut Fragmentizer,
269 arg: Option<&[u8]>,
270 ) -> ImapCoroutineState<Self::Yield, Self::Return> {
271 loop {
272 match &mut self.state {
273 State::Start => {
274 let payload = match self.resume_sasl(SaslArg::None) {
275 Ok(payload) => payload,
276 Err(err) => return ImapCoroutineState::Complete(Err(err)),
277 };
278
279 let (initial_response, pending) = match payload {
284 Some(payload) if self.opts.initial_request => {
285 (Some(Secret::new(payload.into())), None)
286 }
287 payload => (None, payload),
288 };
289
290 let tag = TagGenerator::new().generate();
291 let body = CommandBody::Authenticate {
292 mechanism: AuthMechanism::ScramSha256,
293 initial_response,
294 };
295 let cmd = Command { tag, body };
296 trace!("send IMAP command {cmd:?}");
297
298 self.state = State::Send {
299 send: ImapSend::new(CommandCodec::new(), cmd),
300 pending,
301 };
302 debug!("{}", self.state);
303 }
304 State::Send { send, pending } => {
305 let out = imap_try!(send, fragmentizer, arg);
306
307 if let Some(bye) = out.bye {
308 let err = ImapAuthScramSha256Error::Bye(bye.text.to_string());
309 return ImapCoroutineState::Complete(Err(err));
310 }
311
312 if let Some(cr) = out.continuation_request {
313 let payload = match pending.take() {
318 Some(payload) => payload,
319 None => {
320 match self.resume_sasl(SaslArg::Input(&extract_challenge(cr))) {
321 Ok(payload) => payload.unwrap_or_default(),
322 Err(err) => return ImapCoroutineState::Complete(Err(err)),
323 }
324 }
325 };
326
327 self.state = Self::wants_continue(payload);
328 debug!("{}", self.state);
329 continue;
330 }
331
332 let inlined = pending.is_none();
333
334 let Some(Tagged { body, .. }) = out.tagged else {
335 let err = ImapAuthScramSha256Error::ExpectedContinuationRequest;
336 return ImapCoroutineState::Complete(Err(err));
337 };
338
339 let code = match body.kind {
340 StatusKind::Ok if inlined => body.code,
341 StatusKind::Ok => {
342 let err = ImapAuthScramSha256Error::UnexpectedOk;
343 return ImapCoroutineState::Complete(Err(err));
344 }
345 StatusKind::No => {
346 let err = ImapAuthScramSha256Error::No(body.text.to_string());
347 return ImapCoroutineState::Complete(Err(err));
348 }
349 StatusKind::Bad => {
350 let err = ImapAuthScramSha256Error::Bad(body.text.to_string());
351 return ImapCoroutineState::Complete(Err(err));
352 }
353 };
354
355 if let Err(err) = self.resume_sasl(SaslArg::Done) {
360 return ImapCoroutineState::Complete(Err(err));
361 }
362
363 if let Some(next) = self.wants_capability(code, out.data, out.untagged) {
364 self.state = next;
365 debug!("{}", self.state);
366 continue;
367 }
368
369 if let Some(next) = self.wants_id() {
370 self.state = next;
371 debug!("{}", self.state);
372 continue;
373 }
374
375 let capability = mem::take(&mut self.observed);
376 return ImapCoroutineState::Complete(Ok(capability));
377 }
378 State::Continue(send) => {
379 let out = imap_try!(send, fragmentizer, arg);
380
381 if let Some(bye) = out.bye {
382 let err = ImapAuthScramSha256Error::Bye(bye.text.to_string());
383 return ImapCoroutineState::Complete(Err(err));
384 }
385
386 if let Some(cr) = out.continuation_request {
387 let payload = match self.resume_sasl(SaslArg::Input(&extract_challenge(cr)))
388 {
389 Ok(payload) => payload.unwrap_or_default(),
390 Err(err) => return ImapCoroutineState::Complete(Err(err)),
391 };
392
393 self.state = Self::wants_continue(payload);
394 debug!("{}", self.state);
395 continue;
396 }
397
398 let Some(Tagged { body, .. }) = out.tagged else {
399 let err = ImapAuthScramSha256Error::MissingTagged;
400 return ImapCoroutineState::Complete(Err(err));
401 };
402
403 let code = match body.kind {
404 StatusKind::Ok => body.code,
405 StatusKind::No => {
406 let err = ImapAuthScramSha256Error::No(body.text.to_string());
407 return ImapCoroutineState::Complete(Err(err));
408 }
409 StatusKind::Bad => {
410 let err = ImapAuthScramSha256Error::Bad(body.text.to_string());
411 return ImapCoroutineState::Complete(Err(err));
412 }
413 };
414
415 if let Err(err) = self.resume_sasl(SaslArg::Done) {
421 return ImapCoroutineState::Complete(Err(err));
422 }
423
424 if let Some(next) = self.wants_capability(code, out.data, out.untagged) {
425 self.state = next;
426 debug!("{}", self.state);
427 continue;
428 }
429
430 if let Some(next) = self.wants_id() {
431 self.state = next;
432 debug!("{}", self.state);
433 continue;
434 }
435
436 let capability = mem::take(&mut self.observed);
437 return ImapCoroutineState::Complete(Ok(capability));
438 }
439 State::Capability(capability) => {
440 self.observed = imap_try!(capability, fragmentizer, arg);
441
442 if let Some(next) = self.wants_id() {
443 self.state = next;
444 debug!("{}", self.state);
445 continue;
446 }
447
448 let capability = mem::take(&mut self.observed);
449 return ImapCoroutineState::Complete(Ok(capability));
450 }
451 State::Id(id) => {
452 imap_try!(id, fragmentizer, arg);
453 let capability = mem::take(&mut self.observed);
454 return ImapCoroutineState::Complete(Ok(capability));
455 }
456 }
457 }
458 }
459}
460
461enum State {
462 Start,
463 Send {
464 send: ImapSend<CommandCodec>,
465 pending: Option<Vec<u8>>,
466 },
467 Continue(ImapSend<AuthenticateDataCodec>),
468 Capability(ImapCapabilityGet),
469 Id(ImapServerId),
470}
471
472impl fmt::Display for State {
473 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
474 match self {
475 Self::Start => f.write_str("start mechanism"),
476 Self::Send { pending, .. } if pending.is_some() => f.write_str("send auth"),
477 Self::Send { .. } => f.write_str("send auth with ir"),
478 Self::Continue(_) => f.write_str("send response"),
479 Self::Capability(_) => f.write_str("fetch capabilities"),
480 Self::Id(_) => f.write_str("send id"),
481 }
482 }
483}
484
485fn extract_challenge(cr: CommandContinuationRequest<'static>) -> Vec<u8> {
486 match cr {
487 CommandContinuationRequest::Basic(basic) => basic.text().to_string().into_bytes(),
488 CommandContinuationRequest::Base64(data) => data.as_ref().to_vec(),
489 }
490}
491
492#[cfg(test)]
493mod tests {
494 use core::str;
495
496 use alloc::{borrow::ToOwned, format};
497
498 use base64::{Engine, engine::general_purpose::STANDARD};
499 use hmac::{Hmac, KeyInit, Mac};
500 use io_sasl::rfc5801::SaslGs2ChannelBinding;
501 use secrecy::SecretString;
502 use sha2::Sha256;
503
504 use crate::rfc7677::auth_scram_sha_256::*;
505
506 type HmacSha256 = Hmac<Sha256>;
507
508 const NONCE: &[u8] = b"fyko+d2lbbFgONRv9qkxdawL";
509
510 fn creds() -> SaslScramCreds {
511 SaslScramCreds {
512 username: "alice".to_string(),
513 password: SecretString::from("secret"),
514 nonce: NONCE.to_vec(),
515 channel_binding: SaslGs2ChannelBinding::Unsupported,
516 }
517 }
518
519 #[test]
520 fn ir_success_returns_ok() {
521 let opts = ImapAuthScramSha256Options {
522 initial_request: true,
523 ..Default::default()
524 };
525
526 let mut auth = ImapAuthScramSha256::new(creds(), opts);
527 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
528
529 let bytes = expect_wants_write(&mut auth, &mut frag, None);
530 let line = str::from_utf8(&bytes).expect("utf8 command");
531 let tag = first_word(line).to_owned();
532 let client_first = decode_last_base64_token(line);
533 let client_nonce = extract_client_nonce(&client_first);
534
535 expect_wants_read(&mut auth, &mut frag);
536
537 let server_first = format!("r={client_nonce}ServerExtra,s={SALT_B64},i={ITERATIONS}");
538 let challenge = format!("+ {}\r\n", STANDARD.encode(&server_first));
539 let client_final_bytes =
540 expect_wants_write(&mut auth, &mut frag, Some(challenge.as_bytes()));
541 let client_final_line = str::from_utf8(&client_final_bytes).expect("utf8");
542 let client_final = decode_last_base64_token(client_final_line.trim_end());
543
544 expect_wants_read(&mut auth, &mut frag);
545
546 let server_final = build_server_final(&client_first, &server_first, &client_final);
547 let challenge2 = format!("+ {}\r\n", STANDARD.encode(&server_final));
548 let ack = expect_wants_write(&mut auth, &mut frag, Some(challenge2.as_bytes()));
549 assert_eq!(b"\r\n", &*ack);
550
551 expect_wants_read(&mut auth, &mut frag);
552
553 let reply = format!("{tag} OK AUTHENTICATE completed\r\n");
554 expect_complete_ok(&mut auth, &mut frag, reply.as_bytes());
555 }
556
557 #[test]
558 fn ir_server_error_returns_mechanism_error() {
559 let opts = ImapAuthScramSha256Options {
560 initial_request: true,
561 ..Default::default()
562 };
563
564 let mut auth = ImapAuthScramSha256::new(creds(), opts);
565 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
566
567 let bytes = expect_wants_write(&mut auth, &mut frag, None);
568 let client_first = decode_last_base64_token(str::from_utf8(&bytes).expect("utf8"));
569 let client_nonce = extract_client_nonce(&client_first);
570
571 expect_wants_read(&mut auth, &mut frag);
572
573 let server_first = format!("r={client_nonce}ServerExtra,s={SALT_B64},i={ITERATIONS}");
574 let challenge = format!("+ {}\r\n", STANDARD.encode(&server_first));
575 let _client_final = expect_wants_write(&mut auth, &mut frag, Some(challenge.as_bytes()));
576
577 expect_wants_read(&mut auth, &mut frag);
578
579 let server_final = "e=invalid-proof";
580 let challenge2 = format!("+ {}\r\n", STANDARD.encode(server_final));
581 let err = expect_complete_err(&mut auth, &mut frag, challenge2.as_bytes());
582 let ImapAuthScramSha256Error::Mechanism(SaslScramError::ServerError(text)) = err else {
583 panic!("expected ImapAuthScramSha256Error::Mechanism, got {err:?}");
584 };
585 assert_eq!(text, "invalid-proof");
586 }
587
588 #[test]
589 fn ir_tagged_bad_returns_bad_error() {
590 let opts = ImapAuthScramSha256Options {
591 initial_request: true,
592 ..Default::default()
593 };
594
595 let mut auth = ImapAuthScramSha256::new(creds(), opts);
596 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
597
598 let bytes = expect_wants_write(&mut auth, &mut frag, None);
599 let tag = first_word(str::from_utf8(&bytes).expect("utf8"));
600
601 expect_wants_read(&mut auth, &mut frag);
602
603 let reply = format!("{tag} BAD AUTHENTICATE not enabled\r\n");
604 let err = expect_complete_err(&mut auth, &mut frag, reply.as_bytes());
605 let ImapAuthScramSha256Error::Bad(text) = err else {
606 panic!("expected ImapAuthScramSha256Error::Bad, got {err:?}");
607 };
608 assert_eq!(text, "AUTHENTICATE not enabled");
609 }
610
611 #[test]
612 fn ir_tagged_ok_before_the_server_proved_itself_returns_mechanism_error() {
613 let opts = ImapAuthScramSha256Options {
614 initial_request: true,
615 ..Default::default()
616 };
617
618 let mut auth = ImapAuthScramSha256::new(creds(), opts);
619 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
620
621 let bytes = expect_wants_write(&mut auth, &mut frag, None);
622 let line = str::from_utf8(&bytes).expect("utf8 command");
623 let tag = first_word(line).to_owned();
624 let client_first = decode_last_base64_token(line);
625 let client_nonce = extract_client_nonce(&client_first);
626
627 expect_wants_read(&mut auth, &mut frag);
628
629 let server_first = format!("r={client_nonce}ServerExtra,s={SALT_B64},i={ITERATIONS}");
630 let challenge = format!("+ {}\r\n", STANDARD.encode(&server_first));
631 expect_wants_write(&mut auth, &mut frag, Some(challenge.as_bytes()));
632
633 expect_wants_read(&mut auth, &mut frag);
634
635 let reply = format!("{tag} OK AUTHENTICATE completed\r\n");
640 let err = expect_complete_err(&mut auth, &mut frag, reply.as_bytes());
641 let ImapAuthScramSha256Error::Mechanism(SaslScramError::ServerSignatureNotVerified) = err
642 else {
643 panic!("expected ImapAuthScramSha256Error::Mechanism, got {err:?}");
644 };
645 }
646
647 #[test]
648 fn non_ir_success_returns_ok() {
649 let opts = ImapAuthScramSha256Options::default();
650 let mut auth = ImapAuthScramSha256::new(creds(), opts);
651 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
652
653 let bytes = expect_wants_write(&mut auth, &mut frag, None);
654 let line = str::from_utf8(&bytes).expect("utf8 command");
655 let tag = first_word(line).to_owned();
656 assert!(line.trim_end().ends_with("AUTHENTICATE SCRAM-SHA-256"));
657
658 expect_wants_read(&mut auth, &mut frag);
659
660 let client_first_bytes = expect_wants_write(&mut auth, &mut frag, Some(b"+ \r\n"));
661 let client_first = decode_last_base64_token(
662 str::from_utf8(&client_first_bytes)
663 .expect("utf8")
664 .trim_end(),
665 );
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 = decode_last_base64_token(
675 str::from_utf8(&client_final_bytes)
676 .expect("utf8")
677 .trim_end(),
678 );
679
680 expect_wants_read(&mut auth, &mut frag);
681
682 let server_final = build_server_final(&client_first, &server_first, &client_final);
683 let challenge2 = format!("+ {}\r\n", STANDARD.encode(&server_final));
684 let ack = expect_wants_write(&mut auth, &mut frag, Some(challenge2.as_bytes()));
685 assert_eq!(b"\r\n", &*ack);
686
687 expect_wants_read(&mut auth, &mut frag);
688
689 let reply = format!("{tag} OK AUTHENTICATE completed\r\n");
690 expect_complete_ok(&mut auth, &mut frag, reply.as_bytes());
691 }
692
693 #[test]
694 fn non_ir_server_error_returns_mechanism_error() {
695 let opts = ImapAuthScramSha256Options::default();
696 let mut auth = ImapAuthScramSha256::new(creds(), opts);
697 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
698
699 let bytes = expect_wants_write(&mut auth, &mut frag, None);
700 let _tag = first_word(str::from_utf8(&bytes).expect("utf8"));
701
702 expect_wants_read(&mut auth, &mut frag);
703
704 let client_first_bytes = expect_wants_write(&mut auth, &mut frag, Some(b"+ \r\n"));
705 let client_first = decode_last_base64_token(
706 str::from_utf8(&client_first_bytes)
707 .expect("utf8")
708 .trim_end(),
709 );
710 let client_nonce = extract_client_nonce(&client_first);
711
712 expect_wants_read(&mut auth, &mut frag);
713
714 let server_first = format!("r={client_nonce}ServerExtra,s={SALT_B64},i={ITERATIONS}");
715 let challenge = format!("+ {}\r\n", STANDARD.encode(&server_first));
716 let _client_final = expect_wants_write(&mut auth, &mut frag, Some(challenge.as_bytes()));
717
718 expect_wants_read(&mut auth, &mut frag);
719
720 let server_final = "e=invalid-proof";
721 let challenge2 = format!("+ {}\r\n", STANDARD.encode(server_final));
722 let err = expect_complete_err(&mut auth, &mut frag, challenge2.as_bytes());
723 let ImapAuthScramSha256Error::Mechanism(SaslScramError::ServerError(text)) = err else {
724 panic!("expected ImapAuthScramSha256Error::Mechanism, got {err:?}");
725 };
726 assert_eq!(text, "invalid-proof");
727 }
728
729 const SALT_B64: &str = "QSXCR+Q6sek8bf92";
730 const ITERATIONS: u32 = 4096;
731
732 fn expect_wants_write(
733 cor: &mut ImapAuthScramSha256,
734 frag: &mut Fragmentizer,
735 arg: Option<&[u8]>,
736 ) -> Vec<u8> {
737 match cor.resume(frag, arg) {
738 ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
739 state => panic!("expected WantsWrite, got {state:?}"),
740 }
741 }
742
743 fn expect_wants_read(cor: &mut ImapAuthScramSha256, frag: &mut Fragmentizer) {
744 match cor.resume(frag, None) {
745 ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
746 state => panic!("expected WantsRead, got {state:?}"),
747 }
748 }
749
750 fn expect_complete_ok(cor: &mut ImapAuthScramSha256, frag: &mut Fragmentizer, reply: &[u8]) {
751 match cor.resume(frag, Some(reply)) {
752 ImapCoroutineState::Complete(Ok(_)) => {}
753 state => panic!("expected Complete(Ok), got {state:?}"),
754 }
755 }
756
757 fn expect_complete_err(
758 cor: &mut ImapAuthScramSha256,
759 frag: &mut Fragmentizer,
760 reply: &[u8],
761 ) -> ImapAuthScramSha256Error {
762 match cor.resume(frag, Some(reply)) {
763 ImapCoroutineState::Complete(Err(err)) => err,
764 state => panic!("expected Complete(Err), got {state:?}"),
765 }
766 }
767
768 fn first_word(line: &str) -> &str {
769 line.split_whitespace()
770 .next()
771 .expect("first whitespace-separated token")
772 }
773
774 fn decode_last_base64_token(line: &str) -> String {
775 let b64 = line
776 .trim_end()
777 .rsplit_terminator(char::is_whitespace)
778 .next()
779 .expect("token");
780 let bytes = STANDARD.decode(b64).expect("valid base64");
781 String::from_utf8(bytes).expect("valid utf8")
782 }
783
784 fn extract_client_nonce(client_first: &str) -> &str {
785 client_first
786 .rsplit_once("r=")
787 .expect("client-first has r=")
788 .1
789 }
790
791 fn build_server_final(client_first: &str, server_first: &str, client_final: &str) -> String {
796 let client_first_bare = client_first.strip_prefix("n,,").expect("gs2 header");
797 let client_final_without_proof = client_final
798 .rsplit_once(",p=")
799 .expect("client-final has p=")
800 .0;
801 let auth_message =
802 format!("{client_first_bare},{server_first},{client_final_without_proof}");
803 let salt = STANDARD.decode(SALT_B64).expect("valid salt");
804
805 let mut salted_password = [0u8; 32];
807 pbkdf2::pbkdf2_hmac::<Sha256>(b"secret", &salt, ITERATIONS, &mut salted_password);
808
809 let mut mac = HmacSha256::new_from_slice(&salted_password).unwrap();
811 mac.update(b"Server Key");
812 let server_key = mac.finalize().into_bytes();
813
814 let mut mac = HmacSha256::new_from_slice(&server_key).unwrap();
816 mac.update(auth_message.as_bytes());
817 let server_signature = mac.finalize().into_bytes();
818
819 format!("v={}", STANDARD.encode(server_signature))
820 }
821}