use base64::Engine as _;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use getrandom;
use crate::application::oauth::client::{AuthorizationSecrets, SecretError, State};
use crate::domain::oauth::pkce::CodeVerifier;
const ENTROPY_BYTES: usize = 32;
#[derive(Debug, Clone, Copy, Default)]
pub struct OsSecrets;
impl AuthorizationSecrets for OsSecrets {
fn new_state(&self) -> Result<State, SecretError> {
Ok(State::new(URL_SAFE_NO_PAD.encode(draw()?)))
}
fn new_code_verifier(&self) -> Result<CodeVerifier, SecretError> {
CodeVerifier::new(URL_SAFE_NO_PAD.encode(draw()?)).map_err(|_| SecretError::Rejected)
}
}
fn draw() -> Result<[u8; ENTROPY_BYTES], SecretError> {
let mut bytes = [0_u8; ENTROPY_BYTES];
getrandom::fill(&mut bytes).map_err(|error| match error {
getrandom::Error::UNSUPPORTED => SecretError::EntropyUnsupported,
_ => SecretError::EntropyUnavailable,
})?;
Ok(bytes)
}
#[cfg(test)]
mod tests {
use base64::Engine as _;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use super::{ENTROPY_BYTES, OsSecrets};
use crate::application::oauth::client::{AuthorizationSecrets, SecretError};
use crate::domain::oauth::pkce::ChallengeMethod;
#[test]
fn two_draws_are_not_the_same_value() -> Result<(), SecretError> {
let secrets = OsSecrets;
let first = secrets.new_code_verifier()?;
let second = secrets.new_code_verifier()?;
assert_ne!(first.as_str(), second.as_str());
Ok(())
}
#[test]
fn a_sequence_of_draws_does_not_advance() -> Result<(), SecretError> {
let secrets = OsSecrets;
let values: Vec<String> = (0..16)
.map(|_| {
secrets
.new_code_verifier()
.map(|verifier| verifier.as_str().to_owned())
})
.collect::<Result<_, SecretError>>()?;
let mut sorted = values.clone();
sorted.sort_unstable();
assert_ne!(values, sorted, "the draws came out ordered: {values:?}");
Ok(())
}
#[test]
fn a_verifier_decodes_back_to_the_entropy_it_was_drawn_from() -> Result<(), SecretError> {
let secrets = OsSecrets;
let verifier = secrets.new_code_verifier()?;
let decoded = URL_SAFE_NO_PAD
.decode(verifier.as_str())
.map_err(|_| SecretError::Rejected)?;
assert_eq!(decoded.len(), ENTROPY_BYTES);
Ok(())
}
#[test]
fn a_verifier_is_exactly_the_rfc_minimum_length() -> Result<(), SecretError> {
let secrets = OsSecrets;
assert_eq!(secrets.new_code_verifier()?.as_str().len(), 43);
Ok(())
}
#[test]
fn a_verifier_is_ascii_and_url_safe() -> Result<(), SecretError> {
let secrets = OsSecrets;
let verifier = secrets.new_code_verifier()?;
assert!(
verifier.as_str().bytes().all(|byte| byte.is_ascii()),
"the verifier is not ASCII: {}",
verifier.as_str()
);
assert!(
verifier
.as_str()
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_'),
"the verifier left the base64url alphabet: {}",
verifier.as_str()
);
Ok(())
}
#[test]
fn a_drawn_verifier_yields_a_usable_challenge() -> Result<(), SecretError> {
let secrets = OsSecrets;
let verifier = secrets.new_code_verifier()?;
let challenge = verifier.challenge(ChallengeMethod::S256);
assert_eq!(challenge.as_str().len(), 43);
Ok(())
}
#[test]
fn a_state_survives_a_url_query_unchanged() -> Result<(), SecretError> {
let secrets = OsSecrets;
let state = secrets.new_state()?;
let encoded =
url::form_urlencoded::byte_serialize(state.as_str().as_bytes()).collect::<String>();
assert_eq!(encoded, state.as_str(), "the state was escaped: {encoded}");
Ok(())
}
#[test]
fn a_state_is_not_the_verifier() -> Result<(), SecretError> {
let secrets = OsSecrets;
assert_ne!(
secrets.new_state()?.as_str(),
secrets.new_code_verifier()?.as_str()
);
Ok(())
}
#[test]
fn ninety_draws_are_all_accepted() -> Result<(), SecretError> {
let secrets = OsSecrets;
for _ in 0..90 {
let _ = secrets.new_state()?;
let _ = secrets.new_code_verifier()?;
}
Ok(())
}
#[test]
fn an_entropy_failure_and_an_encoding_failure_are_distinguishable() {
assert_ne!(
SecretError::EntropyUnavailable,
SecretError::EntropyUnsupported
);
assert_ne!(SecretError::EntropyUnavailable, SecretError::Rejected);
assert_ne!(SecretError::EntropyUnsupported, SecretError::Rejected);
}
}