jwk_simple/
encoding.rs

1//! Base64URL encoding utilities with security features.
2//!
3//! This module provides a [`Base64UrlBytes`] wrapper type that handles
4//! base64url encoding/decoding (as required by RFC 7517) with constant-time
5//! operations and automatic memory zeroing for sensitive data.
6
7use base64ct::{Base64UrlUnpadded, Encoding};
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use zeroize::{Zeroize, ZeroizeOnDrop};
10
11use crate::error::Result;
12
13/// A wrapper around raw bytes that serializes to/from base64url encoding.
14///
15/// This type provides:
16/// - Base64url encoding without padding (per RFC 7517)
17/// - Constant-time base64 operations via `base64ct`
18/// - Automatic memory zeroing on drop via `zeroize`
19///
20/// # Examples
21///
22/// ```
23/// use jwk_simple::encoding::Base64UrlBytes;
24///
25/// // Create from raw bytes
26/// let bytes = Base64UrlBytes::new(vec![1, 2, 3, 4]);
27///
28/// // Serialize to JSON (base64url encoded)
29/// let json = serde_json::to_string(&bytes).unwrap();
30/// assert_eq!(json, "\"AQIDBA\"");
31///
32/// // Deserialize from JSON
33/// let decoded: Base64UrlBytes = serde_json::from_str(&json).unwrap();
34/// assert_eq!(decoded.as_bytes(), &[1, 2, 3, 4]);
35/// ```
36#[derive(Clone, Zeroize, ZeroizeOnDrop)]
37pub struct Base64UrlBytes(Vec<u8>);
38
39impl Base64UrlBytes {
40    /// Creates a new `Base64UrlBytes` from raw bytes.
41    ///
42    /// # Arguments
43    ///
44    /// * `bytes` - The raw bytes to wrap.
45    ///
46    /// # Examples
47    ///
48    /// ```
49    /// use jwk_simple::encoding::Base64UrlBytes;
50    ///
51    /// let bytes = Base64UrlBytes::new(vec![0x01, 0x02, 0x03]);
52    /// assert_eq!(bytes.len(), 3);
53    /// ```
54    #[inline]
55    pub fn new(bytes: Vec<u8>) -> Self {
56        Self(bytes)
57    }
58
59    /// Creates a `Base64UrlBytes` by decoding a base64url string.
60    ///
61    /// # Arguments
62    ///
63    /// * `encoded` - A base64url-encoded string (without padding).
64    ///
65    /// # Errors
66    ///
67    /// Returns an error if the input is not valid base64url.
68    ///
69    /// # Examples
70    ///
71    /// ```
72    /// use jwk_simple::encoding::Base64UrlBytes;
73    ///
74    /// let bytes = Base64UrlBytes::from_base64url("AQIDBA").unwrap();
75    /// assert_eq!(bytes.as_bytes(), &[1, 2, 3, 4]);
76    /// ```
77    pub fn from_base64url(encoded: &str) -> Result<Self> {
78        let decoded = Base64UrlUnpadded::decode_vec(encoded)?;
79        Ok(Self(decoded))
80    }
81
82    /// Encodes the bytes as a base64url string (without padding).
83    ///
84    /// # Examples
85    ///
86    /// ```
87    /// use jwk_simple::encoding::Base64UrlBytes;
88    ///
89    /// let bytes = Base64UrlBytes::new(vec![1, 2, 3, 4]);
90    /// assert_eq!(bytes.to_base64url(), "AQIDBA");
91    /// ```
92    pub fn to_base64url(&self) -> String {
93        Base64UrlUnpadded::encode_string(&self.0)
94    }
95
96    /// Returns a reference to the underlying bytes.
97    ///
98    /// # Examples
99    ///
100    /// ```
101    /// use jwk_simple::encoding::Base64UrlBytes;
102    ///
103    /// let bytes = Base64UrlBytes::new(vec![1, 2, 3]);
104    /// assert_eq!(bytes.as_bytes(), &[1, 2, 3]);
105    /// ```
106    #[inline]
107    pub fn as_bytes(&self) -> &[u8] {
108        &self.0
109    }
110
111    /// Returns the length of the underlying bytes.
112    ///
113    /// # Examples
114    ///
115    /// ```
116    /// use jwk_simple::encoding::Base64UrlBytes;
117    ///
118    /// let bytes = Base64UrlBytes::new(vec![1, 2, 3]);
119    /// assert_eq!(bytes.len(), 3);
120    /// ```
121    #[inline]
122    pub fn len(&self) -> usize {
123        self.0.len()
124    }
125
126    /// Returns `true` if the underlying bytes are empty.
127    ///
128    /// # Examples
129    ///
130    /// ```
131    /// use jwk_simple::encoding::Base64UrlBytes;
132    ///
133    /// let empty = Base64UrlBytes::new(vec![]);
134    /// assert!(empty.is_empty());
135    ///
136    /// let not_empty = Base64UrlBytes::new(vec![1]);
137    /// assert!(!not_empty.is_empty());
138    /// ```
139    #[inline]
140    pub fn is_empty(&self) -> bool {
141        self.0.is_empty()
142    }
143
144    /// Consumes the wrapper and returns the underlying bytes.
145    ///
146    /// # Security Note
147    ///
148    /// The returned `Vec<u8>` will NOT be automatically zeroed on drop.
149    /// If the data is sensitive, ensure you zeroize it manually or use
150    /// this method only when necessary.
151    ///
152    /// # Examples
153    ///
154    /// ```
155    /// use jwk_simple::encoding::Base64UrlBytes;
156    ///
157    /// let bytes = Base64UrlBytes::new(vec![1, 2, 3]);
158    /// let raw = bytes.into_bytes();
159    /// assert_eq!(raw, vec![1, 2, 3]);
160    /// ```
161    #[inline]
162    pub fn into_bytes(self) -> Vec<u8> {
163        // Note: We can't prevent the inner vec from being copied,
164        // but at least `self` will be zeroed on drop after this.
165        let mut s = self;
166        std::mem::take(&mut s.0)
167    }
168}
169
170impl std::fmt::Debug for Base64UrlBytes {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        // Don't print the actual bytes in debug output for security
173        f.debug_tuple("Base64UrlBytes")
174            .field(&format!("[{} bytes]", self.0.len()))
175            .finish()
176    }
177}
178
179impl PartialEq for Base64UrlBytes {
180    fn eq(&self, other: &Self) -> bool {
181        // Use constant-time comparison for security
182        use subtle::ConstantTimeEq;
183        self.0.ct_eq(&other.0).into()
184    }
185}
186
187impl Eq for Base64UrlBytes {}
188
189impl From<Vec<u8>> for Base64UrlBytes {
190    fn from(bytes: Vec<u8>) -> Self {
191        Self::new(bytes)
192    }
193}
194
195impl From<&[u8]> for Base64UrlBytes {
196    fn from(bytes: &[u8]) -> Self {
197        Self::new(bytes.to_vec())
198    }
199}
200
201impl AsRef<[u8]> for Base64UrlBytes {
202    fn as_ref(&self) -> &[u8] {
203        &self.0
204    }
205}
206
207impl Serialize for Base64UrlBytes {
208    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
209    where
210        S: Serializer,
211    {
212        serializer.serialize_str(&self.to_base64url())
213    }
214}
215
216impl<'de> Deserialize<'de> for Base64UrlBytes {
217    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
218    where
219        D: Deserializer<'de>,
220    {
221        let s = String::deserialize(deserializer)?;
222        Self::from_base64url(&s).map_err(serde::de::Error::custom)
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn test_roundtrip() {
232        let original = vec![0x01, 0x02, 0x03, 0x04, 0x05];
233        let bytes = Base64UrlBytes::new(original.clone());
234        let encoded = bytes.to_base64url();
235        let decoded = Base64UrlBytes::from_base64url(&encoded).unwrap();
236        assert_eq!(decoded.as_bytes(), &original);
237    }
238
239    #[test]
240    fn test_json_roundtrip() {
241        let original = Base64UrlBytes::new(vec![1, 2, 3, 4]);
242        let json = serde_json::to_string(&original).unwrap();
243        let decoded: Base64UrlBytes = serde_json::from_str(&json).unwrap();
244        assert_eq!(original, decoded);
245    }
246
247    #[test]
248    fn test_empty_bytes() {
249        let empty = Base64UrlBytes::new(vec![]);
250        assert!(empty.is_empty());
251        assert_eq!(empty.len(), 0);
252        assert_eq!(empty.to_base64url(), "");
253    }
254
255    #[test]
256    fn test_known_value() {
257        // Test vector: "AQAB" is base64url for [1, 0, 1] (common RSA exponent 65537 in 3 bytes)
258        // Actually [1, 0, 1] = 65537 = 0x010001
259        let bytes = Base64UrlBytes::from_base64url("AQAB").unwrap();
260        assert_eq!(bytes.as_bytes(), &[0x01, 0x00, 0x01]);
261    }
262}