use std::fmt::{self, Debug};
use base64ct::{Base64UrlUnpadded, Encoding};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use subtle::{Choice, ConstantTimeEq};
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
use crate::error::Result;
#[derive(Clone, PartialEq, Eq, Hash, Zeroize, ZeroizeOnDrop)]
pub struct Base64UrlBytes(Vec<u8>);
impl Base64UrlBytes {
#[inline]
pub fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
pub fn from_base64url(encoded: &str) -> Result<Self> {
let decoded = Base64UrlUnpadded::decode_vec(encoded)?;
Ok(Self(decoded))
}
pub fn to_base64url(&self) -> String {
Base64UrlUnpadded::encode_string(&self.0)
}
#[inline]
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
#[inline]
pub fn len(&self) -> usize {
self.0.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[inline]
pub fn into_bytes(self) -> Zeroizing<Vec<u8>> {
let mut s = self;
Zeroizing::new(std::mem::take(&mut s.0))
}
#[inline]
pub fn ct_eq(&self, other: &Self) -> bool {
bool::from(ConstantTimeEq::ct_eq(self, other))
}
}
impl Debug for Base64UrlBytes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Base64UrlBytes")
.field(&format!("[{} bytes]", self.0.len()))
.finish()
}
}
impl ConstantTimeEq for Base64UrlBytes {
#[inline]
fn ct_eq(&self, other: &Self) -> Choice {
self.0.as_slice().ct_eq(other.0.as_slice())
}
}
impl From<Vec<u8>> for Base64UrlBytes {
fn from(bytes: Vec<u8>) -> Self {
Self::new(bytes)
}
}
impl From<&[u8]> for Base64UrlBytes {
fn from(bytes: &[u8]) -> Self {
Self::new(bytes.to_vec())
}
}
impl AsRef<[u8]> for Base64UrlBytes {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl Serialize for Base64UrlBytes {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_base64url())
}
}
impl<'de> Deserialize<'de> for Base64UrlBytes {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::from_base64url(&s).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_roundtrip() {
let original = vec![0x01, 0x02, 0x03, 0x04, 0x05];
let bytes = Base64UrlBytes::new(original.clone());
let encoded = bytes.to_base64url();
let decoded = Base64UrlBytes::from_base64url(&encoded).unwrap();
assert_eq!(decoded.as_bytes(), &original);
}
#[test]
fn test_json_roundtrip() {
let original = Base64UrlBytes::new(vec![1, 2, 3, 4]);
let json = serde_json::to_string(&original).unwrap();
let decoded: Base64UrlBytes = serde_json::from_str(&json).unwrap();
assert_eq!(original, decoded);
}
#[test]
fn test_empty_bytes() {
let empty = Base64UrlBytes::new(vec![]);
assert!(empty.is_empty());
assert_eq!(empty.len(), 0);
assert_eq!(empty.to_base64url(), "");
}
#[test]
fn test_constant_time_equality() {
let a = Base64UrlBytes::new(vec![1, 2, 3, 4]);
let b = Base64UrlBytes::new(vec![1, 2, 3, 4]);
let c = Base64UrlBytes::new(vec![1, 2, 3, 5]);
let d = Base64UrlBytes::new(vec![1, 2, 3]);
assert!(a.ct_eq(&b));
assert!(!a.ct_eq(&c));
assert!(!a.ct_eq(&d));
}
#[test]
fn test_known_value() {
let bytes = Base64UrlBytes::from_base64url("AQAB").unwrap();
assert_eq!(bytes.as_bytes(), &[0x01, 0x00, 0x01]);
}
#[test]
fn test_from_base64url_invalid() {
assert!(Base64UrlBytes::from_base64url("AQAB==").is_err());
assert!(Base64UrlBytes::from_base64url("!!!").is_err());
}
}