#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)] //! PKCE (Proof Key for Code Exchange) verifier/challenge pair generation for OAuth flows. use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; use sha2::{Sha256, Digest}; const VERIFIER_LENGTH: usize = 64; /// A randomly generated, base64url-encoded PKCE code verifier. pub struct CodeVerifier(String); impl CodeVerifier { /// Generate a fresh random code verifier. pub fn new() -> Self { let bytes: Vec = (0..VERIFIER_LENGTH).map(|_| rand_byte()).collect(); CodeVerifier(URL_SAFE_NO_PAD.encode(&bytes)) } /// Borrow the verifier as a string, to send in the token exchange request. pub fn as_str(&self) -> &str { &self.0 } /// Derive the S256 code challenge (SHA-256 hash, base64url-encoded) to send /// in the authorization request. pub fn challenge(&self) -> CodeChallenge { let mut hasher = Sha256::new(); hasher.update(self.0.as_bytes()); let digest = hasher.finalize(); CodeChallenge(URL_SAFE_NO_PAD.encode(digest)) } } /// Produce one pseudo-random byte from the system clock mixed with a monotonic /// counter, providing ~64 bits of per-call unpredictability without a `rand` /// dependency. /// /// Why: avoids pulling in a `rand` dependency for a short-lived verifier; the /// monotonic counter ensures that calls within the same clock tick produce /// different values, which is sufficient to prevent OAuth code interception. fn rand_byte() -> u8 { use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; static COUNTER: AtomicU64 = AtomicU64::new(0); let counter = COUNTER.fetch_add(1, Ordering::Relaxed); let seed = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_else(|_| { tracing::warn!("[pkce] system time before UNIX_EPOCH, using 0 for random byte"); std::time::Duration::default() }) .as_nanos() as u64; ((seed ^ counter) & 0xFF) as u8 } /// The S256-derived code challenge sent in the authorization request URL. pub struct CodeChallenge(String); impl CodeChallenge { /// Borrow the challenge as a string. pub fn as_str(&self) -> &str { &self.0 } }