1use base64::engine::general_purpose::URL_SAFE_NO_PAD;
4use base64::Engine;
5use rand::Rng;
6use sha2::Digest;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum PkceMethod {
11 S256,
13 Plain,
15}
16
17pub fn generate_pkce_pair() -> (String, String) {
23 let mut verifier_bytes = [0u8; 32];
24 rand::rng().fill(&mut verifier_bytes[..]);
25 let verifier = URL_SAFE_NO_PAD.encode(verifier_bytes);
26
27 let challenge = sha256_base64url(&verifier);
28 (verifier, challenge)
29}
30
31pub fn verify_pkce(code_verifier: &str, stored_challenge: &str, method: &str) -> bool {
35 match method {
36 "S256" => {
37 let computed = sha256_base64url(code_verifier);
38 constant_time_eq(computed.as_bytes(), stored_challenge.as_bytes())
39 }
40 "plain" => constant_time_eq(code_verifier.as_bytes(), stored_challenge.as_bytes()),
41 _ => false,
42 }
43}
44
45fn sha256_base64url(input: &str) -> String {
46 let hash = sha2::Sha256::digest(input.as_bytes());
47 URL_SAFE_NO_PAD.encode(hash)
48}
49
50fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
51 if a.len() != b.len() {
52 return false;
53 }
54 a.iter().zip(b.iter()).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60
61 #[test]
62 fn generate_pkce_pair_deterministic_challenge() {
63 let (v, c) = generate_pkce_pair();
64 let expected = sha256_base64url(&v);
65 assert_eq!(c, expected);
66 }
67
68 #[test]
69 fn verify_pkce_s256_correct() {
70 let (verifier, challenge) = generate_pkce_pair();
71 assert!(verify_pkce(&verifier, &challenge, "S256"));
72 }
73
74 #[test]
75 fn verify_pkce_s256_wrong_verifier() {
76 let (_, challenge) = generate_pkce_pair();
77 assert!(!verify_pkce("wrong-verifier", &challenge, "S256"));
78 }
79
80 #[test]
81 fn verify_pkce_plain_correct() {
82 assert!(verify_pkce("test", "test", "plain"));
83 }
84
85 #[test]
86 fn verify_pkce_plain_wrong() {
87 assert!(!verify_pkce("test", "wrong", "plain"));
88 }
89
90 #[test]
91 fn verify_pkce_unknown_method() {
92 assert!(!verify_pkce("a", "b", "unknown"));
93 }
94}