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}