secure_gate/encoding/
base64.rs

1// Forbid unsafe_code when the "zeroize" feature is disabled, to ensure secure handling
2#![cfg_attr(not(feature = "zeroize"), forbid(unsafe_code))]
3use alloc::string::String;
4use base64::engine::general_purpose::URL_SAFE_NO_PAD;
5use base64::Engine;
6
7fn zeroize_input(s: &mut String) {
8    #[cfg(feature = "zeroize")]
9    {
10        zeroize::Zeroize::zeroize(s);
11    }
12    #[cfg(not(feature = "zeroize"))]
13    {
14        let _ = s; // Suppress unused variable warning when zeroize is disabled
15    }
16}
17
18/// Validated, URL-safe base64 string wrapper for secret data (no padding).
19///
20/// This struct ensures the contained string is valid URL-safe base64.
21/// Provides methods for decoding back to bytes.
22///
23/// # Examples
24///
25/// ```
26/// # use secure_gate::encoding::base64::Base64String;
27/// let valid = Base64String::new("SGVsbG8".to_string()).unwrap();
28/// assert_eq!(valid.expose_secret(), "SGVsbG8");
29/// let bytes = valid.into_bytes(); // Vec<u8> of "Hello"
30/// ```
31pub struct Base64String(pub(crate) crate::Dynamic<String>);
32
33impl Base64String {
34    /// Create a new `Base64String` from a `String`, validating it as URL-safe base64 (no padding).
35    ///
36    /// The input `String` is consumed.
37    ///
38    /// # Security Note
39    ///
40    /// **Invalid inputs are only securely zeroized if the `zeroize` feature is enabled.**
41    /// Without `zeroize`, rejected bytes may remain in memory until the `String` is dropped
42    /// normally. Enable the `zeroize` feature for secure wiping of invalid inputs.
43    ///
44    /// Validation rules:
45    /// - Valid URL-safe base64 characters (A-Z, a-z, 0-9, -, _)
46    /// - No padding ('=' not allowed, as we use no-pad)
47    /// - Must be decodable as valid base64 (prevents `to_bytes()` panics)
48    ///
49    /// # Errors
50    ///
51    /// Returns `Err("invalid base64 string")` if validation fails.
52    ///
53    /// # Example
54    ///
55    /// ```
56    /// # #[cfg(feature = "encoding-base64")]
57    /// # {
58    /// use secure_gate::encoding::base64::Base64String;
59    /// let valid = Base64String::new("SGVsbG8".to_string()).unwrap();
60    /// assert_eq!(valid.expose_secret(), "SGVsbG8");
61    /// let bytes = valid.into_bytes(); // Vec<u8> of "Hello"
62    /// # }
63    /// ```
64    pub fn new(s: String) -> Result<Self, &'static str> {
65        if URL_SAFE_NO_PAD.decode(&s).is_ok() {
66            Ok(Self(crate::Dynamic::new(s)))
67        } else {
68            let mut s = s;
69            zeroize_input(&mut s);
70            Err("invalid base64 string")
71        }
72    }
73
74    /// Internal constructor for trusted base64 strings (e.g., from RNG).
75    ///
76    /// Skips validation – caller must ensure the string is valid base64.
77    #[allow(dead_code)]
78    pub(crate) fn new_unchecked(s: String) -> Self {
79        Self(crate::Dynamic::new(s))
80    }
81
82    /// Exact number of bytes the decoded base64 string represents.
83    #[inline(always)]
84    pub fn byte_len(&self) -> usize {
85        let len = self.len();
86        (len / 4) * 3 + (len % 4 == 2) as usize + (len % 4 == 3) as usize * 2
87    }
88
89    /// Length of the encoded string (in characters).
90    #[inline(always)]
91    pub const fn len(&self) -> usize {
92        self.0.len()
93    }
94
95    /// Whether the encoded string is empty.
96    #[inline(always)]
97    pub const fn is_empty(&self) -> bool {
98        self.0.is_empty()
99    }
100}
101
102// Constant-time equality for base64 strings – prevents timing attacks when ct-eq enabled
103impl PartialEq for Base64String {
104    fn eq(&self, other: &Self) -> bool {
105        #[cfg(feature = "ct-eq")]
106        {
107            use crate::ct_eq::ConstantTimeEq;
108            self.0
109                .expose_secret()
110                .as_bytes()
111                .ct_eq(other.0.expose_secret().as_bytes())
112        }
113        #[cfg(not(feature = "ct-eq"))]
114        {
115            self.0.expose_secret() == other.0.expose_secret()
116        }
117    }
118}
119
120impl Eq for Base64String {}
121
122/// Debug implementation (always redacted).
123impl core::fmt::Debug for Base64String {
124    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
125        f.write_str("[REDACTED]")
126    }
127}