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