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}