cratefield_oauth_client/
pkce.rs1use base64::Engine as _;
6use sha2::{Digest, Sha256};
7use thiserror::Error;
8use zeroize::Zeroizing;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
13#[error("no randomness available")]
14pub struct RandomError;
15
16pub struct Pkce {
22 verifier: Zeroizing<String>,
23 challenge: String,
24}
25
26impl Pkce {
27 pub fn new() -> Result<Self, RandomError> {
35 let mut bytes = [0u8; 32];
36 getrandom::fill(&mut bytes).map_err(|_| RandomError)?;
37 let verifier =
38 Zeroizing::new(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes));
39 Ok(Self {
40 challenge: challenge_of(&verifier),
41 verifier,
42 })
43 }
44
45 #[must_use]
47 pub fn verifier(&self) -> &str {
48 &self.verifier
49 }
50
51 #[must_use]
53 pub fn challenge(&self) -> &str {
54 &self.challenge
55 }
56}
57
58fn challenge_of(verifier: &str) -> String {
61 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(Sha256::digest(verifier.as_bytes()))
62}
63
64pub fn random_state() -> Result<String, RandomError> {
72 let mut bytes = [0u8; 16];
73 getrandom::fill(&mut bytes).map_err(|_| RandomError)?;
74 Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes))
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
84 fn the_s256_challenge_matches_rfc_7636_appendix_b() {
85 let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
89 assert_eq!(
90 challenge_of(verifier),
91 "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
92 );
93 }
94
95 #[test]
96 fn a_fresh_pair_has_the_minimum_length_and_matches_itself() {
97 let pkce = Pkce::new().expect("randomness");
98 assert_eq!(pkce.verifier().len(), 43, "32 bytes of base64url");
99 assert_eq!(challenge_of(pkce.verifier()), pkce.challenge());
100 }
101
102 #[test]
103 fn pairs_are_never_reused() {
104 let first = Pkce::new().expect("randomness");
105 let second = Pkce::new().expect("randomness");
106 assert_ne!(first.verifier(), second.verifier());
107 }
108
109 #[test]
110 fn states_are_fresh_too() {
111 assert_ne!(
112 random_state().expect("randomness"),
113 random_state().expect("randomness")
114 );
115 assert_eq!(
116 random_state().expect("randomness").len(),
117 22,
118 "16 bytes of base64url"
119 );
120 }
121}