Skip to main content

vtcode_config/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::{Context, Result};
7use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
8use sha2::{Digest, Sha256};
9
10/// PKCE code verifier length (43-128 characters per RFC 7636)
11const CODE_VERIFIER_LENGTH: usize = 64;
12
13/// Characters allowed in code verifier (unreserved URI characters)
14const CODE_VERIFIER_CHARSET: &[u8] =
15    b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
16
17/// PKCE challenge pair containing verifier and challenge strings.
18#[derive(Debug, Clone)]
19pub struct PkceChallenge {
20    /// The code verifier (random string, kept secret by client)
21    pub code_verifier: String,
22    /// The code challenge (SHA-256 hash of verifier, sent to authorization server)
23    pub code_challenge: String,
24    /// The challenge method (always "S256" for SHA-256)
25    pub code_challenge_method: String,
26}
27
28impl PkceChallenge {
29    /// Create a new PKCE challenge from a code verifier.
30    pub fn from_verifier(code_verifier: String) -> Result<Self> {
31        let code_challenge = compute_s256_challenge(&code_verifier)?;
32        Ok(Self {
33            code_verifier,
34            code_challenge,
35            code_challenge_method: "S256".to_string(),
36        })
37    }
38}
39
40/// Generate a cryptographically secure PKCE challenge pair.
41///
42/// This function generates a random code verifier and computes
43/// the corresponding S256 code challenge.
44///
45/// # Example
46/// ```
47/// use vtcode_config::auth::pkce::generate_pkce_challenge;
48///
49/// let challenge = generate_pkce_challenge().unwrap();
50/// println!("Verifier: {}", challenge.code_verifier);
51/// println!("Challenge: {}", challenge.code_challenge);
52/// ```
53pub fn generate_pkce_challenge() -> Result<PkceChallenge> {
54    let code_verifier = generate_code_verifier()?;
55    PkceChallenge::from_verifier(code_verifier)
56}
57
58/// Generate a cryptographically random code verifier.
59fn generate_code_verifier() -> Result<String> {
60    use std::time::{SystemTime, UNIX_EPOCH};
61
62    // Use a simple but effective random generation approach
63    // Combine system time entropy with process ID for uniqueness
64    let mut verifier = String::with_capacity(CODE_VERIFIER_LENGTH);
65
66    // Seed from system time nanoseconds + process ID
67    let nanos = SystemTime::now()
68        .duration_since(UNIX_EPOCH)
69        .context("System time before UNIX epoch")?
70        .as_nanos();
71
72    let pid = std::process::id() as u128;
73    let mut state = nanos.wrapping_add(pid);
74
75    // XorShift128+ inspired PRNG for good distribution
76    for _ in 0..CODE_VERIFIER_LENGTH {
77        // Mix entropy
78        state ^= state << 13;
79        state ^= state >> 7;
80        state ^= state << 17;
81
82        // Add more entropy from high-res time
83        let extra = SystemTime::now()
84            .duration_since(UNIX_EPOCH)
85            .map(|d| d.as_nanos())
86            .unwrap_or(0);
87        state = state.wrapping_add(extra);
88
89        let idx = (state % CODE_VERIFIER_CHARSET.len() as u128) as usize;
90        verifier.push(CODE_VERIFIER_CHARSET[idx] as char);
91    }
92
93    Ok(verifier)
94}
95
96/// Compute S256 code challenge from a code verifier.
97///
98/// S256 = BASE64URL(SHA256(code_verifier))
99fn compute_s256_challenge(code_verifier: &str) -> Result<String> {
100    let mut hasher = Sha256::new();
101    hasher.update(code_verifier.as_bytes());
102    let hash = hasher.finalize();
103
104    Ok(URL_SAFE_NO_PAD.encode(hash))
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_generate_pkce_challenge() {
113        let challenge = generate_pkce_challenge().unwrap();
114
115        // Verify code verifier length
116        assert_eq!(challenge.code_verifier.len(), CODE_VERIFIER_LENGTH);
117
118        // Verify all characters are in allowed charset
119        for c in challenge.code_verifier.chars() {
120            assert!(
121                CODE_VERIFIER_CHARSET.contains(&(c as u8)),
122                "Invalid character in verifier: {}",
123                c
124            );
125        }
126
127        // Verify challenge method
128        assert_eq!(challenge.code_challenge_method, "S256");
129
130        // Verify challenge is valid base64url (43 chars for SHA-256)
131        assert_eq!(challenge.code_challenge.len(), 43);
132    }
133
134    #[test]
135    fn test_deterministic_challenge() {
136        // Same verifier should produce same challenge
137        let verifier = "test_verifier_string_for_deterministic_test";
138        let challenge1 = PkceChallenge::from_verifier(verifier.to_string()).unwrap();
139        let challenge2 = PkceChallenge::from_verifier(verifier.to_string()).unwrap();
140
141        assert_eq!(challenge1.code_challenge, challenge2.code_challenge);
142    }
143
144    #[test]
145    fn test_unique_verifiers() {
146        // Multiple calls should produce different verifiers
147        let c1 = generate_pkce_challenge().unwrap();
148        let c2 = generate_pkce_challenge().unwrap();
149
150        assert_ne!(c1.code_verifier, c2.code_verifier);
151    }
152}