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}