Skip to main content

base64_ng/buffers/
secret.rs

1use crate::{constant_time_eq_public_len, wipe_vec_all, wipe_vec_spare_capacity};
2use alloc::{string::String, vec::Vec};
3
4/// Owned sensitive bytes with redacted formatting and drop-time cleanup.
5///
6/// `SecretBuffer` is available with the `alloc` feature. It is intended for
7/// decoded keys, tokens, and other values that should not be accidentally
8/// logged. The buffer exposes contents only through explicit reveal methods.
9///
10/// Spare vector capacity is cleared when wrapping owned bytes. On drop,
11/// initialized bytes and vector spare capacity are cleared with the crate's
12/// internal best-effort wipe helpers. This is data-retention reduction, not a
13/// formal zeroization guarantee, and it cannot make claims about allocator
14/// behavior or historical copies outside the wrapper.
15///
16/// # Platform Memory Controls
17///
18/// `SecretBuffer` does not lock its allocation into physical memory. The OS
19/// may page its contents to disk, include them in hibernation images, or expose
20/// them through crash dumps. High-assurance deployments must combine
21/// `SecretBuffer` with platform memory-locking where available, encrypted or
22/// disabled swap, crash-dump suppression, and allocator isolation appropriate
23/// for their environment.
24///
25/// On `wasm32` targets, the wipe barrier uses only a compiler fence. The wasm
26/// runtime JIT may still optimize or retain cleared bytes in ways this crate
27/// cannot control. `wasm32` builds fail closed by default; enable
28/// `allow-wasm32-best-effort-wipe` only when the deployment explicitly accepts
29/// this limitation and applies its own memory strategy around owned secret
30/// buffers.
31#[cfg(feature = "alloc")]
32pub struct SecretBuffer {
33    bytes: alloc::vec::Vec<u8>,
34}
35
36/// Owned secret bytes extracted from [`SecretBuffer`].
37///
38/// This wrapper keeps redacted formatting, best-effort spare-capacity clearing
39/// at construction time, and best-effort full wipe on drop after a
40/// [`SecretBuffer`] is consumed for owned interop. Use
41/// [`Self::into_exposed_unprotected_vec_caller_must_zeroize`] only when a raw
42/// `Vec<u8>` is unavoidable and the caller will handle cleanup.
43#[cfg(feature = "alloc")]
44pub struct ExposedSecretVec {
45    bytes: alloc::vec::Vec<u8>,
46}
47
48#[cfg(feature = "alloc")]
49impl ExposedSecretVec {
50    /// Wraps an owned vector as exposed secret material.
51    #[must_use]
52    pub fn from_vec(mut bytes: alloc::vec::Vec<u8>) -> Self {
53        wipe_vec_spare_capacity(&mut bytes);
54        Self { bytes }
55    }
56
57    /// Returns the number of initialized secret bytes.
58    #[must_use]
59    pub fn len(&self) -> usize {
60        self.bytes.len()
61    }
62
63    /// Returns whether the buffer contains no initialized secret bytes.
64    #[must_use]
65    pub fn is_empty(&self) -> bool {
66        self.bytes.is_empty()
67    }
68
69    /// Reveals the secret bytes.
70    ///
71    /// This method is intentionally named to make secret access explicit at the
72    /// call site.
73    #[must_use]
74    pub fn expose_secret(&self) -> &[u8] {
75        &self.bytes
76    }
77
78    /// Reveals the secret bytes mutably.
79    ///
80    /// This method is intentionally named to make secret access explicit at the
81    /// call site.
82    #[must_use]
83    pub fn expose_secret_mut(&mut self) -> &mut [u8] {
84        &mut self.bytes
85    }
86
87    /// Consumes the wrapper and returns a raw `Vec<u8>`.
88    ///
89    /// This is an unprotected escape hatch. The returned vector is no longer
90    /// redacted by formatting and will not be cleared by this crate on drop.
91    /// Callers must clear it with their own approved zeroization policy.
92    #[must_use = "caller must zeroize the returned Vec"]
93    pub fn into_exposed_unprotected_vec_caller_must_zeroize(mut self) -> alloc::vec::Vec<u8> {
94        core::mem::take(&mut self.bytes)
95    }
96}
97
98#[cfg(feature = "alloc")]
99impl core::fmt::Debug for ExposedSecretVec {
100    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
101        formatter
102            .debug_struct("ExposedSecretVec")
103            .field("bytes", &"<redacted>")
104            .field("len", &self.len())
105            .finish()
106    }
107}
108
109#[cfg(feature = "alloc")]
110impl core::fmt::Display for ExposedSecretVec {
111    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
112        formatter.write_str("<redacted>")
113    }
114}
115
116#[cfg(feature = "alloc")]
117impl Drop for ExposedSecretVec {
118    fn drop(&mut self) {
119        wipe_vec_all(&mut self.bytes);
120    }
121}
122
123#[cfg(feature = "alloc")]
124struct WipeVecGuard {
125    bytes: alloc::vec::Vec<u8>,
126}
127
128#[cfg(feature = "alloc")]
129impl WipeVecGuard {
130    fn from_vec(bytes: alloc::vec::Vec<u8>) -> Self {
131        Self { bytes }
132    }
133
134    fn into_validated_secret_string(
135        mut self,
136    ) -> Result<alloc::string::String, alloc::vec::Vec<u8>> {
137        wipe_vec_spare_capacity(&mut self.bytes);
138        let bytes = core::mem::take(&mut self.bytes);
139        string_from_validated_secret_bytes(bytes)
140    }
141}
142
143#[cfg(feature = "alloc")]
144impl Drop for WipeVecGuard {
145    fn drop(&mut self) {
146        wipe_vec_all(&mut self.bytes);
147    }
148}
149
150#[cfg(feature = "alloc")]
151impl AsRef<[u8]> for ExposedSecretVec {
152    fn as_ref(&self) -> &[u8] {
153        self.expose_secret()
154    }
155}
156
157#[cfg(feature = "alloc")]
158impl AsMut<[u8]> for ExposedSecretVec {
159    fn as_mut(&mut self) -> &mut [u8] {
160        self.expose_secret_mut()
161    }
162}
163
164/// Owned secret UTF-8 text extracted from [`SecretBuffer`].
165///
166/// This wrapper keeps redacted formatting, best-effort spare-capacity clearing
167/// at construction time, and best-effort full wipe on drop after a
168/// [`SecretBuffer`] is consumed for string interop. Use
169/// [`Self::into_exposed_unprotected_string_caller_must_zeroize`] only when a
170/// raw `String` is unavoidable and the caller will handle cleanup.
171#[cfg(feature = "alloc")]
172pub struct ExposedSecretString {
173    text: alloc::string::String,
174}
175
176#[cfg(feature = "alloc")]
177impl ExposedSecretString {
178    /// Wraps an owned UTF-8 string as exposed secret text.
179    #[must_use]
180    pub fn from_string(text: alloc::string::String) -> Self {
181        let mut bytes = text.into_bytes();
182        wipe_vec_spare_capacity(&mut bytes);
183        let text = match string_from_validated_secret_bytes(bytes) {
184            Ok(text) => text,
185            Err(mut bytes) => {
186                // This branch is unreachable for bytes produced from a valid
187                // `String`. If unsafe upstream code violates that invariant,
188                // wipe the bytes and fail closed without introducing a
189                // release-mode panic in secret cleanup code.
190                wipe_vec_all(&mut bytes);
191                alloc::string::String::new()
192            }
193        };
194        Self { text }
195    }
196
197    /// Returns the length of the secret text in bytes.
198    #[must_use]
199    pub fn len(&self) -> usize {
200        self.text.len()
201    }
202
203    /// Returns whether the secret text is empty.
204    #[must_use]
205    pub fn is_empty(&self) -> bool {
206        self.text.is_empty()
207    }
208
209    /// Reveals the secret text.
210    ///
211    /// This method is intentionally named to make secret access explicit at
212    /// the call site.
213    #[must_use]
214    pub fn expose_secret(&self) -> &str {
215        &self.text
216    }
217
218    /// Reveals the secret text as bytes.
219    ///
220    /// This method is intentionally named to make secret access explicit at
221    /// the call site.
222    #[must_use]
223    pub fn expose_secret_bytes(&self) -> &[u8] {
224        self.text.as_bytes()
225    }
226
227    /// Consumes the wrapper and returns a raw `String`.
228    ///
229    /// This is an unprotected escape hatch. The returned string is no longer
230    /// redacted by formatting and will not be cleared by this crate on drop.
231    /// Callers must clear it with their own approved zeroization policy.
232    #[must_use = "caller must zeroize the returned String"]
233    pub fn into_exposed_unprotected_string_caller_must_zeroize(mut self) -> alloc::string::String {
234        core::mem::take(&mut self.text)
235    }
236}
237
238#[cfg(feature = "alloc")]
239impl core::fmt::Debug for ExposedSecretString {
240    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
241        formatter
242            .debug_struct("ExposedSecretString")
243            .field("text", &"<redacted>")
244            .field("len", &self.len())
245            .finish()
246    }
247}
248
249#[cfg(feature = "alloc")]
250impl core::fmt::Display for ExposedSecretString {
251    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
252        formatter.write_str("<redacted>")
253    }
254}
255
256#[cfg(feature = "alloc")]
257impl Drop for ExposedSecretString {
258    fn drop(&mut self) {
259        let mut bytes = core::mem::take(&mut self.text).into_bytes();
260        wipe_vec_all(&mut bytes);
261    }
262}
263
264#[cfg(feature = "alloc")]
265impl AsRef<str> for ExposedSecretString {
266    fn as_ref(&self) -> &str {
267        self.expose_secret()
268    }
269}
270
271#[cfg(feature = "alloc")]
272impl SecretBuffer {
273    /// Wraps an existing vector as sensitive material.
274    #[must_use]
275    pub fn from_vec(mut bytes: alloc::vec::Vec<u8>) -> Self {
276        wipe_vec_spare_capacity(&mut bytes);
277        Self { bytes }
278    }
279
280    /// Copies a slice into an owned sensitive buffer.
281    #[must_use]
282    pub fn from_slice(bytes: &[u8]) -> Self {
283        Self::from_vec(bytes.to_vec())
284    }
285
286    /// Returns the number of initialized secret bytes.
287    #[must_use]
288    pub fn len(&self) -> usize {
289        self.bytes.len()
290    }
291
292    /// Returns whether the buffer contains no initialized secret bytes.
293    #[must_use]
294    pub fn is_empty(&self) -> bool {
295        self.bytes.is_empty()
296    }
297
298    /// Reveals the secret bytes.
299    ///
300    /// This method is intentionally named to make secret access explicit at the
301    /// call site.
302    #[must_use]
303    pub fn expose_secret(&self) -> &[u8] {
304        &self.bytes
305    }
306
307    /// Reveals the secret bytes as UTF-8 text.
308    ///
309    /// This method is intentionally named to make secret access explicit at the
310    /// call site. Secret material may be arbitrary binary data, so this method
311    /// is fallible.
312    pub fn expose_secret_utf8(&self) -> Result<&str, core::str::Utf8Error> {
313        core::str::from_utf8(self.expose_secret())
314    }
315
316    /// Reveals the secret bytes mutably.
317    ///
318    /// This method is intentionally named to make secret access explicit at the
319    /// call site.
320    #[must_use]
321    pub fn expose_secret_mut(&mut self) -> &mut [u8] {
322        &mut self.bytes
323    }
324
325    /// Consumes the wrapper and returns owned secret bytes.
326    ///
327    /// This is an explicit escape hatch for interop with APIs that require an
328    /// owned vector-like value. The returned [`ExposedSecretVec`] remains
329    /// redacted by formatting and clears its vector on drop.
330    #[must_use]
331    pub fn into_exposed_vec(mut self) -> ExposedSecretVec {
332        ExposedSecretVec::from_vec(core::mem::take(&mut self.bytes))
333    }
334
335    /// Consumes the wrapper and returns the owned secret bytes as UTF-8 text.
336    ///
337    /// This is an explicit escape hatch for interop with APIs that require an
338    /// owned string-like value. The returned [`ExposedSecretString`] remains
339    /// redacted by formatting and clears its heap allocation on drop.
340    ///
341    /// If the secret bytes are not valid UTF-8, the original redacted wrapper
342    /// is returned unchanged.
343    #[must_use = "handle invalid UTF-8 errors and keep the returned wrapper protected"]
344    pub fn try_into_exposed_string(self) -> Result<ExposedSecretString, Self> {
345        if core::str::from_utf8(self.expose_secret()).is_err() {
346            return Err(self);
347        }
348
349        // Keep the bytes behind a wiping guard until the final infallible
350        // ownership transfer into `String`.
351        let mut exposed = self.into_exposed_vec();
352        let guard = WipeVecGuard::from_vec(core::mem::take(&mut exposed.bytes));
353        drop(exposed);
354        match guard.into_validated_secret_string() {
355            Ok(text) => Ok(ExposedSecretString::from_string(text)),
356            Err(bytes) => Err(SecretBuffer::from_vec(bytes)),
357        }
358    }
359
360    /// Compares this secret to `other` without short-circuiting on the first
361    /// differing byte.
362    ///
363    /// Length and the final equality result remain public. Different lengths
364    /// return `false` immediately; use this helper only when the compared
365    /// lengths are public protocol facts or have been normalized by the
366    /// caller. For equal-length inputs, this helper scans every byte before
367    /// returning. It is constant-time-oriented best effort, not a formal
368    /// cryptographic constant-time guarantee. This comparison is deliberately
369    /// explicit: redacted buffer types do not implement [`PartialEq`] because
370    /// `==` would make a best-effort helper look like a formal token/MAC
371    /// comparison primitive.
372    ///
373    /// Do not use this helper as the sole MAC, bearer-token, password-hash, or
374    /// authentication-secret comparison primitive in high-assurance systems.
375    /// Applications that can admit dependencies should use a reviewed
376    /// constant-time comparison primitive, such as `subtle`, at the protocol
377    /// boundary.
378    #[doc(alias = "constant_time_eq")]
379    #[must_use]
380    pub fn constant_time_eq_public_len(&self, other: &[u8]) -> bool {
381        constant_time_eq_public_len(self.expose_secret(), other)
382    }
383
384    /// Clears the initialized bytes and makes the buffer empty.
385    pub fn clear(&mut self) {
386        wipe_vec_all(&mut self.bytes);
387        self.bytes.clear();
388    }
389}
390
391#[cfg(feature = "alloc")]
392impl core::fmt::Debug for SecretBuffer {
393    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
394        formatter
395            .debug_struct("SecretBuffer")
396            .field("bytes", &"<redacted>")
397            .field("len", &self.len())
398            .finish()
399    }
400}
401
402#[cfg(feature = "alloc")]
403impl core::fmt::Display for SecretBuffer {
404    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
405        formatter.write_str("<redacted>")
406    }
407}
408
409#[cfg(feature = "alloc")]
410impl Drop for SecretBuffer {
411    fn drop(&mut self) {
412        wipe_vec_all(&mut self.bytes);
413    }
414}
415
416#[cfg(feature = "alloc")]
417fn string_from_validated_secret_bytes(bytes: Vec<u8>) -> Result<String, Vec<u8>> {
418    String::from_utf8(bytes).map_err(alloc::string::FromUtf8Error::into_bytes)
419}