use rand_core::{OsRng, RngCore};
use sha2::{Digest, Sha256};
pub struct PkceVerifier(String);
pub struct PkceChallenge(String);
impl PkceVerifier {
pub fn new() -> Self {
let mut bytes = [0u8; 32];
OsRng.fill_bytes(&mut bytes);
PkceVerifier(base64url_encode(&bytes))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn challenge(&self) -> PkceChallenge {
let digest = Sha256::digest(self.0.as_bytes());
PkceChallenge(base64url_encode(&digest))
}
}
impl Default for PkceVerifier {
fn default() -> Self {
Self::new()
}
}
impl PkceChallenge {
pub fn as_str(&self) -> &str {
&self.0
}
}
const TABLE: &[u8; 64] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
#[doc(hidden)]
pub fn base64url_encode(bytes: &[u8]) -> String {
let mut out = String::with_capacity(((bytes.len() + 2) / 3) * 4);
let mut i = 0;
while i + 3 <= bytes.len() {
let b0 = bytes[i] as usize;
let b1 = bytes[i + 1] as usize;
let b2 = bytes[i + 2] as usize;
out.push(TABLE[b0 >> 2] as char);
out.push(TABLE[((b0 & 3) << 4) | (b1 >> 4)] as char);
out.push(TABLE[((b1 & 0xf) << 2) | (b2 >> 6)] as char);
out.push(TABLE[b2 & 0x3f] as char);
i += 3;
}
let rem = bytes.len() - i;
if rem == 1 {
let b0 = bytes[i] as usize;
out.push(TABLE[b0 >> 2] as char);
out.push(TABLE[(b0 & 3) << 4] as char);
} else if rem == 2 {
let b0 = bytes[i] as usize;
let b1 = bytes[i + 1] as usize;
out.push(TABLE[b0 >> 2] as char);
out.push(TABLE[((b0 & 3) << 4) | (b1 >> 4)] as char);
out.push(TABLE[(b1 & 0xf) << 2] as char);
}
out
}
#[doc(hidden)]
pub fn base64url_decode(s: &str) -> Result<Vec<u8>, String> {
Ok(crate::core::base64::Decoder::EITHER.decode(s)?)
}
#[cfg(test)]
mod tests {
#[test]
fn pkce_takes_either_alphabet() {
assert_eq!(vec![0xfb, 0xff], super::base64url_decode("-_8").unwrap());
assert_eq!(vec![0xfb, 0xff], super::base64url_decode("+/8=").unwrap());
assert!(super::base64url_decode("a b").is_err());
}
}