base64_ng/buffers.rs
1//! Stack-backed and owned buffer wrappers.
2
3use crate::{
4 DecodeError, EncodeError, STANDARD, constant_time_eq_public_len, wipe_bytes, wipe_tail,
5};
6#[cfg(feature = "alloc")]
7use crate::{wipe_vec_all, wipe_vec_spare_capacity};
8#[cfg(feature = "alloc")]
9use alloc::{string::String, vec::Vec};
10
11/// Stack-backed encoded Base64 output.
12///
13/// This type is intended for short values where heap allocation would be
14/// unnecessary but manually sizing and passing a separate output slice is
15/// noisy. Its visible bytes are produced by crate encoders, so [`Self::as_str`]
16/// can return `&str` without exposing a fallible UTF-8 conversion to callers.
17///
18/// The backing array is cleared when the value is dropped. This is best-effort
19/// data-retention reduction and is not a formal zeroization guarantee.
20///
21/// On `wasm32` targets, the wipe barrier uses only a compiler fence. The wasm
22/// runtime JIT may still optimize or retain cleared bytes in ways this crate
23/// cannot control. `wasm32` builds fail closed by default; enable
24/// `allow-wasm32-best-effort-wipe` only when the deployment explicitly accepts
25/// this limitation and applies its own memory strategy around stack-backed
26/// buffers.
27pub struct EncodedBuffer<const CAP: usize> {
28 bytes: [u8; CAP],
29 len: usize,
30}
31
32/// Owned stack array extracted from [`EncodedBuffer`].
33///
34/// This wrapper keeps the extracted encoded bytes on the crate's best-effort
35/// drop-time cleanup path. Use
36/// [`Self::into_exposed_unprotected_array_caller_must_zeroize`] only when a
37/// bare array is unavoidable and the caller will handle cleanup.
38pub struct ExposedEncodedArray<const CAP: usize> {
39 bytes: [u8; CAP],
40 len: usize,
41}
42
43impl<const CAP: usize> ExposedEncodedArray<CAP> {
44 /// Wraps an encoded backing array and visible length.
45 ///
46 /// # Panics
47 ///
48 /// Panics if `len` is greater than `CAP`.
49 #[must_use]
50 pub const fn from_array(bytes: [u8; CAP], len: usize) -> Self {
51 assert!(len <= CAP, "visible length exceeds array capacity");
52 Self { bytes, len }
53 }
54
55 /// Returns the visible encoded bytes.
56 #[must_use]
57 pub fn as_bytes(&self) -> &[u8] {
58 &self.bytes[..self.len]
59 }
60
61 /// Returns the number of visible encoded bytes.
62 #[must_use]
63 pub const fn len(&self) -> usize {
64 self.len
65 }
66
67 /// Returns whether there are no visible encoded bytes.
68 #[must_use]
69 pub const fn is_empty(&self) -> bool {
70 self.len == 0
71 }
72
73 /// Returns the backing array capacity.
74 #[must_use]
75 pub const fn capacity(&self) -> usize {
76 CAP
77 }
78
79 /// Consumes the wrapper and returns a bare array plus visible length.
80 ///
81 /// This is an unprotected escape hatch. The returned array will not be
82 /// cleared by this crate on drop. Callers must clear it with their own
83 /// approved zeroization policy.
84 ///
85 /// # Security
86 ///
87 /// Treat this as a cleanup-boundary API. Failing to clear the returned
88 /// array leaves the encoded bytes in ordinary caller-owned memory until
89 /// overwritten by later stack or heap activity.
90 #[must_use = "caller must zeroize the returned array"]
91 pub fn into_exposed_unprotected_array_caller_must_zeroize(mut self) -> ([u8; CAP], usize) {
92 let len = self.len;
93 self.len = 0;
94 (core::mem::replace(&mut self.bytes, [0u8; CAP]), len)
95 }
96}
97
98impl<const CAP: usize> Drop for ExposedEncodedArray<CAP> {
99 fn drop(&mut self) {
100 wipe_bytes(&mut self.bytes);
101 self.len = 0;
102 }
103}
104
105impl<const CAP: usize> core::fmt::Debug for ExposedEncodedArray<CAP> {
106 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
107 formatter
108 .debug_struct("ExposedEncodedArray")
109 .field("bytes", &"<redacted>")
110 .field("len", &self.len)
111 .field("capacity", &CAP)
112 .finish()
113 }
114}
115
116impl<const CAP: usize> EncodedBuffer<CAP> {
117 /// Creates an empty encoded buffer.
118 #[must_use]
119 pub const fn new() -> Self {
120 Self {
121 bytes: [0u8; CAP],
122 len: 0,
123 }
124 }
125
126 /// Returns the full backing array as an output slice for crate-internal
127 /// encode paths.
128 pub(crate) fn as_mut_capacity(&mut self) -> &mut [u8] {
129 &mut self.bytes
130 }
131
132 /// Sets the visible length after a crate-internal encode path succeeds.
133 pub(crate) fn set_filled(&mut self, written: usize) -> Result<(), EncodeError> {
134 debug_assert!(
135 written <= CAP,
136 "encoder wrote past stack-backed buffer capacity"
137 );
138 if written > CAP {
139 self.clear();
140 return Err(EncodeError::OutputTooSmall {
141 required: written,
142 available: CAP,
143 });
144 }
145 self.len = written;
146 Ok(())
147 }
148
149 /// Returns the number of visible encoded bytes.
150 #[must_use]
151 pub const fn len(&self) -> usize {
152 self.len
153 }
154
155 /// Returns whether the buffer has no visible encoded bytes.
156 #[must_use]
157 pub const fn is_empty(&self) -> bool {
158 self.len == 0
159 }
160
161 /// Returns whether the visible encoded bytes fill the stack backing array.
162 #[must_use]
163 pub const fn is_full(&self) -> bool {
164 self.len == CAP
165 }
166
167 /// Returns the stack capacity in bytes.
168 #[must_use]
169 pub const fn capacity(&self) -> usize {
170 CAP
171 }
172
173 /// Returns the number of unused bytes in the stack backing array.
174 #[must_use]
175 pub const fn remaining_capacity(&self) -> usize {
176 CAP - self.len
177 }
178
179 /// Returns the visible encoded bytes.
180 #[must_use]
181 pub fn as_bytes(&self) -> &[u8] {
182 &self.bytes[..self.len]
183 }
184
185 /// Returns the visible encoded bytes as UTF-8 text.
186 ///
187 /// Encoded Base64 output is produced as ASCII by this crate, so this
188 /// method should not fail unless an internal invariant has been broken.
189 /// It is provided for callers that prefer a fallible accessor over
190 /// [`Self::as_str`].
191 pub fn as_utf8(&self) -> Result<&str, core::str::Utf8Error> {
192 core::str::from_utf8(self.as_bytes())
193 }
194
195 /// Returns the visible encoded bytes as UTF-8.
196 ///
197 /// # Panics
198 ///
199 /// Panics only if the crate's internal invariant is broken and the buffer
200 /// contains non-UTF-8 bytes.
201 #[must_use]
202 pub fn as_str(&self) -> &str {
203 match self.as_utf8() {
204 Ok(output) => output,
205 Err(_) => unreachable!("base64 encoder produced non-UTF-8 output"),
206 }
207 }
208
209 /// Compares this encoded output to `other` without short-circuiting on the
210 /// first differing byte.
211 ///
212 /// Length and the final equality result remain public. Different lengths
213 /// return `false` immediately; use this helper only when the compared
214 /// lengths are public protocol facts or have been normalized by the
215 /// caller. For equal-length inputs, this helper scans every byte before
216 /// returning. It is constant-time-oriented best effort, not a formal
217 /// cryptographic constant-time guarantee. This comparison is deliberately
218 /// explicit: redacted buffer types do not implement [`PartialEq`] because
219 /// `==` would make a best-effort helper look like a formal token/MAC
220 /// comparison primitive.
221 ///
222 /// Do not use this helper as the sole MAC, bearer-token, password-hash, or
223 /// authentication-secret comparison primitive in high-assurance systems.
224 /// Applications that can admit dependencies should use a reviewed
225 /// constant-time comparison primitive, such as `subtle`, at the protocol
226 /// boundary.
227 #[doc(alias = "constant_time_eq")]
228 #[must_use]
229 pub fn constant_time_eq_public_len(&self, other: &[u8]) -> bool {
230 constant_time_eq_public_len(self.as_bytes(), other)
231 }
232
233 /// Consumes the wrapper and returns the backing array plus visible length
234 /// inside a drop-wiping exposed wrapper.
235 ///
236 /// This is an explicit escape hatch for no-alloc interop with APIs that
237 /// require ownership of a fixed array. The returned
238 /// [`ExposedEncodedArray`] remains redacted by formatting and clears its
239 /// backing array on drop.
240 #[must_use]
241 pub fn into_exposed_array(mut self) -> ExposedEncodedArray<CAP> {
242 let len = self.len;
243 self.len = 0;
244 ExposedEncodedArray::from_array(core::mem::replace(&mut self.bytes, [0u8; CAP]), len)
245 }
246
247 /// Clears the visible bytes and the full backing array.
248 pub fn clear(&mut self) {
249 wipe_bytes(&mut self.bytes);
250 self.len = 0;
251 }
252
253 /// Clears bytes after the visible prefix.
254 pub fn clear_tail(&mut self) {
255 wipe_tail(&mut self.bytes, self.len);
256 }
257}
258
259impl<const CAP: usize> AsRef<[u8]> for EncodedBuffer<CAP> {
260 fn as_ref(&self) -> &[u8] {
261 self.as_bytes()
262 }
263}
264
265impl<const CAP: usize> Clone for EncodedBuffer<CAP> {
266 /// Clones the visible encoded bytes into a second stack-backed buffer.
267 ///
268 /// Security note: cloning duplicates the visible bytes in memory. Both the
269 /// original and the clone must be dropped or explicitly cleared before the
270 /// duplicated bytes are gone on the crate's best-effort cleanup path. The
271 /// compiler may also create temporary stack copies while performing the
272 /// copy; those intermediates are outside this crate's cleanup boundary.
273 /// Avoid cloning encoded secret material; use `SecretBuffer` when redacted
274 /// formatting and heap-owned secret handling are required.
275 fn clone(&self) -> Self {
276 let mut output = Self::new();
277 output.bytes[..self.len].copy_from_slice(self.as_bytes());
278 output.len = self.len;
279 output
280 }
281}
282
283impl<const CAP: usize> core::fmt::Debug for EncodedBuffer<CAP> {
284 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
285 formatter
286 .debug_struct("EncodedBuffer")
287 .field("bytes", &"<redacted>")
288 .field("len", &self.len)
289 .field("capacity", &CAP)
290 .finish()
291 }
292}
293
294impl<const CAP: usize> core::fmt::Display for EncodedBuffer<CAP> {
295 /// Writes the full Base64 text.
296 ///
297 /// Security note: this is intentionally not redacted. Do not use
298 /// `EncodedBuffer` for encoded secrets that may reach logs or error
299 /// messages; use `SecretBuffer` for redacted formatting.
300 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
301 formatter.write_str(self.as_str())
302 }
303}
304
305impl<const CAP: usize> Default for EncodedBuffer<CAP> {
306 fn default() -> Self {
307 Self::new()
308 }
309}
310
311impl<const CAP: usize> Drop for EncodedBuffer<CAP> {
312 fn drop(&mut self) {
313 self.clear();
314 }
315}
316
317impl<const CAP: usize> TryFrom<&[u8]> for EncodedBuffer<CAP> {
318 type Error = EncodeError;
319
320 /// Encodes bytes into strict standard padded Base64 in a stack-backed
321 /// buffer.
322 ///
323 /// Use [`crate::Engine::encode_buffer`] or [`crate::Profile::encode_buffer`] when a
324 /// different alphabet, padding mode, or line-wrapping profile is required.
325 fn try_from(input: &[u8]) -> Result<Self, Self::Error> {
326 STANDARD.encode_buffer(input)
327 }
328}
329
330impl<const CAP: usize, const N: usize> TryFrom<&[u8; N]> for EncodedBuffer<CAP> {
331 type Error = EncodeError;
332
333 /// Encodes a byte array into strict standard padded Base64 in a
334 /// stack-backed buffer.
335 ///
336 /// Use [`crate::Engine::encode_buffer`] or [`crate::Profile::encode_buffer`] when a
337 /// different alphabet, padding mode, or line-wrapping profile is required.
338 fn try_from(input: &[u8; N]) -> Result<Self, Self::Error> {
339 Self::try_from(&input[..])
340 }
341}
342
343impl<const CAP: usize> TryFrom<&str> for EncodedBuffer<CAP> {
344 type Error = EncodeError;
345
346 /// Encodes UTF-8 text bytes into strict standard padded Base64 in a
347 /// stack-backed buffer.
348 ///
349 /// This treats the string as raw input bytes. Use
350 /// [`crate::Engine::encode_buffer`] or [`crate::Profile::encode_buffer`] when a
351 /// different alphabet, padding mode, or line-wrapping profile is required.
352 fn try_from(input: &str) -> Result<Self, Self::Error> {
353 Self::try_from(input.as_bytes())
354 }
355}
356
357/// Stack-backed decoded Base64 output.
358///
359/// This type is intended for short decoded values where heap allocation would
360/// be unnecessary but manually sizing and passing a separate output slice is
361/// noisy. Decoded data may be binary or secret-bearing, so formatting is
362/// redacted and contents are exposed only through explicit byte accessors.
363///
364/// The backing array is cleared when the value is dropped. This is best-effort
365/// data-retention reduction and is not a formal zeroization guarantee.
366///
367/// On `wasm32` targets, the wipe barrier uses only a compiler fence. The wasm
368/// runtime JIT may still optimize or retain cleared bytes in ways this crate
369/// cannot control. `wasm32` builds fail closed by default; enable
370/// `allow-wasm32-best-effort-wipe` only when the deployment explicitly accepts
371/// this limitation and applies its own memory strategy around stack-backed
372/// buffers.
373pub struct DecodedBuffer<const CAP: usize> {
374 bytes: [u8; CAP],
375 len: usize,
376}
377
378/// Owned stack array extracted from [`DecodedBuffer`].
379///
380/// This wrapper keeps the extracted decoded bytes on the crate's best-effort
381/// drop-time cleanup path. Use
382/// [`Self::into_exposed_unprotected_array_caller_must_zeroize`] only when a
383/// bare array is unavoidable and the caller will handle cleanup.
384pub struct ExposedDecodedArray<const CAP: usize> {
385 bytes: [u8; CAP],
386 len: usize,
387}
388
389impl<const CAP: usize> ExposedDecodedArray<CAP> {
390 /// Wraps a decoded backing array and visible length.
391 ///
392 /// # Panics
393 ///
394 /// Panics if `len` is greater than `CAP`.
395 #[must_use]
396 pub const fn from_array(bytes: [u8; CAP], len: usize) -> Self {
397 assert!(len <= CAP, "visible length exceeds array capacity");
398 Self { bytes, len }
399 }
400
401 /// Returns the visible decoded bytes.
402 #[must_use]
403 pub fn as_bytes(&self) -> &[u8] {
404 &self.bytes[..self.len]
405 }
406
407 /// Returns the number of visible decoded bytes.
408 #[must_use]
409 pub const fn len(&self) -> usize {
410 self.len
411 }
412
413 /// Returns whether there are no visible decoded bytes.
414 #[must_use]
415 pub const fn is_empty(&self) -> bool {
416 self.len == 0
417 }
418
419 /// Returns the backing array capacity.
420 #[must_use]
421 pub const fn capacity(&self) -> usize {
422 CAP
423 }
424
425 /// Consumes the wrapper and returns a bare array plus visible length.
426 ///
427 /// This is an unprotected escape hatch. The returned array will not be
428 /// cleared by this crate on drop. Callers must clear it with their own
429 /// approved zeroization policy.
430 ///
431 /// # Security
432 ///
433 /// Treat this as a cleanup-boundary API. Failing to clear the returned
434 /// array leaves decoded bytes, which may be secret-bearing, in ordinary
435 /// caller-owned memory until overwritten by later stack or heap activity.
436 #[must_use = "caller must zeroize the returned array"]
437 pub fn into_exposed_unprotected_array_caller_must_zeroize(mut self) -> ([u8; CAP], usize) {
438 let len = self.len;
439 self.len = 0;
440 (core::mem::replace(&mut self.bytes, [0u8; CAP]), len)
441 }
442}
443
444impl<const CAP: usize> Drop for ExposedDecodedArray<CAP> {
445 fn drop(&mut self) {
446 wipe_bytes(&mut self.bytes);
447 self.len = 0;
448 }
449}
450
451impl<const CAP: usize> core::fmt::Debug for ExposedDecodedArray<CAP> {
452 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
453 formatter
454 .debug_struct("ExposedDecodedArray")
455 .field("bytes", &"<redacted>")
456 .field("len", &self.len)
457 .field("capacity", &CAP)
458 .finish()
459 }
460}
461
462impl<const CAP: usize> DecodedBuffer<CAP> {
463 /// Creates an empty decoded buffer.
464 #[must_use]
465 pub const fn new() -> Self {
466 Self {
467 bytes: [0u8; CAP],
468 len: 0,
469 }
470 }
471
472 /// Returns the full backing array as an output slice for crate-internal
473 /// decode paths.
474 pub(crate) fn as_mut_capacity(&mut self) -> &mut [u8] {
475 &mut self.bytes
476 }
477
478 /// Sets the visible length after a crate-internal decode path succeeds.
479 pub(crate) fn set_filled(&mut self, written: usize) -> Result<(), DecodeError> {
480 debug_assert!(
481 written <= CAP,
482 "decoder wrote past stack-backed buffer capacity"
483 );
484 if written > CAP {
485 self.clear();
486 return Err(DecodeError::OutputTooSmall {
487 required: written,
488 available: CAP,
489 });
490 }
491 self.len = written;
492 Ok(())
493 }
494
495 /// Returns the number of visible decoded bytes.
496 #[must_use]
497 pub const fn len(&self) -> usize {
498 self.len
499 }
500
501 /// Returns whether the buffer has no visible decoded bytes.
502 #[must_use]
503 pub const fn is_empty(&self) -> bool {
504 self.len == 0
505 }
506
507 /// Returns whether the visible decoded bytes fill the stack backing array.
508 #[must_use]
509 pub const fn is_full(&self) -> bool {
510 self.len == CAP
511 }
512
513 /// Returns the stack capacity in bytes.
514 #[must_use]
515 pub const fn capacity(&self) -> usize {
516 CAP
517 }
518
519 /// Returns the number of unused bytes in the stack backing array.
520 #[must_use]
521 pub const fn remaining_capacity(&self) -> usize {
522 CAP - self.len
523 }
524
525 /// Returns the visible decoded bytes.
526 #[must_use]
527 pub fn as_bytes(&self) -> &[u8] {
528 &self.bytes[..self.len]
529 }
530
531 /// Returns the visible decoded bytes as UTF-8 text.
532 ///
533 /// Decoded Base64 output is arbitrary bytes, so this method is fallible.
534 /// Use [`Self::as_bytes`] when the decoded payload is binary or when text
535 /// validation belongs to a higher protocol layer.
536 pub fn as_utf8(&self) -> Result<&str, core::str::Utf8Error> {
537 core::str::from_utf8(self.as_bytes())
538 }
539
540 /// Compares this decoded output to `other` without short-circuiting on the
541 /// first differing byte.
542 ///
543 /// Length and the final equality result remain public. Different lengths
544 /// return `false` immediately; use this helper only when the compared
545 /// lengths are public protocol facts or have been normalized by the
546 /// caller. For equal-length inputs, this helper scans every byte before
547 /// returning. It is constant-time-oriented best effort, not a formal
548 /// cryptographic constant-time guarantee. This comparison is deliberately
549 /// explicit: redacted buffer types do not implement [`PartialEq`] because
550 /// `==` would make a best-effort helper look like a formal token/MAC
551 /// comparison primitive.
552 ///
553 /// Do not use this helper as the sole MAC, bearer-token, password-hash, or
554 /// authentication-secret comparison primitive in high-assurance systems.
555 /// Applications that can admit dependencies should use a reviewed
556 /// constant-time comparison primitive, such as `subtle`, at the protocol
557 /// boundary.
558 #[doc(alias = "constant_time_eq")]
559 #[must_use]
560 pub fn constant_time_eq_public_len(&self, other: &[u8]) -> bool {
561 constant_time_eq_public_len(self.as_bytes(), other)
562 }
563
564 /// Consumes the wrapper and returns the backing array plus visible length
565 /// inside a drop-wiping exposed wrapper.
566 ///
567 /// This is an explicit escape hatch for no-alloc interop with APIs that
568 /// require ownership of a fixed array. The returned
569 /// [`ExposedDecodedArray`] remains redacted by formatting and clears its
570 /// backing array on drop.
571 #[must_use]
572 pub fn into_exposed_array(mut self) -> ExposedDecodedArray<CAP> {
573 let len = self.len;
574 self.len = 0;
575 ExposedDecodedArray::from_array(core::mem::replace(&mut self.bytes, [0u8; CAP]), len)
576 }
577
578 /// Clears the visible bytes and the full backing array.
579 pub fn clear(&mut self) {
580 wipe_bytes(&mut self.bytes);
581 self.len = 0;
582 }
583
584 /// Clears bytes after the visible prefix.
585 pub fn clear_tail(&mut self) {
586 wipe_tail(&mut self.bytes, self.len);
587 }
588}
589
590impl<const CAP: usize> AsRef<[u8]> for DecodedBuffer<CAP> {
591 fn as_ref(&self) -> &[u8] {
592 self.as_bytes()
593 }
594}
595
596impl<const CAP: usize> Clone for DecodedBuffer<CAP> {
597 /// Clones the visible decoded bytes into a second stack-backed buffer.
598 ///
599 /// Security note: cloning duplicates decoded bytes in memory. Both the
600 /// original and the clone must be dropped or explicitly cleared before the
601 /// duplicated bytes are gone on the crate's best-effort cleanup path. The
602 /// compiler may also create temporary stack copies while performing the
603 /// copy; those intermediates are outside this crate's cleanup boundary. For
604 /// high-assurance applications, avoid cloning decoded key material and use
605 /// `SecretBuffer` for heap-owned secrets without a `Clone` implementation.
606 fn clone(&self) -> Self {
607 let mut output = Self::new();
608 output.bytes[..self.len].copy_from_slice(self.as_bytes());
609 output.len = self.len;
610 output
611 }
612}
613
614impl<const CAP: usize> core::fmt::Debug for DecodedBuffer<CAP> {
615 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
616 formatter
617 .debug_struct("DecodedBuffer")
618 .field("bytes", &"<redacted>")
619 .field("len", &self.len)
620 .field("capacity", &CAP)
621 .finish()
622 }
623}
624
625impl<const CAP: usize> Default for DecodedBuffer<CAP> {
626 fn default() -> Self {
627 Self::new()
628 }
629}
630
631impl<const CAP: usize> Drop for DecodedBuffer<CAP> {
632 fn drop(&mut self) {
633 self.clear();
634 }
635}
636
637impl<const CAP: usize> TryFrom<&[u8]> for DecodedBuffer<CAP> {
638 type Error = DecodeError;
639
640 /// Decodes strict standard padded Base64 into a stack-backed buffer.
641 ///
642 /// Use [`crate::Engine::decode_buffer`] or [`crate::Profile::decode_buffer`] when a
643 /// different alphabet, padding mode, or line-wrapping profile is required.
644 ///
645 /// # Security
646 ///
647 /// This idiomatic conversion uses the strict standard decoder, not the
648 /// constant-time-oriented decoder. It may branch or return early on
649 /// malformed input and reports exact [`DecodeError`] positions. For
650 /// secret-bearing tokens or key material where malformed-input timing
651 /// matters, use [`crate::ct::STANDARD`].`decode_buffer::<CAP>(input)`
652 /// instead.
653 fn try_from(input: &[u8]) -> Result<Self, Self::Error> {
654 STANDARD.decode_buffer(input)
655 }
656}
657
658impl<const CAP: usize, const N: usize> TryFrom<&[u8; N]> for DecodedBuffer<CAP> {
659 type Error = DecodeError;
660
661 /// Decodes a strict standard padded Base64 byte array into a stack-backed
662 /// buffer.
663 ///
664 /// Use [`crate::Engine::decode_buffer`] or [`crate::Profile::decode_buffer`] when a
665 /// different alphabet, padding mode, or line-wrapping profile is required.
666 ///
667 /// # Security
668 ///
669 /// This idiomatic conversion uses the strict standard decoder, not the
670 /// constant-time-oriented decoder. It may branch or return early on
671 /// malformed input and reports exact [`DecodeError`] positions. For
672 /// secret-bearing tokens or key material where malformed-input timing
673 /// matters, use [`crate::ct::STANDARD`].`decode_buffer::<CAP>(input)`
674 /// instead.
675 fn try_from(input: &[u8; N]) -> Result<Self, Self::Error> {
676 Self::try_from(&input[..])
677 }
678}
679
680impl<const CAP: usize> TryFrom<&str> for DecodedBuffer<CAP> {
681 type Error = DecodeError;
682
683 /// Decodes strict standard padded Base64 text into a stack-backed buffer.
684 ///
685 /// Use [`crate::Engine::decode_buffer`] or [`crate::Profile::decode_buffer`] when a
686 /// different alphabet, padding mode, or line-wrapping profile is required.
687 ///
688 /// # Security
689 ///
690 /// This idiomatic conversion uses the strict standard decoder, not the
691 /// constant-time-oriented decoder. It may branch or return early on
692 /// malformed input and reports exact [`DecodeError`] positions. For
693 /// secret-bearing tokens or key material where malformed-input timing
694 /// matters, use [`crate::ct::STANDARD`].`decode_buffer::<CAP>(input)`
695 /// instead.
696 fn try_from(input: &str) -> Result<Self, Self::Error> {
697 Self::try_from(input.as_bytes())
698 }
699}
700
701impl<const CAP: usize> core::str::FromStr for DecodedBuffer<CAP> {
702 type Err = DecodeError;
703
704 /// Decodes strict standard padded Base64 text into a stack-backed buffer.
705 ///
706 /// Use [`crate::Engine::decode_buffer`] or [`crate::Profile::decode_buffer`] when a
707 /// different alphabet, padding mode, or line-wrapping profile is required.
708 ///
709 /// # Security
710 ///
711 /// This idiomatic conversion uses the strict standard decoder, not the
712 /// constant-time-oriented decoder. It may branch or return early on
713 /// malformed input and reports exact [`DecodeError`] positions. For
714 /// secret-bearing tokens or key material where malformed-input timing
715 /// matters, use [`crate::ct::STANDARD`].`decode_buffer::<CAP>(input)`
716 /// instead.
717 fn from_str(input: &str) -> Result<Self, Self::Err> {
718 Self::try_from(input)
719 }
720}
721
722/// Owned sensitive bytes with redacted formatting and drop-time cleanup.
723///
724/// `SecretBuffer` is available with the `alloc` feature. It is intended for
725/// decoded keys, tokens, and other values that should not be accidentally
726/// logged. The buffer exposes contents only through explicit reveal methods.
727///
728/// Spare vector capacity is cleared when wrapping owned bytes. On drop,
729/// initialized bytes and vector spare capacity are cleared with the crate's
730/// internal best-effort wipe helpers. This is data-retention reduction, not a
731/// formal zeroization guarantee, and it cannot make claims about allocator
732/// behavior or historical copies outside the wrapper.
733///
734/// # Platform Memory Controls
735///
736/// `SecretBuffer` does not lock its allocation into physical memory. The OS
737/// may page its contents to disk, include them in hibernation images, or expose
738/// them through crash dumps. High-assurance deployments must combine
739/// `SecretBuffer` with platform memory-locking where available, encrypted or
740/// disabled swap, crash-dump suppression, and allocator isolation appropriate
741/// for their environment.
742///
743/// On `wasm32` targets, the wipe barrier uses only a compiler fence. The wasm
744/// runtime JIT may still optimize or retain cleared bytes in ways this crate
745/// cannot control. `wasm32` builds fail closed by default; enable
746/// `allow-wasm32-best-effort-wipe` only when the deployment explicitly accepts
747/// this limitation and applies its own memory strategy around owned secret
748/// buffers.
749#[cfg(feature = "alloc")]
750pub struct SecretBuffer {
751 bytes: alloc::vec::Vec<u8>,
752}
753
754/// Owned secret bytes extracted from [`SecretBuffer`].
755///
756/// This wrapper keeps redacted formatting, best-effort spare-capacity clearing
757/// at construction time, and best-effort full wipe on drop after a
758/// [`SecretBuffer`] is consumed for owned interop. Use
759/// [`Self::into_exposed_unprotected_vec_caller_must_zeroize`] only when a raw
760/// `Vec<u8>` is unavoidable and the caller will handle cleanup.
761#[cfg(feature = "alloc")]
762pub struct ExposedSecretVec {
763 bytes: alloc::vec::Vec<u8>,
764}
765
766#[cfg(feature = "alloc")]
767impl ExposedSecretVec {
768 /// Wraps an owned vector as exposed secret material.
769 #[must_use]
770 pub fn from_vec(mut bytes: alloc::vec::Vec<u8>) -> Self {
771 wipe_vec_spare_capacity(&mut bytes);
772 Self { bytes }
773 }
774
775 /// Returns the number of initialized secret bytes.
776 #[must_use]
777 pub fn len(&self) -> usize {
778 self.bytes.len()
779 }
780
781 /// Returns whether the buffer contains no initialized secret bytes.
782 #[must_use]
783 pub fn is_empty(&self) -> bool {
784 self.bytes.is_empty()
785 }
786
787 /// Reveals the secret bytes.
788 ///
789 /// This method is intentionally named to make secret access explicit at the
790 /// call site.
791 #[must_use]
792 pub fn expose_secret(&self) -> &[u8] {
793 &self.bytes
794 }
795
796 /// Reveals the secret bytes mutably.
797 ///
798 /// This method is intentionally named to make secret access explicit at the
799 /// call site.
800 #[must_use]
801 pub fn expose_secret_mut(&mut self) -> &mut [u8] {
802 &mut self.bytes
803 }
804
805 /// Consumes the wrapper and returns a raw `Vec<u8>`.
806 ///
807 /// This is an unprotected escape hatch. The returned vector is no longer
808 /// redacted by formatting and will not be cleared by this crate on drop.
809 /// Callers must clear it with their own approved zeroization policy.
810 #[must_use = "caller must zeroize the returned Vec"]
811 pub fn into_exposed_unprotected_vec_caller_must_zeroize(mut self) -> alloc::vec::Vec<u8> {
812 core::mem::take(&mut self.bytes)
813 }
814}
815
816#[cfg(feature = "alloc")]
817impl core::fmt::Debug for ExposedSecretVec {
818 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
819 formatter
820 .debug_struct("ExposedSecretVec")
821 .field("bytes", &"<redacted>")
822 .field("len", &self.len())
823 .finish()
824 }
825}
826
827#[cfg(feature = "alloc")]
828impl core::fmt::Display for ExposedSecretVec {
829 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
830 formatter.write_str("<redacted>")
831 }
832}
833
834#[cfg(feature = "alloc")]
835impl Drop for ExposedSecretVec {
836 fn drop(&mut self) {
837 wipe_vec_all(&mut self.bytes);
838 }
839}
840
841#[cfg(feature = "alloc")]
842struct WipeVecGuard {
843 bytes: alloc::vec::Vec<u8>,
844}
845
846#[cfg(feature = "alloc")]
847impl WipeVecGuard {
848 fn from_vec(bytes: alloc::vec::Vec<u8>) -> Self {
849 Self { bytes }
850 }
851
852 fn into_validated_secret_string(mut self) -> alloc::string::String {
853 wipe_vec_spare_capacity(&mut self.bytes);
854 let bytes = core::mem::take(&mut self.bytes);
855 core::mem::forget(self);
856 string_from_validated_secret_bytes(bytes)
857 }
858}
859
860#[cfg(feature = "alloc")]
861impl Drop for WipeVecGuard {
862 fn drop(&mut self) {
863 wipe_vec_all(&mut self.bytes);
864 }
865}
866
867#[cfg(feature = "alloc")]
868impl AsRef<[u8]> for ExposedSecretVec {
869 fn as_ref(&self) -> &[u8] {
870 self.expose_secret()
871 }
872}
873
874#[cfg(feature = "alloc")]
875impl AsMut<[u8]> for ExposedSecretVec {
876 fn as_mut(&mut self) -> &mut [u8] {
877 self.expose_secret_mut()
878 }
879}
880
881/// Owned secret UTF-8 text extracted from [`SecretBuffer`].
882///
883/// This wrapper keeps redacted formatting, best-effort spare-capacity clearing
884/// at construction time, and best-effort full wipe on drop after a
885/// [`SecretBuffer`] is consumed for string interop. Use
886/// [`Self::into_exposed_unprotected_string_caller_must_zeroize`] only when a
887/// raw `String` is unavoidable and the caller will handle cleanup.
888#[cfg(feature = "alloc")]
889pub struct ExposedSecretString {
890 text: alloc::string::String,
891}
892
893#[cfg(feature = "alloc")]
894impl ExposedSecretString {
895 /// Wraps an owned UTF-8 string as exposed secret text.
896 #[must_use]
897 pub fn from_string(text: alloc::string::String) -> Self {
898 let mut bytes = text.into_bytes();
899 wipe_vec_spare_capacity(&mut bytes);
900 let text = string_from_validated_secret_bytes(bytes);
901 Self { text }
902 }
903
904 /// Returns the length of the secret text in bytes.
905 #[must_use]
906 pub fn len(&self) -> usize {
907 self.text.len()
908 }
909
910 /// Returns whether the secret text is empty.
911 #[must_use]
912 pub fn is_empty(&self) -> bool {
913 self.text.is_empty()
914 }
915
916 /// Reveals the secret text.
917 ///
918 /// This method is intentionally named to make secret access explicit at
919 /// the call site.
920 #[must_use]
921 pub fn expose_secret(&self) -> &str {
922 &self.text
923 }
924
925 /// Reveals the secret text as bytes.
926 ///
927 /// This method is intentionally named to make secret access explicit at
928 /// the call site.
929 #[must_use]
930 pub fn expose_secret_bytes(&self) -> &[u8] {
931 self.text.as_bytes()
932 }
933
934 /// Consumes the wrapper and returns a raw `String`.
935 ///
936 /// This is an unprotected escape hatch. The returned string is no longer
937 /// redacted by formatting and will not be cleared by this crate on drop.
938 /// Callers must clear it with their own approved zeroization policy.
939 #[must_use = "caller must zeroize the returned String"]
940 pub fn into_exposed_unprotected_string_caller_must_zeroize(mut self) -> alloc::string::String {
941 core::mem::take(&mut self.text)
942 }
943}
944
945#[cfg(feature = "alloc")]
946impl core::fmt::Debug for ExposedSecretString {
947 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
948 formatter
949 .debug_struct("ExposedSecretString")
950 .field("text", &"<redacted>")
951 .field("len", &self.len())
952 .finish()
953 }
954}
955
956#[cfg(feature = "alloc")]
957impl core::fmt::Display for ExposedSecretString {
958 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
959 formatter.write_str("<redacted>")
960 }
961}
962
963#[cfg(feature = "alloc")]
964impl Drop for ExposedSecretString {
965 fn drop(&mut self) {
966 let mut bytes = core::mem::take(&mut self.text).into_bytes();
967 wipe_vec_all(&mut bytes);
968 }
969}
970
971#[cfg(feature = "alloc")]
972impl AsRef<str> for ExposedSecretString {
973 fn as_ref(&self) -> &str {
974 self.expose_secret()
975 }
976}
977
978#[cfg(feature = "alloc")]
979impl SecretBuffer {
980 /// Wraps an existing vector as sensitive material.
981 #[must_use]
982 pub fn from_vec(mut bytes: alloc::vec::Vec<u8>) -> Self {
983 wipe_vec_spare_capacity(&mut bytes);
984 Self { bytes }
985 }
986
987 /// Copies a slice into an owned sensitive buffer.
988 #[must_use]
989 pub fn from_slice(bytes: &[u8]) -> Self {
990 Self::from_vec(bytes.to_vec())
991 }
992
993 /// Returns the number of initialized secret bytes.
994 #[must_use]
995 pub fn len(&self) -> usize {
996 self.bytes.len()
997 }
998
999 /// Returns whether the buffer contains no initialized secret bytes.
1000 #[must_use]
1001 pub fn is_empty(&self) -> bool {
1002 self.bytes.is_empty()
1003 }
1004
1005 /// Reveals the secret bytes.
1006 ///
1007 /// This method is intentionally named to make secret access explicit at the
1008 /// call site.
1009 #[must_use]
1010 pub fn expose_secret(&self) -> &[u8] {
1011 &self.bytes
1012 }
1013
1014 /// Reveals the secret bytes as UTF-8 text.
1015 ///
1016 /// This method is intentionally named to make secret access explicit at the
1017 /// call site. Secret material may be arbitrary binary data, so this method
1018 /// is fallible.
1019 pub fn expose_secret_utf8(&self) -> Result<&str, core::str::Utf8Error> {
1020 core::str::from_utf8(self.expose_secret())
1021 }
1022
1023 /// Reveals the secret bytes mutably.
1024 ///
1025 /// This method is intentionally named to make secret access explicit at the
1026 /// call site.
1027 #[must_use]
1028 pub fn expose_secret_mut(&mut self) -> &mut [u8] {
1029 &mut self.bytes
1030 }
1031
1032 /// Consumes the wrapper and returns owned secret bytes.
1033 ///
1034 /// This is an explicit escape hatch for interop with APIs that require an
1035 /// owned vector-like value. The returned [`ExposedSecretVec`] remains
1036 /// redacted by formatting and clears its vector on drop.
1037 #[must_use]
1038 pub fn into_exposed_vec(mut self) -> ExposedSecretVec {
1039 ExposedSecretVec::from_vec(core::mem::take(&mut self.bytes))
1040 }
1041
1042 /// Consumes the wrapper and returns the owned secret bytes as UTF-8 text.
1043 ///
1044 /// This is an explicit escape hatch for interop with APIs that require an
1045 /// owned string-like value. The returned [`ExposedSecretString`] remains
1046 /// redacted by formatting and clears its heap allocation on drop.
1047 ///
1048 /// If the secret bytes are not valid UTF-8, the original redacted wrapper
1049 /// is returned unchanged.
1050 #[must_use = "handle invalid UTF-8 errors and keep the returned wrapper protected"]
1051 pub fn try_into_exposed_string(self) -> Result<ExposedSecretString, Self> {
1052 if core::str::from_utf8(self.expose_secret()).is_err() {
1053 return Err(self);
1054 }
1055
1056 // Keep the bytes behind a wiping guard until the final infallible
1057 // ownership transfer into `String`.
1058 let mut exposed = self.into_exposed_vec();
1059 let guard = WipeVecGuard::from_vec(core::mem::take(&mut exposed.bytes));
1060 drop(exposed);
1061 Ok(ExposedSecretString::from_string(
1062 guard.into_validated_secret_string(),
1063 ))
1064 }
1065
1066 /// Compares this secret to `other` without short-circuiting on the first
1067 /// differing byte.
1068 ///
1069 /// Length and the final equality result remain public. Different lengths
1070 /// return `false` immediately; use this helper only when the compared
1071 /// lengths are public protocol facts or have been normalized by the
1072 /// caller. For equal-length inputs, this helper scans every byte before
1073 /// returning. It is constant-time-oriented best effort, not a formal
1074 /// cryptographic constant-time guarantee. This comparison is deliberately
1075 /// explicit: redacted buffer types do not implement [`PartialEq`] because
1076 /// `==` would make a best-effort helper look like a formal token/MAC
1077 /// comparison primitive.
1078 ///
1079 /// Do not use this helper as the sole MAC, bearer-token, password-hash, or
1080 /// authentication-secret comparison primitive in high-assurance systems.
1081 /// Applications that can admit dependencies should use a reviewed
1082 /// constant-time comparison primitive, such as `subtle`, at the protocol
1083 /// boundary.
1084 #[doc(alias = "constant_time_eq")]
1085 #[must_use]
1086 pub fn constant_time_eq_public_len(&self, other: &[u8]) -> bool {
1087 constant_time_eq_public_len(self.expose_secret(), other)
1088 }
1089
1090 /// Clears the initialized bytes and makes the buffer empty.
1091 pub fn clear(&mut self) {
1092 wipe_vec_all(&mut self.bytes);
1093 self.bytes.clear();
1094 }
1095}
1096
1097#[cfg(feature = "alloc")]
1098impl core::fmt::Debug for SecretBuffer {
1099 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1100 formatter
1101 .debug_struct("SecretBuffer")
1102 .field("bytes", &"<redacted>")
1103 .field("len", &self.len())
1104 .finish()
1105 }
1106}
1107
1108#[cfg(feature = "alloc")]
1109impl core::fmt::Display for SecretBuffer {
1110 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1111 formatter.write_str("<redacted>")
1112 }
1113}
1114
1115#[cfg(feature = "alloc")]
1116impl Drop for SecretBuffer {
1117 fn drop(&mut self) {
1118 wipe_vec_all(&mut self.bytes);
1119 }
1120}
1121
1122#[cfg(feature = "alloc")]
1123impl From<alloc::vec::Vec<u8>> for SecretBuffer {
1124 /// Wraps an owned vector as sensitive material.
1125 ///
1126 /// Spare capacity is cleared immediately before the vector is stored.
1127 /// Use [`SecretBuffer::from_slice`] when the source data is borrowed.
1128 fn from(bytes: alloc::vec::Vec<u8>) -> Self {
1129 Self::from_vec(bytes)
1130 }
1131}
1132
1133#[cfg(feature = "alloc")]
1134impl From<alloc::string::String> for SecretBuffer {
1135 /// Wraps an owned UTF-8 string as sensitive material.
1136 ///
1137 /// The string is consumed without copying its initialized bytes. Spare
1138 /// vector capacity is cleared immediately before the bytes are stored.
1139 fn from(text: alloc::string::String) -> Self {
1140 Self::from_vec(text.into_bytes())
1141 }
1142}
1143
1144#[cfg(feature = "alloc")]
1145impl<const CAP: usize> From<EncodedBuffer<CAP>> for SecretBuffer {
1146 /// Copies visible encoded bytes from a stack-backed buffer into an owned
1147 /// redacted buffer.
1148 ///
1149 /// The consumed stack-backed buffer clears its backing array when it is
1150 /// dropped at the end of the conversion.
1151 fn from(buffer: EncodedBuffer<CAP>) -> Self {
1152 Self::from_slice(buffer.as_bytes())
1153 }
1154}
1155
1156#[cfg(feature = "alloc")]
1157impl<const CAP: usize> From<DecodedBuffer<CAP>> for SecretBuffer {
1158 /// Copies visible decoded bytes from a stack-backed buffer into an owned
1159 /// redacted buffer.
1160 ///
1161 /// The consumed stack-backed buffer clears its backing array when it is
1162 /// dropped at the end of the conversion.
1163 fn from(buffer: DecodedBuffer<CAP>) -> Self {
1164 Self::from_slice(buffer.as_bytes())
1165 }
1166}
1167
1168#[cfg(feature = "alloc")]
1169impl TryFrom<&[u8]> for SecretBuffer {
1170 type Error = DecodeError;
1171
1172 /// Decodes strict standard padded Base64 into a redacted owned buffer.
1173 ///
1174 /// Use [`crate::Engine::decode_secret`] or [`crate::Profile::decode_secret`] when a
1175 /// different alphabet, padding mode, or line-wrapping profile is required.
1176 ///
1177 /// # Security
1178 ///
1179 /// This idiomatic conversion uses the strict standard decoder, not the
1180 /// constant-time-oriented decoder. It may branch or return early on
1181 /// malformed input and reports exact [`DecodeError`] positions. For
1182 /// secret-bearing tokens or key material where malformed-input timing
1183 /// matters, use [`crate::ct::STANDARD`].`decode_buffer(input)` or
1184 /// [`crate::ct::STANDARD`].`decode_slice_staged_clear_tail(...)` and then
1185 /// wrap the successful output in `SecretBuffer`.
1186 fn try_from(input: &[u8]) -> Result<Self, Self::Error> {
1187 STANDARD.decode_secret(input)
1188 }
1189}
1190
1191#[cfg(feature = "alloc")]
1192impl<const N: usize> TryFrom<&[u8; N]> for SecretBuffer {
1193 type Error = DecodeError;
1194
1195 /// Decodes a strict standard padded Base64 byte array into a redacted
1196 /// owned buffer.
1197 ///
1198 /// Use [`crate::Engine::decode_secret`] or [`crate::Profile::decode_secret`] when a
1199 /// different alphabet, padding mode, or line-wrapping profile is required.
1200 ///
1201 /// # Security
1202 ///
1203 /// This idiomatic conversion uses the strict standard decoder, not the
1204 /// constant-time-oriented decoder. It may branch or return early on
1205 /// malformed input and reports exact [`DecodeError`] positions. For
1206 /// secret-bearing tokens or key material where malformed-input timing
1207 /// matters, use [`crate::ct::STANDARD`].`decode_buffer(input)` or
1208 /// [`crate::ct::STANDARD`].`decode_slice_staged_clear_tail(...)` and then
1209 /// wrap the successful output in `SecretBuffer`.
1210 fn try_from(input: &[u8; N]) -> Result<Self, Self::Error> {
1211 Self::try_from(&input[..])
1212 }
1213}
1214
1215#[cfg(feature = "alloc")]
1216impl TryFrom<&str> for SecretBuffer {
1217 type Error = DecodeError;
1218
1219 /// Decodes strict standard padded Base64 text into a redacted owned buffer.
1220 ///
1221 /// Use [`crate::Engine::decode_secret`] or [`crate::Profile::decode_secret`] when a
1222 /// different alphabet, padding mode, or line-wrapping profile is required.
1223 ///
1224 /// # Security
1225 ///
1226 /// This idiomatic conversion uses the strict standard decoder, not the
1227 /// constant-time-oriented decoder. It may branch or return early on
1228 /// malformed input and reports exact [`DecodeError`] positions. For
1229 /// secret-bearing tokens or key material where malformed-input timing
1230 /// matters, use [`crate::ct::STANDARD`].`decode_buffer(input)` or
1231 /// [`crate::ct::STANDARD`].`decode_slice_staged_clear_tail(...)` and then
1232 /// wrap the successful output in `SecretBuffer`.
1233 fn try_from(input: &str) -> Result<Self, Self::Error> {
1234 Self::try_from(input.as_bytes())
1235 }
1236}
1237
1238#[cfg(feature = "alloc")]
1239impl core::str::FromStr for SecretBuffer {
1240 type Err = DecodeError;
1241
1242 /// Decodes strict standard padded Base64 text into a redacted owned buffer.
1243 ///
1244 /// Use [`crate::Engine::decode_secret`] or [`crate::Profile::decode_secret`] when a
1245 /// different alphabet, padding mode, or line-wrapping profile is required.
1246 ///
1247 /// # Security
1248 ///
1249 /// This idiomatic conversion uses the strict standard decoder, not the
1250 /// constant-time-oriented decoder. It may branch or return early on
1251 /// malformed input and reports exact [`DecodeError`] positions. For
1252 /// secret-bearing tokens or key material where malformed-input timing
1253 /// matters, use [`crate::ct::STANDARD`].`decode_buffer(input)` or
1254 /// [`crate::ct::STANDARD`].`decode_slice_staged_clear_tail(...)` and then
1255 /// wrap the successful output in `SecretBuffer`.
1256 fn from_str(input: &str) -> Result<Self, Self::Err> {
1257 Self::try_from(input)
1258 }
1259}
1260
1261#[cfg(feature = "alloc")]
1262fn string_from_validated_secret_bytes(bytes: Vec<u8>) -> String {
1263 match String::from_utf8(bytes) {
1264 Ok(string) => string,
1265 Err(error) => {
1266 let mut bytes = error.into_bytes();
1267 wipe_vec_all(&mut bytes);
1268 String::new()
1269 }
1270 }
1271}