use super::{Base64Encodable, Footer, PasetoError};
use std::str;
pub(crate) const MAX_TOKEN_SIZE: usize = 64 * 1024;
pub(crate) const MAX_FOOTER_SIZE: usize = 1024;
#[derive(Debug, Clone, Copy)]
pub struct UntrustedToken<'a> {
version: &'a str,
purpose: &'a str,
footer: Option<&'a str>,
}
impl<'a> UntrustedToken<'a> {
pub fn try_parse(token: &'a str) -> Result<Self, PasetoError> {
if token.len() > MAX_TOKEN_SIZE {
return Err(PasetoError::TokenTooLarge);
}
let parts: Vec<&str> = token.split('.').collect();
let parts_len = parts.len();
if !(3..=4).contains(&parts_len) {
return Err(PasetoError::IncorrectSize);
}
let version = parts.first().ok_or(PasetoError::IncorrectSize)?;
let purpose = parts.get(1).ok_or(PasetoError::IncorrectSize)?;
let footer = if parts_len == 4 {
let f = *parts.get(3).ok_or(PasetoError::IncorrectSize)?;
if f.len() > MAX_FOOTER_SIZE {
return Err(PasetoError::FooterTooLarge);
}
Some(f)
} else {
None
};
Ok(Self {
version,
purpose,
footer,
})
}
#[must_use]
pub const fn version(&self) -> &str {
self.version
}
#[must_use]
pub const fn purpose(&self) -> &str {
self.purpose
}
#[must_use]
pub const fn footer_base64(&self) -> Option<&str> {
self.footer
}
pub fn footer_decoded(&self) -> Result<Option<Vec<u8>>, PasetoError> {
match self.footer {
Some(footer_b64) => {
let decoded = Footer::from(footer_b64).decode()?;
Ok(Some(decoded))
}
None => Ok(None),
}
}
pub fn footer_str(&self) -> Result<Option<String>, PasetoError> {
match self.footer_decoded()? {
Some(bytes) => {
let s = str::from_utf8(&bytes)?.to_string();
Ok(Some(s))
}
None => Ok(None),
}
}
}
#[cfg(test)]
mod unit_tests {
use super::*;
#[test]
fn test_parse_token_with_footer() {
let token = "v4.local.payload.Zm9vdGVy"; let untrusted = UntrustedToken::try_parse(token).expect("failed to parse token");
assert_eq!(untrusted.version(), "v4");
assert_eq!(untrusted.purpose(), "local");
assert_eq!(untrusted.footer_base64(), Some("Zm9vdGVy"));
}
#[test]
fn test_parse_token_without_footer() {
let token = "v4.local.payload";
let untrusted = UntrustedToken::try_parse(token).expect("failed to parse token");
assert_eq!(untrusted.version(), "v4");
assert_eq!(untrusted.purpose(), "local");
assert!(untrusted.footer_base64().is_none());
}
#[test]
fn test_parse_token_too_few_parts() {
let token = "v4.local";
let result = UntrustedToken::try_parse(token);
assert!(matches!(result, Err(PasetoError::IncorrectSize)));
}
#[test]
fn test_parse_token_too_many_parts() {
let token = "v4.local.payload.footer.extra";
let result = UntrustedToken::try_parse(token);
assert!(matches!(result, Err(PasetoError::IncorrectSize)));
}
#[test]
fn test_footer_decoded() {
let token = "v4.local.payload.Zm9vdGVy"; let untrusted = UntrustedToken::try_parse(token).expect("failed to parse token");
let footer_bytes = untrusted
.footer_decoded()
.expect("failed to decode footer")
.expect("footer should be present");
assert_eq!(footer_bytes, b"footer");
}
#[test]
fn test_footer_decoded_returns_none_when_no_footer() {
let token = "v4.local.payload";
let untrusted = UntrustedToken::try_parse(token).expect("failed to parse token");
let footer_bytes = untrusted.footer_decoded().expect("should not error");
assert!(footer_bytes.is_none());
}
#[test]
fn test_footer_str() {
let token = "v4.local.payload.Zm9vdGVy"; let untrusted = UntrustedToken::try_parse(token).expect("failed to parse token");
let footer_str = untrusted
.footer_str()
.expect("failed to decode footer")
.expect("footer should be present");
assert_eq!(&footer_str, "footer");
}
#[test]
fn test_footer_str_json() {
let token = "v4.local.payload.eyJraWQiOiJrZXktMSJ9";
let untrusted = UntrustedToken::try_parse(token).expect("failed to parse token");
let footer_str = untrusted
.footer_str()
.expect("failed to decode footer")
.expect("footer should be present");
assert_eq!(&footer_str, r#"{"kid":"key-1"}"#);
}
#[test]
fn test_invalid_base64_in_footer() {
let token = "v4.local.payload.!!!invalid!!!";
let untrusted = UntrustedToken::try_parse(token).expect("failed to parse token");
let result = untrusted.footer_decoded();
assert!(result.is_err());
}
#[test]
fn test_all_versions() {
for version in &["v1", "v2", "v3", "v4"] {
let token = format!("{}.local.payload", version);
let untrusted = UntrustedToken::try_parse(&token).expect("failed to parse token");
assert_eq!(untrusted.version(), *version);
}
}
#[test]
fn test_both_purposes() {
for purpose in &["local", "public"] {
let token = format!("v4.{}.payload", purpose);
let untrusted = UntrustedToken::try_parse(&token).expect("failed to parse token");
assert_eq!(untrusted.purpose(), *purpose);
}
}
#[test]
fn test_oversized_token_rejected() {
let oversized_payload = "A".repeat(MAX_TOKEN_SIZE);
let token = format!("v4.local.{oversized_payload}");
assert!(token.len() > MAX_TOKEN_SIZE);
let result = UntrustedToken::try_parse(&token);
assert!(matches!(result, Err(PasetoError::TokenTooLarge)));
}
#[test]
fn test_oversized_footer_rejected() {
let oversized_footer = "A".repeat(MAX_FOOTER_SIZE + 1);
let token = format!("v4.local.payload.{oversized_footer}");
assert!(token.len() < MAX_TOKEN_SIZE);
let result = UntrustedToken::try_parse(&token);
assert!(matches!(result, Err(PasetoError::FooterTooLarge)));
}
#[test]
fn test_footer_at_max_size_accepted() {
let max_footer = "A".repeat(MAX_FOOTER_SIZE);
let token = format!("v4.local.payload.{max_footer}");
let untrusted = UntrustedToken::try_parse(&token).expect("should accept footer at limit");
assert!(untrusted.footer_base64().is_some());
}
}