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