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}