use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use sha2::{Digest, Sha256};
use crate::error::{AuthError, Result};
const SELECTOR_LEN: usize = 16;
const VERIFIER_LEN: usize = 32;
pub(super) struct OpaqueToken {
selector: [u8; SELECTOR_LEN],
verifier: [u8; VERIFIER_LEN],
}
pub(super) struct ParsedToken {
pub(super) selector: String,
pub(super) verifier_hash: Vec<u8>,
}
impl OpaqueToken {
pub(super) fn generate() -> Self {
use rand::RngCore as _;
let mut selector = [0u8; SELECTOR_LEN];
let mut verifier = [0u8; VERIFIER_LEN];
rand::rng().fill_bytes(&mut selector);
rand::rng().fill_bytes(&mut verifier);
Self { selector, verifier }
}
pub(super) fn selector_b64(&self) -> String {
URL_SAFE_NO_PAD.encode(self.selector)
}
pub(super) fn verifier_hash(&self) -> Vec<u8> {
Sha256::digest(self.verifier).to_vec()
}
pub(super) fn to_token_string(&self) -> String {
format!(
"{}.{}",
URL_SAFE_NO_PAD.encode(self.selector),
URL_SAFE_NO_PAD.encode(self.verifier)
)
}
pub(super) fn parse(kind: &str, token: &str) -> Result<ParsedToken> {
let (selector_b64, verifier_b64) =
token.split_once('.').ok_or_else(|| AuthError::InvalidToken {
reason: format!("{kind} token is not in selector.verifier form"),
})?;
let selector =
URL_SAFE_NO_PAD.decode(selector_b64).map_err(|_| AuthError::InvalidToken {
reason: format!("{kind} token selector is not valid base64url"),
})?;
let verifier =
URL_SAFE_NO_PAD.decode(verifier_b64).map_err(|_| AuthError::InvalidToken {
reason: format!("{kind} token verifier is not valid base64url"),
})?;
if selector.len() != SELECTOR_LEN || verifier.len() != VERIFIER_LEN {
return Err(AuthError::InvalidToken {
reason: format!("{kind} token has an unexpected length"),
});
}
Ok(ParsedToken {
selector: selector_b64.to_string(),
verifier_hash: Sha256::digest(&verifier).to_vec(),
})
}
}
#[allow(clippy::unwrap_used)] #[cfg(test)]
mod tests;