Skip to main content

vtcode_auth/
pkce.rs

1//! PKCE (Proof Key for Code Exchange) utilities for OAuth 2.0.
2//!
3//! Implements RFC 7636 for secure OAuth flows without client secrets.
4//! Uses SHA-256 (S256) code challenge method as recommended by the spec.
5
6use anyhow::Result;
7use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
8use ring::rand::{SecureRandom, SystemRandom};
9use sha2::{Digest, Sha256};
10
11/// PKCE code verifier length (43-128 characters per RFC 7636)
12const CODE_VERIFIER_LENGTH: usize = 64;
13
14/// Characters allowed in code verifier (unreserved URI characters)
15const CODE_VERIFIER_CHARSET: &[u8] =
16    b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
17
18/// PKCE challenge pair containing verifier and challenge strings.
19#[derive(Debug, Clone)]
20pub struct PkceChallenge {
21    /// The code verifier (random string, kept secret by client)
22    pub code_verifier: String,
23    /// The code challenge (SHA-256 hash of verifier, sent to authorization server)
24    pub code_challenge: String,
25    /// The challenge method (always "S256" for SHA-256)
26    pub code_challenge_method: String,
27}
28
29impl PkceChallenge {
30    /// Create a new PKCE challenge from a code verifier.
31    pub fn from_verifier(code_verifier: String) -> Result<Self> {
32        let code_challenge = compute_s256_challenge(&code_verifier)?;
33        Ok(Self {
34            code_verifier,
35            code_challenge,
36            code_challenge_method: "S256".to_string(),
37        })
38    }
39}
40
41/// Generate a cryptographically secure PKCE challenge pair.
42///
43/// This function generates a random code verifier and computes
44/// the corresponding S256 code challenge.
45///
46/// # Example
47/// ```
48/// use vtcode_auth::generate_pkce_challenge;
49///
50/// let challenge = generate_pkce_challenge().unwrap();
51/// println!("Verifier: {}", challenge.code_verifier);
52/// println!("Challenge: {}", challenge.code_challenge);
53/// ```
54pub fn generate_pkce_challenge() -> Result<PkceChallenge> {
55    let code_verifier = generate_code_verifier()?;
56    PkceChallenge::from_verifier(code_verifier)
57}
58
59/// Generate a cryptographically random code verifier per RFC 7636 §4.1.
60///
61/// Uses `ring::rand::SystemRandom` (backed by the OS CSPRNG) instead of a
62/// user-space PRNG to ensure ≥128 bits of entropy as required by the spec.
63fn generate_code_verifier() -> Result<String> {
64    let rng = SystemRandom::new();
65    let charset_len = CODE_VERIFIER_CHARSET.len() as u8;
66    let max_valid = (256u16 - 256u16 % charset_len as u16) as u8;
67    let mut verifier = String::with_capacity(CODE_VERIFIER_LENGTH);
68    let mut buf = [0u8; 1];
69
70    while verifier.len() < CODE_VERIFIER_LENGTH {
71        rng.fill(&mut buf)
72            .map_err(|_| anyhow::anyhow!("failed to read from OS random source"))?;
73        // Rejection sampling to avoid modulo bias.
74        if buf[0] < max_valid {
75            let idx = (buf[0] % charset_len) as usize;
76            verifier.push(CODE_VERIFIER_CHARSET[idx] as char);
77        }
78    }
79
80    Ok(verifier)
81}
82
83/// Compute S256 code challenge from a code verifier.
84///
85/// S256 = BASE64URL(SHA256(code_verifier))
86fn compute_s256_challenge(code_verifier: &str) -> Result<String> {
87    let mut hasher = Sha256::new();
88    hasher.update(code_verifier.as_bytes());
89    let hash = hasher.finalize();
90
91    Ok(URL_SAFE_NO_PAD.encode(hash))
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn test_generate_pkce_challenge() {
100        let challenge = generate_pkce_challenge().unwrap();
101
102        // Verify code verifier length
103        assert_eq!(challenge.code_verifier.len(), CODE_VERIFIER_LENGTH);
104
105        // Verify all characters are in allowed charset
106        for c in challenge.code_verifier.chars() {
107            assert!(
108                CODE_VERIFIER_CHARSET.contains(&(c as u8)),
109                "Invalid character in verifier: {}",
110                c
111            );
112        }
113
114        // Verify challenge method
115        assert_eq!(challenge.code_challenge_method, "S256");
116
117        // Verify challenge is valid base64url (43 chars for SHA-256)
118        assert_eq!(challenge.code_challenge.len(), 43);
119    }
120
121    #[test]
122    fn test_deterministic_challenge() {
123        // Same verifier should produce same challenge
124        let verifier = "test_verifier_string_for_deterministic_test";
125        let challenge1 = PkceChallenge::from_verifier(verifier.to_string()).unwrap();
126        let challenge2 = PkceChallenge::from_verifier(verifier.to_string()).unwrap();
127
128        assert_eq!(challenge1.code_challenge, challenge2.code_challenge);
129    }
130
131    #[test]
132    fn test_unique_verifiers() {
133        // Multiple calls should produce different verifiers
134        let c1 = generate_pkce_challenge().unwrap();
135        let c2 = generate_pkce_challenge().unwrap();
136
137        assert_ne!(c1.code_verifier, c2.code_verifier);
138    }
139}