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    ///
180    /// Safe Rust guarantees that `String` contains valid UTF-8. If unsafe
181    /// upstream code has violated that invariant before calling this function,
182    /// this helper wipes the invalid bytes and returns an empty fail-closed
183    /// wrapper. Do not treat unsafe-created invalid `String` values as
184    /// protocol data.
185    #[must_use]
186    pub fn from_string(text: alloc::string::String) -> Self {
187        let mut bytes = text.into_bytes();
188        wipe_vec_spare_capacity(&mut bytes);
189        let text = match string_from_validated_secret_bytes(bytes) {
190            Ok(text) => text,
191            Err(mut bytes) => {
192                // This branch is unreachable for bytes produced from a valid
193                // `String`. If unsafe upstream code violates that invariant,
194                // wipe the bytes and fail closed without introducing a
195                // release-mode panic in secret cleanup code.
196                wipe_vec_all(&mut bytes);
197                alloc::string::String::new()
198            }
199        };
200        Self { text }
201    }
202
203    /// Returns the length of the secret text in bytes.
204    #[must_use]
205    pub fn len(&self) -> usize {
206        self.text.len()
207    }
208
209    /// Returns whether the secret text is empty.
210    #[must_use]
211    pub fn is_empty(&self) -> bool {
212        self.text.is_empty()
213    }
214
215    /// Reveals the secret text.
216    ///
217    /// This method is intentionally named to make secret access explicit at
218    /// the call site.
219    #[must_use]
220    pub fn expose_secret(&self) -> &str {
221        &self.text
222    }
223
224    /// Reveals the secret text as bytes.
225    ///
226    /// This method is intentionally named to make secret access explicit at
227    /// the call site.
228    #[must_use]
229    pub fn expose_secret_bytes(&self) -> &[u8] {
230        self.text.as_bytes()
231    }
232
233    /// Consumes the wrapper and returns a raw `String`.
234    ///
235    /// This is an unprotected escape hatch. The returned string is no longer
236    /// redacted by formatting and will not be cleared by this crate on drop.
237    /// Callers must clear it with their own approved zeroization policy.
238    #[must_use = "caller must zeroize the returned String"]
239    pub fn into_exposed_unprotected_string_caller_must_zeroize(mut self) -> alloc::string::String {
240        core::mem::take(&mut self.text)
241    }
242}
243
244#[cfg(feature = "alloc")]
245impl core::fmt::Debug for ExposedSecretString {
246    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
247        formatter
248            .debug_struct("ExposedSecretString")
249            .field("text", &"<redacted>")
250            .field("len", &self.len())
251            .finish()
252    }
253}
254
255#[cfg(feature = "alloc")]
256impl core::fmt::Display for ExposedSecretString {
257    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
258        formatter.write_str("<redacted>")
259    }
260}
261
262#[cfg(feature = "alloc")]
263impl Drop for ExposedSecretString {
264    fn drop(&mut self) {
265        let mut bytes = core::mem::take(&mut self.text).into_bytes();
266        wipe_vec_all(&mut bytes);
267    }
268}
269
270#[cfg(feature = "alloc")]
271impl AsRef<str> for ExposedSecretString {
272    fn as_ref(&self) -> &str {
273        self.expose_secret()
274    }
275}
276
277#[cfg(feature = "alloc")]
278impl SecretBuffer {
279    /// Wraps an existing vector as sensitive material.
280    #[must_use]
281    pub fn from_vec(mut bytes: alloc::vec::Vec<u8>) -> Self {
282        wipe_vec_spare_capacity(&mut bytes);
283        Self { bytes }
284    }
285
286    /// Copies a slice into an owned sensitive buffer.
287    #[must_use]
288    pub fn from_slice(bytes: &[u8]) -> Self {
289        Self::from_vec(bytes.to_vec())
290    }
291
292    /// Returns the number of initialized secret bytes.
293    #[must_use]
294    pub fn len(&self) -> usize {
295        self.bytes.len()
296    }
297
298    /// Returns whether the buffer contains no initialized secret bytes.
299    #[must_use]
300    pub fn is_empty(&self) -> bool {
301        self.bytes.is_empty()
302    }
303
304    /// Reveals the secret bytes.
305    ///
306    /// This method is intentionally named to make secret access explicit at the
307    /// call site.
308    #[must_use]
309    pub fn expose_secret(&self) -> &[u8] {
310        &self.bytes
311    }
312
313    /// Reveals the secret bytes as UTF-8 text.
314    ///
315    /// This method is intentionally named to make secret access explicit at the
316    /// call site. Secret material may be arbitrary binary data, so this method
317    /// is fallible.
318    pub fn expose_secret_utf8(&self) -> Result<&str, core::str::Utf8Error> {
319        core::str::from_utf8(self.expose_secret())
320    }
321
322    /// Reveals the secret bytes mutably.
323    ///
324    /// This method is intentionally named to make secret access explicit at the
325    /// call site.
326    #[must_use]
327    pub fn expose_secret_mut(&mut self) -> &mut [u8] {
328        &mut self.bytes
329    }
330
331    /// Consumes the wrapper and returns owned secret bytes.
332    ///
333    /// This is an explicit escape hatch for interop with APIs that require an
334    /// owned vector-like value. The returned [`ExposedSecretVec`] remains
335    /// redacted by formatting and clears its vector on drop.
336    #[must_use]
337    pub fn into_exposed_vec(mut self) -> ExposedSecretVec {
338        ExposedSecretVec::from_vec(core::mem::take(&mut self.bytes))
339    }
340
341    /// Consumes the wrapper and returns the owned secret bytes as UTF-8 text.
342    ///
343    /// This is an explicit escape hatch for interop with APIs that require an
344    /// owned string-like value. The returned [`ExposedSecretString`] remains
345    /// redacted by formatting and clears its heap allocation on drop.
346    ///
347    /// If the secret bytes are not valid UTF-8, the original redacted wrapper
348    /// is returned unchanged.
349    #[must_use = "handle invalid UTF-8 errors and keep the returned wrapper protected"]
350    pub fn try_into_exposed_string(self) -> Result<ExposedSecretString, Self> {
351        if core::str::from_utf8(self.expose_secret()).is_err() {
352            return Err(self);
353        }
354
355        // Keep the bytes behind a wiping guard until the final infallible
356        // ownership transfer into `String`.
357        let mut exposed = self.into_exposed_vec();
358        let guard = WipeVecGuard::from_vec(core::mem::take(&mut exposed.bytes));
359        drop(exposed);
360        match guard.into_validated_secret_string() {
361            Ok(text) => Ok(ExposedSecretString::from_string(text)),
362            Err(bytes) => Err(SecretBuffer::from_vec(bytes)),
363        }
364    }
365
366    /// Compares this secret to `other` without short-circuiting on the first
367    /// differing byte.
368    ///
369    /// Length and the final equality result remain public. Different lengths
370    /// return `false` immediately; use this helper only when the compared
371    /// lengths are public protocol facts or have been normalized by the
372    /// caller. For equal-length inputs, this helper scans every byte before
373    /// returning. It is constant-time-oriented best effort, not a formal
374    /// cryptographic constant-time guarantee. This comparison is deliberately
375    /// explicit: redacted buffer types do not implement [`PartialEq`] because
376    /// `==` would make a best-effort helper look like a formal token/MAC
377    /// comparison primitive.
378    ///
379    /// Do not use this helper as the sole MAC, bearer-token, password-hash, or
380    /// authentication-secret comparison primitive in high-assurance systems.
381    /// Applications that can admit dependencies should use a reviewed
382    /// constant-time comparison primitive, such as `subtle`, at the protocol
383    /// boundary.
384    #[doc(alias = "constant_time_eq")]
385    #[must_use]
386    pub fn constant_time_eq_public_len(&self, other: &[u8]) -> bool {
387        constant_time_eq_public_len(self.expose_secret(), other)
388    }
389
390    /// Clears the initialized bytes and makes the buffer empty.
391    pub fn clear(&mut self) {
392        wipe_vec_all(&mut self.bytes);
393        self.bytes.clear();
394    }
395}
396
397#[cfg(feature = "alloc")]
398impl core::fmt::Debug for SecretBuffer {
399    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
400        formatter
401            .debug_struct("SecretBuffer")
402            .field("bytes", &"<redacted>")
403            .field("len", &self.len())
404            .finish()
405    }
406}
407
408#[cfg(feature = "alloc")]
409impl core::fmt::Display for SecretBuffer {
410    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
411        formatter.write_str("<redacted>")
412    }
413}
414
415#[cfg(feature = "alloc")]
416impl Drop for SecretBuffer {
417    fn drop(&mut self) {
418        wipe_vec_all(&mut self.bytes);
419    }
420}
421
422#[cfg(feature = "alloc")]
423fn string_from_validated_secret_bytes(bytes: Vec<u8>) -> Result<String, Vec<u8>> {
424    String::from_utf8(bytes).map_err(alloc::string::FromUtf8Error::into_bytes)
425}