use std::fmt;
use base64::Engine as _;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use sha2::{Digest as _, Sha256};
use subtle::ConstantTimeEq as _;
use thiserror::Error;
use zeroize::Zeroize as _;
use crate::SecretString;
const RANDOM_TOKEN_BYTES: usize = 32;
const GENERATED_TOKEN_LENGTH: usize = 43;
const MIN_PKCE_VERIFIER_LENGTH: usize = 43;
const MAX_PKCE_VERIFIER_LENGTH: usize = 128;
const MIN_CALLBACK_VALUE_LENGTH: usize = 32;
const MAX_CALLBACK_VALUE_LENGTH: usize = 128;
pub struct PkceVerifier(SecretString);
impl PkceVerifier {
pub fn generate() -> Result<Self, BindingError> {
Ok(Self(SecretString::from(random_token()?)))
}
pub fn new(verifier: SecretString) -> Result<Self, BindingError> {
if !valid_token(
verifier.expose_secret(),
MIN_PKCE_VERIFIER_LENGTH,
MAX_PKCE_VERIFIER_LENGTH,
) {
return Err(BindingError::InvalidPkceVerifier);
}
Ok(Self(verifier))
}
#[must_use]
pub fn expose_secret(&self) -> &str {
self.0.expose_secret()
}
#[must_use]
pub fn challenge(&self) -> PkceChallenge {
let digest = Sha256::digest(self.0.expose_secret().as_bytes());
PkceChallenge(URL_SAFE_NO_PAD.encode(digest))
}
}
impl fmt::Debug for PkceVerifier {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("PkceVerifier([REDACTED])")
}
}
#[derive(Clone, Eq, PartialEq)]
pub struct PkceChallenge(String);
impl PkceChallenge {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Debug for PkceChallenge {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_tuple("PkceChallenge")
.field(&self.0)
.finish()
}
}
pub struct PkceS256 {
verifier: PkceVerifier,
challenge: PkceChallenge,
}
impl PkceS256 {
pub fn generate() -> Result<Self, BindingError> {
let verifier = PkceVerifier::generate()?;
let challenge = verifier.challenge();
Ok(Self {
verifier,
challenge,
})
}
#[must_use]
pub fn from_verifier(verifier: PkceVerifier) -> Self {
let challenge = verifier.challenge();
Self {
verifier,
challenge,
}
}
#[must_use]
pub const fn verifier(&self) -> &PkceVerifier {
&self.verifier
}
#[must_use]
pub const fn challenge(&self) -> &PkceChallenge {
&self.challenge
}
}
impl fmt::Debug for PkceS256 {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("PkceS256")
.field("verifier", &"<redacted>")
.field("challenge", &self.challenge)
.finish()
}
}
pub struct CallbackBinding {
state: SecretString,
nonce: SecretString,
}
impl CallbackBinding {
pub fn generate() -> Result<Self, BindingError> {
Ok(Self {
state: SecretString::from(random_token()?),
nonce: SecretString::from(random_token()?),
})
}
pub fn new(state: SecretString, nonce: SecretString) -> Result<Self, BindingError> {
if !valid_token(
state.expose_secret(),
MIN_CALLBACK_VALUE_LENGTH,
MAX_CALLBACK_VALUE_LENGTH,
) || !valid_token(
nonce.expose_secret(),
MIN_CALLBACK_VALUE_LENGTH,
MAX_CALLBACK_VALUE_LENGTH,
) {
return Err(BindingError::InvalidCallbackBinding);
}
Ok(Self { state, nonce })
}
#[must_use]
pub fn expose_state(&self) -> &str {
self.state.expose_secret()
}
#[must_use]
pub fn expose_nonce(&self) -> &str {
self.nonce.expose_secret()
}
pub fn verify(&self, returned_state: &str, returned_nonce: &str) -> Result<(), BindingError> {
let state_matches = constant_time_bounded_eq(
self.state.expose_secret().as_bytes(),
returned_state.as_bytes(),
);
let nonce_matches = constant_time_bounded_eq(
self.nonce.expose_secret().as_bytes(),
returned_nonce.as_bytes(),
);
if state_matches & nonce_matches {
Ok(())
} else {
Err(BindingError::CallbackMismatch)
}
}
}
impl fmt::Debug for CallbackBinding {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CallbackBinding")
.field("state", &"<redacted>")
.field("nonce", &"<redacted>")
.finish()
}
}
#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
#[non_exhaustive]
pub enum BindingError {
#[error("secure operating system randomness is unavailable")]
RandomnessUnavailable,
#[error("invalid PKCE verifier")]
InvalidPkceVerifier,
#[error("invalid callback binding")]
InvalidCallbackBinding,
#[error("OAuth callback binding mismatch")]
CallbackMismatch,
}
pub fn generate_oauth_correlation_token() -> Result<SecretString, BindingError> {
Ok(SecretString::from(random_token()?))
}
pub fn generate_login_selection_epoch() -> Result<String, BindingError> {
random_token()
}
#[must_use]
pub fn valid_login_selection_epoch(value: &str) -> bool {
value.len() == GENERATED_TOKEN_LENGTH
&& value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
}
fn random_token() -> Result<String, BindingError> {
let mut bytes = [0_u8; RANDOM_TOKEN_BYTES];
getrandom::fill(&mut bytes).map_err(|_| BindingError::RandomnessUnavailable)?;
let token = URL_SAFE_NO_PAD.encode(bytes);
bytes.zeroize();
debug_assert_eq!(token.len(), GENERATED_TOKEN_LENGTH);
Ok(token)
}
fn valid_token(value: &str, minimum: usize, maximum: usize) -> bool {
(minimum..=maximum).contains(&value.len())
&& value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~'))
}
pub fn verify_callback_state(expected: &str, returned: &str) -> Result<(), BindingError> {
if constant_time_bounded_eq(expected.as_bytes(), returned.as_bytes()) & !expected.is_empty() {
Ok(())
} else {
Err(BindingError::CallbackMismatch)
}
}
fn constant_time_bounded_eq(expected: &[u8], returned: &[u8]) -> bool {
let mut expected_padded = [0_u8; MAX_CALLBACK_VALUE_LENGTH];
let mut returned_padded = [0_u8; MAX_CALLBACK_VALUE_LENGTH];
let expected_bounded = expected.len() <= MAX_CALLBACK_VALUE_LENGTH;
let returned_bounded = returned.len() <= MAX_CALLBACK_VALUE_LENGTH;
if expected_bounded {
expected_padded[..expected.len()].copy_from_slice(expected);
}
if returned_bounded {
returned_padded[..returned.len()].copy_from_slice(returned);
}
let bytes_equal = expected_padded.ct_eq(&returned_padded);
let lengths_equal = expected
.len()
.to_le_bytes()
.ct_eq(&returned.len().to_le_bytes());
let result =
expected_bounded & returned_bounded & bool::from(bytes_equal) & bool::from(lengths_equal);
expected_padded.zeroize();
returned_padded.zeroize();
result
}
#[cfg(test)]
#[allow(
clippy::expect_used,
clippy::unwrap_used,
reason = "unit tests may assert with expect/unwrap"
)]
mod tests {
#[test]
fn callback_state_verification_is_bounded_and_exact() {
assert!(verify_callback_state("random-state", "random-state").is_ok());
for returned in ["", "random-stat", "random-state-extra", "wrong-state"] {
assert!(verify_callback_state("random-state", returned).is_err());
}
assert!(verify_callback_state("", "").is_err());
let oversized = "a".repeat(MAX_CALLBACK_VALUE_LENGTH + 1);
assert!(verify_callback_state(&oversized, &oversized).is_err());
}
#[test]
fn login_selection_epoch_has_one_encoding() {
let first = generate_login_selection_epoch().unwrap();
let second = generate_login_selection_epoch().unwrap();
assert_ne!(first, second);
assert!(valid_login_selection_epoch(&first));
let rejected = [
String::new(),
"0123456789abcdef".repeat(4),
first[1..].to_owned(),
format!("{first}A"),
format!("{}=", &first[1..]),
format!("{}.", &first[1..]),
];
for value in &rejected {
assert!(!valid_login_selection_epoch(value), "{value}");
}
}
use super::*;
#[test]
fn rfc_7636_s256_vector() -> Result<(), BindingError> {
let verifier = PkceVerifier::new(SecretString::from(String::from(
"dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
)))?;
assert_eq!(
verifier.challenge().as_str(),
"E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
);
Ok(())
}
#[test]
fn generated_values_are_bounded_and_distinct() -> Result<(), BindingError> {
let pkce = PkceS256::generate()?;
let callback = CallbackBinding::generate()?;
assert_eq!(
pkce.verifier().expose_secret().len(),
GENERATED_TOKEN_LENGTH
);
assert_eq!(pkce.challenge().as_str().len(), GENERATED_TOKEN_LENGTH);
assert_eq!(callback.expose_state().len(), GENERATED_TOKEN_LENGTH);
assert_eq!(callback.expose_nonce().len(), GENERATED_TOKEN_LENGTH);
assert_ne!(callback.expose_state(), callback.expose_nonce());
Ok(())
}
#[test]
fn callback_requires_both_values() -> Result<(), BindingError> {
let state = String::from("01234567890123456789012345678901state");
let nonce = String::from("01234567890123456789012345678901nonce");
let binding = CallbackBinding::new(
SecretString::from(state.clone()),
SecretString::from(nonce.clone()),
)?;
assert_eq!(binding.verify(&state, &nonce), Ok(()));
assert_eq!(
binding.verify(&format!("{state}x"), &nonce),
Err(BindingError::CallbackMismatch)
);
assert_eq!(
binding.verify(&state, &"x".repeat(10_000)),
Err(BindingError::CallbackMismatch)
);
assert!(!format!("{binding:?}").contains(&state));
Ok(())
}
}