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}