1use anyhow::{Context, Result};
7use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
8use ring::rand::{SecureRandom, SystemRandom};
9use sha2::{Digest, Sha256};
10use std::fmt;
11
12const CODE_VERIFIER_LENGTH: usize = 64;
14
15const CODE_VERIFIER_CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
17
18#[derive(Clone)]
24pub struct PkceChallenge {
25 pub(crate) code_verifier: String,
27 pub(crate) code_challenge: String,
29 pub(crate) code_challenge_method: String,
31}
32
33impl fmt::Debug for PkceChallenge {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 f.debug_struct("PkceChallenge")
36 .field("code_verifier", &"<redacted>")
37 .field("code_challenge", &self.code_challenge)
38 .field("code_challenge_method", &self.code_challenge_method)
39 .finish()
40 }
41}
42
43impl PkceChallenge {
44 pub fn code_challenge(&self) -> &str {
46 &self.code_challenge
47 }
48
49 pub fn code_challenge_method(&self) -> &str {
51 &self.code_challenge_method
52 }
53
54 fn from_verifier(code_verifier: String) -> Result<Self> {
56 let code_challenge = compute_s256_challenge(&code_verifier)?;
57 Ok(Self {
58 code_verifier,
59 code_challenge,
60 code_challenge_method: "S256".to_string(),
61 })
62 }
63}
64
65pub fn generate_pkce_challenge() -> Result<PkceChallenge> {
79 let code_verifier = generate_code_verifier()?;
80 PkceChallenge::from_verifier(code_verifier)
81}
82
83fn generate_code_verifier() -> Result<String> {
88 let rng = SystemRandom::new();
89 let charset_len = u8::try_from(CODE_VERIFIER_CHARSET.len()).context("PKCE verifier alphabet is too large")?;
90 let max_valid =
91 u8::try_from(256u16 - 256u16 % u16::from(charset_len)).context("PKCE verifier rejection range is invalid")?;
92 let mut verifier = String::with_capacity(CODE_VERIFIER_LENGTH);
93 let mut buf = [0u8; 1];
94
95 while verifier.len() < CODE_VERIFIER_LENGTH {
96 rng.fill(&mut buf)
97 .map_err(|_| anyhow::anyhow!("failed to read from OS random source"))?;
98 if buf[0] < max_valid {
100 let idx = usize::from(buf[0] % charset_len);
101 if let Some(&character) = CODE_VERIFIER_CHARSET.get(idx) {
102 verifier.push(char::from(character));
103 }
104 }
105 }
106
107 Ok(verifier)
108}
109
110fn compute_s256_challenge(code_verifier: &str) -> Result<String> {
114 let mut hasher = Sha256::new();
115 hasher.update(code_verifier.as_bytes());
116 let hash = hasher.finalize();
117
118 Ok(URL_SAFE_NO_PAD.encode(hash))
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn test_generate_pkce_challenge() {
127 let challenge = generate_pkce_challenge().unwrap();
128
129 assert_eq!(challenge.code_verifier.len(), CODE_VERIFIER_LENGTH);
131
132 for c in challenge.code_verifier.chars() {
134 assert!(CODE_VERIFIER_CHARSET.contains(&(c as u8)), "Invalid character in verifier: {c}");
135 }
136
137 assert_eq!(challenge.code_challenge_method, "S256");
139
140 assert_eq!(challenge.code_challenge.len(), 43);
142 }
143
144 #[test]
145 fn test_deterministic_challenge() {
146 let verifier = "test_verifier_string_for_deterministic_test";
148 let challenge1 = PkceChallenge::from_verifier(verifier.to_string()).unwrap();
149 let challenge2 = PkceChallenge::from_verifier(verifier.to_string()).unwrap();
150
151 assert_eq!(challenge1.code_challenge, challenge2.code_challenge);
152 }
153
154 #[test]
155 fn test_unique_verifiers() {
156 let c1 = generate_pkce_challenge().unwrap();
158 let c2 = generate_pkce_challenge().unwrap();
159
160 assert_ne!(c1.code_verifier, c2.code_verifier);
161 }
162}