use base64::Engine as _;
use sha2::{Digest, Sha256};
use thiserror::Error;
use zeroize::Zeroizing;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("no randomness available")]
pub struct RandomError;
pub struct Pkce {
verifier: Zeroizing<String>,
challenge: String,
}
impl Pkce {
pub fn new() -> Result<Self, RandomError> {
let mut bytes = [0u8; 32];
getrandom::fill(&mut bytes).map_err(|_| RandomError)?;
let verifier =
Zeroizing::new(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes));
Ok(Self {
challenge: challenge_of(&verifier),
verifier,
})
}
#[must_use]
pub fn verifier(&self) -> &str {
&self.verifier
}
#[must_use]
pub fn challenge(&self) -> &str {
&self.challenge
}
}
fn challenge_of(verifier: &str) -> String {
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(Sha256::digest(verifier.as_bytes()))
}
pub fn random_state() -> Result<String, RandomError> {
let mut bytes = [0u8; 16];
getrandom::fill(&mut bytes).map_err(|_| RandomError)?;
Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_s256_challenge_matches_rfc_7636_appendix_b() {
let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
assert_eq!(
challenge_of(verifier),
"E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
);
}
#[test]
fn a_fresh_pair_has_the_minimum_length_and_matches_itself() {
let pkce = Pkce::new().expect("randomness");
assert_eq!(pkce.verifier().len(), 43, "32 bytes of base64url");
assert_eq!(challenge_of(pkce.verifier()), pkce.challenge());
}
#[test]
fn pairs_are_never_reused() {
let first = Pkce::new().expect("randomness");
let second = Pkce::new().expect("randomness");
assert_ne!(first.verifier(), second.verifier());
}
#[test]
fn states_are_fresh_too() {
assert_ne!(
random_state().expect("randomness"),
random_state().expect("randomness")
);
assert_eq!(
random_state().expect("randomness").len(),
22,
"16 bytes of base64url"
);
}
}