Skip to main content

base64_ng_serde/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2#![deny(unsafe_code)]
3#![deny(missing_docs)]
4#![deny(clippy::all)]
5#![deny(clippy::pedantic)]
6
7//! Optional `serde` integration for `base64-ng`.
8//!
9//! This crate keeps serialization support out of the core package. It provides
10//! explicit wrappers and `#[serde(with = "...")]` modules so applications must
11//! choose the alphabet and padding policy at the field boundary.
12//!
13//! # Security
14//!
15//! Deserialization helpers in this crate use `base64_ng::Engine::decode_vec`,
16//! the strict timing-variable decoder. They map decode failures to redacted
17//! error classes, but they are not constant-time-oriented secret decoders. Do
18//! not use these serde modules for API keys, bearer tokens, private keys, or
19//! other secret-bearing fields when malformed-input timing matters. Decode
20//! those values explicitly with `base64_ng::ct` or with
21//! `base64_ng_sanitization::CtDecodeSanitizationExt` instead.
22
23#[cfg(feature = "alloc")]
24extern crate alloc;
25
26#[cfg(feature = "alloc")]
27use alloc::{string::String, vec::Vec};
28
29#[cfg(feature = "alloc")]
30use base64_ng::{
31    Alphabet, DecodeError, Engine, MIME, PEM, Profile, STANDARD, STANDARD_NO_PAD, URL_SAFE,
32    URL_SAFE_NO_PAD, clear_bytes, constant_time_eq,
33};
34#[cfg(feature = "alloc")]
35use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
36
37/// Owned bytes serialized as strict standard padded Base64.
38///
39/// This wrapper is still an interoperability type, not a secret container.
40/// It clears its initialized bytes on drop as a retention-reduction measure,
41/// but clones are independent copies and serialization intentionally exposes
42/// the Base64 text to the serializer.
43#[cfg(feature = "alloc")]
44#[derive(Clone)]
45pub struct Base64Standard(Vec<u8>);
46
47/// Owned bytes serialized as URL-safe unpadded Base64.
48///
49/// This wrapper is still an interoperability type, not a secret container.
50/// It clears its initialized bytes on drop as a retention-reduction measure,
51/// but clones are independent copies and serialization intentionally exposes
52/// the Base64 text to the serializer.
53#[cfg(feature = "alloc")]
54#[derive(Clone)]
55pub struct Base64UrlSafeNoPad(Vec<u8>);
56
57#[cfg(feature = "alloc")]
58impl Base64Standard {
59    /// Wraps bytes for standard Base64 serialization.
60    #[must_use]
61    pub const fn new(bytes: Vec<u8>) -> Self {
62        Self(bytes)
63    }
64
65    /// Returns the wrapped bytes.
66    #[must_use]
67    pub fn as_bytes(&self) -> &[u8] {
68        &self.0
69    }
70
71    /// Consumes the wrapper and returns the owned bytes.
72    ///
73    /// The returned vector is no longer cleared by this wrapper on drop.
74    /// Callers handling sensitive values must apply their own cleanup policy.
75    #[must_use]
76    pub fn into_inner(mut self) -> Vec<u8> {
77        core::mem::take(&mut self.0)
78    }
79}
80
81#[cfg(feature = "alloc")]
82impl Base64UrlSafeNoPad {
83    /// Wraps bytes for URL-safe no-padding Base64 serialization.
84    #[must_use]
85    pub const fn new(bytes: Vec<u8>) -> Self {
86        Self(bytes)
87    }
88
89    /// Returns the wrapped bytes.
90    #[must_use]
91    pub fn as_bytes(&self) -> &[u8] {
92        &self.0
93    }
94
95    /// Consumes the wrapper and returns the owned bytes.
96    ///
97    /// The returned vector is no longer cleared by this wrapper on drop.
98    /// Callers handling sensitive values must apply their own cleanup policy.
99    #[must_use]
100    pub fn into_inner(mut self) -> Vec<u8> {
101        core::mem::take(&mut self.0)
102    }
103}
104
105#[cfg(feature = "alloc")]
106impl Drop for Base64Standard {
107    fn drop(&mut self) {
108        clear_bytes(&mut self.0);
109    }
110}
111
112#[cfg(feature = "alloc")]
113impl Drop for Base64UrlSafeNoPad {
114    fn drop(&mut self) {
115        clear_bytes(&mut self.0);
116    }
117}
118
119#[cfg(feature = "alloc")]
120impl PartialEq for Base64Standard {
121    fn eq(&self, other: &Self) -> bool {
122        constant_time_eq(self.as_bytes(), other.as_bytes())
123    }
124}
125
126#[cfg(feature = "alloc")]
127impl Eq for Base64Standard {}
128
129#[cfg(feature = "alloc")]
130impl PartialEq for Base64UrlSafeNoPad {
131    fn eq(&self, other: &Self) -> bool {
132        constant_time_eq(self.as_bytes(), other.as_bytes())
133    }
134}
135
136#[cfg(feature = "alloc")]
137impl Eq for Base64UrlSafeNoPad {}
138
139#[cfg(feature = "alloc")]
140impl core::fmt::Debug for Base64Standard {
141    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
142        formatter
143            .debug_struct("Base64Standard")
144            .field("bytes", &"<redacted>")
145            .field("len", &self.0.len())
146            .finish()
147    }
148}
149
150#[cfg(feature = "alloc")]
151impl core::fmt::Debug for Base64UrlSafeNoPad {
152    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
153        formatter
154            .debug_struct("Base64UrlSafeNoPad")
155            .field("bytes", &"<redacted>")
156            .field("len", &self.0.len())
157            .finish()
158    }
159}
160
161#[cfg(feature = "alloc")]
162impl Serialize for Base64Standard {
163    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
164    where
165        S: Serializer,
166    {
167        standard::serialize(&self.0, serializer)
168    }
169}
170
171#[cfg(feature = "alloc")]
172impl<'de> Deserialize<'de> for Base64Standard {
173    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
174    where
175        D: Deserializer<'de>,
176    {
177        standard::deserialize(deserializer).map(Self)
178    }
179}
180
181#[cfg(feature = "alloc")]
182impl Serialize for Base64UrlSafeNoPad {
183    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
184    where
185        S: Serializer,
186    {
187        url_safe_no_pad::serialize(&self.0, serializer)
188    }
189}
190
191#[cfg(feature = "alloc")]
192impl<'de> Deserialize<'de> for Base64UrlSafeNoPad {
193    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
194    where
195        D: Deserializer<'de>,
196    {
197        url_safe_no_pad::deserialize(deserializer).map(Self)
198    }
199}
200
201/// Serde helpers for strict standard padded Base64 fields.
202///
203/// # Security
204///
205/// Deserialization uses the strict timing-variable decoder. Use this module
206/// for interoperability-oriented fields, not secret-bearing fields where
207/// malformed-input timing matters.
208#[cfg(feature = "alloc")]
209pub mod standard {
210    use super::{STANDARD, Vec, deserialize_with_engine, serialize_with_engine};
211    use serde::{Deserializer, Serializer};
212
213    /// Serializes bytes as strict standard padded Base64 text.
214    ///
215    /// # Errors
216    ///
217    /// Returns the serializer's error if Base64 encoding fails or the
218    /// serializer rejects the string value.
219    pub fn serialize<S>(bytes: impl AsRef<[u8]>, serializer: S) -> Result<S::Ok, S::Error>
220    where
221        S: Serializer,
222    {
223        serialize_with_engine(STANDARD, bytes.as_ref(), serializer)
224    }
225
226    /// Deserializes strict standard padded Base64 text into owned bytes.
227    ///
228    /// # Errors
229    ///
230    /// Returns the deserializer's error if the value is not a string or if the
231    /// string is not valid strict standard padded Base64.
232    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
233    where
234        D: Deserializer<'de>,
235    {
236        deserialize_with_engine(STANDARD, deserializer)
237    }
238}
239
240/// Serde helpers for strict standard unpadded Base64 fields.
241///
242/// # Security
243///
244/// Deserialization uses the strict timing-variable decoder. Use this module
245/// for interoperability-oriented fields, not secret-bearing fields where
246/// malformed-input timing matters.
247#[cfg(feature = "alloc")]
248pub mod standard_no_pad {
249    use super::{STANDARD_NO_PAD, Vec, deserialize_with_engine, serialize_with_engine};
250    use serde::{Deserializer, Serializer};
251
252    /// Serializes bytes as strict standard unpadded Base64 text.
253    ///
254    /// # Errors
255    ///
256    /// Returns the serializer's error if Base64 encoding fails or the
257    /// serializer rejects the string value.
258    pub fn serialize<S>(bytes: impl AsRef<[u8]>, serializer: S) -> Result<S::Ok, S::Error>
259    where
260        S: Serializer,
261    {
262        serialize_with_engine(STANDARD_NO_PAD, bytes.as_ref(), serializer)
263    }
264
265    /// Deserializes strict standard unpadded Base64 text into owned bytes.
266    ///
267    /// # Errors
268    ///
269    /// Returns the deserializer's error if the value is not a string or if the
270    /// string is not valid strict standard unpadded Base64.
271    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
272    where
273        D: Deserializer<'de>,
274    {
275        deserialize_with_engine(STANDARD_NO_PAD, deserializer)
276    }
277}
278
279/// Serde helpers for URL-safe padded Base64 fields.
280///
281/// # Security
282///
283/// Deserialization uses the strict timing-variable decoder. Use this module
284/// for interoperability-oriented fields, not secret-bearing fields where
285/// malformed-input timing matters.
286#[cfg(feature = "alloc")]
287pub mod url_safe {
288    use super::{URL_SAFE, Vec, deserialize_with_engine, serialize_with_engine};
289    use serde::{Deserializer, Serializer};
290
291    /// Serializes bytes as URL-safe padded Base64 text.
292    ///
293    /// # Errors
294    ///
295    /// Returns the serializer's error if Base64 encoding fails or the
296    /// serializer rejects the string value.
297    pub fn serialize<S>(bytes: impl AsRef<[u8]>, serializer: S) -> Result<S::Ok, S::Error>
298    where
299        S: Serializer,
300    {
301        serialize_with_engine(URL_SAFE, bytes.as_ref(), serializer)
302    }
303
304    /// Deserializes URL-safe padded Base64 text into owned bytes.
305    ///
306    /// # Errors
307    ///
308    /// Returns the deserializer's error if the value is not a string or if the
309    /// string is not valid URL-safe padded Base64.
310    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
311    where
312        D: Deserializer<'de>,
313    {
314        deserialize_with_engine(URL_SAFE, deserializer)
315    }
316}
317
318/// Serde helpers for URL-safe unpadded Base64 fields.
319///
320/// # Security
321///
322/// Deserialization uses the strict timing-variable decoder. Use this module
323/// for interoperability-oriented fields, not secret-bearing fields where
324/// malformed-input timing matters.
325#[cfg(feature = "alloc")]
326pub mod url_safe_no_pad {
327    use super::{URL_SAFE_NO_PAD, Vec, deserialize_with_engine, serialize_with_engine};
328    use serde::{Deserializer, Serializer};
329
330    /// Serializes bytes as URL-safe unpadded Base64 text.
331    ///
332    /// # Errors
333    ///
334    /// Returns the serializer's error if Base64 encoding fails or the
335    /// serializer rejects the string value.
336    pub fn serialize<S>(bytes: impl AsRef<[u8]>, serializer: S) -> Result<S::Ok, S::Error>
337    where
338        S: Serializer,
339    {
340        serialize_with_engine(URL_SAFE_NO_PAD, bytes.as_ref(), serializer)
341    }
342
343    /// Deserializes URL-safe unpadded Base64 text into owned bytes.
344    ///
345    /// # Errors
346    ///
347    /// Returns the deserializer's error if the value is not a string or if the
348    /// string is not valid URL-safe unpadded Base64.
349    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
350    where
351        D: Deserializer<'de>,
352    {
353        deserialize_with_engine(URL_SAFE_NO_PAD, deserializer)
354    }
355}
356
357/// Serde helpers for MIME Base64 fields with 76-column CRLF wrapping.
358///
359/// # Security
360///
361/// Deserialization uses the strict timing-variable decoder. Use this module
362/// for interoperability-oriented fields, not secret-bearing fields where
363/// malformed-input timing matters.
364#[cfg(feature = "alloc")]
365pub mod mime {
366    use super::{MIME, Vec, deserialize_with_profile, serialize_with_profile};
367    use serde::{Deserializer, Serializer};
368
369    /// Serializes bytes as MIME Base64 text with 76-column CRLF wrapping.
370    ///
371    /// # Errors
372    ///
373    /// Returns the serializer's error if Base64 encoding fails or the
374    /// serializer rejects the string value.
375    pub fn serialize<S>(bytes: impl AsRef<[u8]>, serializer: S) -> Result<S::Ok, S::Error>
376    where
377        S: Serializer,
378    {
379        serialize_with_profile(&MIME, bytes.as_ref(), serializer)
380    }
381
382    /// Deserializes MIME Base64 text into owned bytes.
383    ///
384    /// # Errors
385    ///
386    /// Returns the deserializer's error if the value is not a string or if the
387    /// string is not valid strict MIME Base64 for the configured wrapping
388    /// profile.
389    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
390    where
391        D: Deserializer<'de>,
392    {
393        deserialize_with_profile(&MIME, deserializer)
394    }
395}
396
397/// Serde helpers for PEM Base64 fields with 64-column LF wrapping.
398///
399/// # Security
400///
401/// Deserialization uses the strict timing-variable decoder. Use this module
402/// for interoperability-oriented fields, not secret-bearing fields where
403/// malformed-input timing matters.
404#[cfg(feature = "alloc")]
405pub mod pem {
406    use super::{PEM, Vec, deserialize_with_profile, serialize_with_profile};
407    use serde::{Deserializer, Serializer};
408
409    /// Serializes bytes as PEM Base64 text with 64-column LF wrapping.
410    ///
411    /// # Errors
412    ///
413    /// Returns the serializer's error if Base64 encoding fails or the
414    /// serializer rejects the string value.
415    pub fn serialize<S>(bytes: impl AsRef<[u8]>, serializer: S) -> Result<S::Ok, S::Error>
416    where
417        S: Serializer,
418    {
419        serialize_with_profile(&PEM, bytes.as_ref(), serializer)
420    }
421
422    /// Deserializes PEM Base64 text into owned bytes.
423    ///
424    /// # Errors
425    ///
426    /// Returns the deserializer's error if the value is not a string or if the
427    /// string is not valid strict PEM Base64 for the configured wrapping
428    /// profile.
429    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
430    where
431        D: Deserializer<'de>,
432    {
433        deserialize_with_profile(&PEM, deserializer)
434    }
435}
436
437#[cfg(feature = "alloc")]
438fn serialize_with_engine<A, const PAD: bool, S>(
439    engine: Engine<A, PAD>,
440    bytes: &[u8],
441    serializer: S,
442) -> Result<S::Ok, S::Error>
443where
444    A: base64_ng::Alphabet,
445    S: Serializer,
446{
447    let encoded = engine
448        .encode_string(bytes)
449        .map_err(serde::ser::Error::custom)?;
450    serializer.serialize_str(&encoded)
451}
452
453#[cfg(feature = "alloc")]
454fn serialize_with_profile<A, const PAD: bool, S>(
455    profile: &Profile<A, PAD>,
456    bytes: &[u8],
457    serializer: S,
458) -> Result<S::Ok, S::Error>
459where
460    A: Alphabet,
461    S: Serializer,
462{
463    let encoded = profile
464        .encode_string(bytes)
465        .map_err(serde::ser::Error::custom)?;
466    serializer.serialize_str(&encoded)
467}
468
469#[cfg(feature = "alloc")]
470fn deserialize_with_engine<'de, A, const PAD: bool, D>(
471    engine: Engine<A, PAD>,
472    deserializer: D,
473) -> Result<Vec<u8>, D::Error>
474where
475    A: base64_ng::Alphabet,
476    D: Deserializer<'de>,
477{
478    let encoded = String::deserialize(deserializer)?;
479    engine
480        .decode_vec(encoded.as_bytes())
481        .map_err(|error: DecodeError| D::Error::custom(error.kind()))
482}
483
484#[cfg(feature = "alloc")]
485fn deserialize_with_profile<'de, A, const PAD: bool, D>(
486    profile: &Profile<A, PAD>,
487    deserializer: D,
488) -> Result<Vec<u8>, D::Error>
489where
490    A: Alphabet,
491    D: Deserializer<'de>,
492{
493    let encoded = String::deserialize(deserializer)?;
494    profile
495        .decode_vec(encoded.as_bytes())
496        .map_err(|error: DecodeError| D::Error::custom(error.kind()))
497}