Skip to main content

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