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}