Skip to main content

base64_ng/buffers/
decoded.rs

1use crate::{DecodeError, STANDARD, constant_time_eq_public_len, wipe_bytes, wipe_tail};
2
3/// Stack-backed decoded Base64 output.
4///
5/// This type is intended for short decoded values where heap allocation would
6/// be unnecessary but manually sizing and passing a separate output slice is
7/// noisy. Decoded data may be binary or secret-bearing, so formatting is
8/// redacted and contents are exposed only through explicit byte accessors.
9///
10/// The backing array is cleared when the value is dropped. This is best-effort
11/// data-retention reduction and is not a formal zeroization guarantee.
12///
13/// # Security: cloning
14///
15/// This type implements [`Clone`] for no-alloc ergonomics, but cloning
16/// duplicates decoded bytes. The compiler may also create temporary
17/// intermediates during the copy that are outside this crate's cleanup
18/// boundary. For heap-owned key material, prefer the alloc-gated
19/// `SecretBuffer`, which does not implement `Clone`.
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 DecodedBuffer<const CAP: usize> {
28    bytes: [u8; CAP],
29    len: usize,
30}
31
32/// Owned stack array extracted from [`DecodedBuffer`].
33///
34/// This wrapper keeps the extracted decoded 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 ExposedDecodedArray<const CAP: usize> {
39    bytes: [u8; CAP],
40    len: usize,
41}
42
43impl<const CAP: usize> ExposedDecodedArray<CAP> {
44    /// Wraps a decoded 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 decoded bytes.
56    #[must_use]
57    pub fn as_bytes(&self) -> &[u8] {
58        &self.bytes[..self.len]
59    }
60
61    /// Returns the number of visible decoded bytes.
62    #[must_use]
63    pub const fn len(&self) -> usize {
64        self.len
65    }
66
67    /// Returns whether there are no visible decoded 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 decoded bytes, which may be secret-bearing, in ordinary
89    /// caller-owned memory until 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 ExposedDecodedArray<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 ExposedDecodedArray<CAP> {
106    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
107        formatter
108            .debug_struct("ExposedDecodedArray")
109            .field("bytes", &"<redacted>")
110            .field("len", &self.len)
111            .field("capacity", &CAP)
112            .finish()
113    }
114}
115
116impl<const CAP: usize> DecodedBuffer<CAP> {
117    /// Creates an empty decoded 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    /// decode 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 decode path succeeds.
133    pub(crate) fn set_filled(&mut self, written: usize) -> Result<(), DecodeError> {
134        debug_assert!(
135            written <= CAP,
136            "decoder wrote past stack-backed buffer capacity"
137        );
138        if written > CAP {
139            self.clear();
140            return Err(DecodeError::OutputTooSmall {
141                required: written,
142                available: CAP,
143            });
144        }
145        self.len = written;
146        Ok(())
147    }
148
149    /// Returns the number of visible decoded bytes.
150    #[must_use]
151    pub const fn len(&self) -> usize {
152        self.len
153    }
154
155    /// Returns whether the buffer has no visible decoded bytes.
156    #[must_use]
157    pub const fn is_empty(&self) -> bool {
158        self.len == 0
159    }
160
161    /// Returns whether the visible decoded 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 decoded bytes.
180    #[must_use]
181    pub fn as_bytes(&self) -> &[u8] {
182        &self.bytes[..self.len]
183    }
184
185    /// Returns the visible decoded bytes as UTF-8 text.
186    ///
187    /// Decoded Base64 output is arbitrary bytes, so this method is fallible.
188    /// Use [`Self::as_bytes`] when the decoded payload is binary or when text
189    /// validation belongs to a higher protocol layer.
190    pub fn as_utf8(&self) -> Result<&str, core::str::Utf8Error> {
191        core::str::from_utf8(self.as_bytes())
192    }
193
194    /// Compares this decoded output to `other` without short-circuiting on the
195    /// first differing byte.
196    ///
197    /// Length and the final equality result remain public. Different lengths
198    /// return `false` immediately; use this helper only when the compared
199    /// lengths are public protocol facts or have been normalized by the
200    /// caller. For equal-length inputs, this helper scans every byte before
201    /// returning. It is constant-time-oriented best effort, not a formal
202    /// cryptographic constant-time guarantee. This comparison is deliberately
203    /// explicit: redacted buffer types do not implement [`PartialEq`] because
204    /// `==` would make a best-effort helper look like a formal token/MAC
205    /// comparison primitive.
206    ///
207    /// Do not use this helper as the sole MAC, bearer-token, password-hash, or
208    /// authentication-secret comparison primitive in high-assurance systems.
209    /// Applications that can admit dependencies should use a reviewed
210    /// constant-time comparison primitive, such as `subtle`, at the protocol
211    /// boundary.
212    #[doc(alias = "constant_time_eq")]
213    #[must_use]
214    pub fn constant_time_eq_public_len(&self, other: &[u8]) -> bool {
215        constant_time_eq_public_len(self.as_bytes(), other)
216    }
217
218    /// Consumes the wrapper and returns the backing array plus visible length
219    /// inside a drop-wiping exposed wrapper.
220    ///
221    /// This is an explicit escape hatch for no-alloc interop with APIs that
222    /// require ownership of a fixed array. The returned
223    /// [`ExposedDecodedArray`] remains redacted by formatting and clears its
224    /// backing array on drop.
225    #[must_use]
226    pub fn into_exposed_array(mut self) -> ExposedDecodedArray<CAP> {
227        let len = self.len;
228        self.len = 0;
229        ExposedDecodedArray::from_array(core::mem::replace(&mut self.bytes, [0u8; CAP]), len)
230    }
231
232    /// Clears the visible bytes and the full backing array.
233    pub fn clear(&mut self) {
234        wipe_bytes(&mut self.bytes);
235        self.len = 0;
236    }
237
238    /// Clears bytes after the visible prefix.
239    pub fn clear_tail(&mut self) {
240        wipe_tail(&mut self.bytes, self.len);
241    }
242}
243
244impl<const CAP: usize> AsRef<[u8]> for DecodedBuffer<CAP> {
245    fn as_ref(&self) -> &[u8] {
246        self.as_bytes()
247    }
248}
249
250impl<const CAP: usize> Clone for DecodedBuffer<CAP> {
251    /// Clones the visible decoded bytes into a second stack-backed buffer.
252    ///
253    /// Security note: cloning duplicates decoded bytes in memory. Both the
254    /// original and the clone must be dropped or explicitly cleared before the
255    /// duplicated bytes are gone on the crate's best-effort cleanup path. The
256    /// compiler may also create temporary stack copies while performing the
257    /// copy; those intermediates are outside this crate's cleanup boundary. For
258    /// high-assurance applications, avoid cloning decoded key material and use
259    /// `SecretBuffer` for heap-owned secrets without a `Clone` implementation.
260    fn clone(&self) -> Self {
261        let mut output = Self::new();
262        output.bytes[..self.len].copy_from_slice(self.as_bytes());
263        output.len = self.len;
264        output
265    }
266}
267
268impl<const CAP: usize> core::fmt::Debug for DecodedBuffer<CAP> {
269    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
270        formatter
271            .debug_struct("DecodedBuffer")
272            .field("bytes", &"<redacted>")
273            .field("len", &self.len)
274            .field("capacity", &CAP)
275            .finish()
276    }
277}
278
279impl<const CAP: usize> Default for DecodedBuffer<CAP> {
280    fn default() -> Self {
281        Self::new()
282    }
283}
284
285impl<const CAP: usize> Drop for DecodedBuffer<CAP> {
286    fn drop(&mut self) {
287        self.clear();
288    }
289}
290
291impl<const CAP: usize> TryFrom<&[u8]> for DecodedBuffer<CAP> {
292    type Error = DecodeError;
293
294    /// Decodes strict standard padded Base64 into a stack-backed buffer.
295    ///
296    /// Use [`crate::Engine::decode_buffer`] or [`crate::Profile::decode_buffer`]
297    /// when a different alphabet, padding mode, or line-wrapping profile is
298    /// required. These conversions always use [`crate::STANDARD`]; URL-safe,
299    /// bcrypt, crypt, MIME, PEM, and custom alphabets must use an explicit
300    /// engine or profile.
301    ///
302    /// # Security
303    ///
304    /// This idiomatic conversion uses the strict standard decoder, not the
305    /// constant-time-oriented decoder. It may branch or return early on
306    /// malformed input and reports exact [`DecodeError`] positions. For
307    /// secret-bearing tokens or key material where malformed-input timing
308    /// matters, use [`crate::ct::CtEngine::decode_buffer`] through
309    /// [`crate::ct::STANDARD`] instead.
310    fn try_from(input: &[u8]) -> Result<Self, Self::Error> {
311        STANDARD.decode_buffer(input)
312    }
313}
314
315impl<const CAP: usize, const N: usize> TryFrom<&[u8; N]> for DecodedBuffer<CAP> {
316    type Error = DecodeError;
317
318    /// Decodes a strict standard padded Base64 byte array into a stack-backed
319    /// buffer.
320    ///
321    /// Use [`crate::Engine::decode_buffer`] or [`crate::Profile::decode_buffer`]
322    /// when a different alphabet, padding mode, or line-wrapping profile is
323    /// required. These conversions always use [`crate::STANDARD`]; URL-safe,
324    /// bcrypt, crypt, MIME, PEM, and custom alphabets must use an explicit
325    /// engine or profile.
326    ///
327    /// # Security
328    ///
329    /// This idiomatic conversion uses the strict standard decoder, not the
330    /// constant-time-oriented decoder. It may branch or return early on
331    /// malformed input and reports exact [`DecodeError`] positions. For
332    /// secret-bearing tokens or key material where malformed-input timing
333    /// matters, use [`crate::ct::CtEngine::decode_buffer`] through
334    /// [`crate::ct::STANDARD`] instead.
335    fn try_from(input: &[u8; N]) -> Result<Self, Self::Error> {
336        Self::try_from(&input[..])
337    }
338}
339
340impl<const CAP: usize> TryFrom<&str> for DecodedBuffer<CAP> {
341    type Error = DecodeError;
342
343    /// Decodes strict standard padded Base64 text into a stack-backed buffer.
344    ///
345    /// Use [`crate::Engine::decode_buffer`] or [`crate::Profile::decode_buffer`]
346    /// when a different alphabet, padding mode, or line-wrapping profile is
347    /// required. These conversions always use [`crate::STANDARD`]; URL-safe,
348    /// bcrypt, crypt, MIME, PEM, and custom alphabets must use an explicit
349    /// engine or profile.
350    ///
351    /// # Security
352    ///
353    /// This idiomatic conversion uses the strict standard decoder, not the
354    /// constant-time-oriented decoder. It may branch or return early on
355    /// malformed input and reports exact [`DecodeError`] positions. For
356    /// secret-bearing tokens or key material where malformed-input timing
357    /// matters, use [`crate::ct::CtEngine::decode_buffer`] through
358    /// [`crate::ct::STANDARD`] instead.
359    fn try_from(input: &str) -> Result<Self, Self::Error> {
360        Self::try_from(input.as_bytes())
361    }
362}
363
364impl<const CAP: usize> core::str::FromStr for DecodedBuffer<CAP> {
365    type Err = DecodeError;
366
367    /// Decodes strict standard padded Base64 text into a stack-backed buffer.
368    ///
369    /// Use [`crate::Engine::decode_buffer`] or [`crate::Profile::decode_buffer`]
370    /// when a different alphabet, padding mode, or line-wrapping profile is
371    /// required. These conversions always use [`crate::STANDARD`]; URL-safe,
372    /// bcrypt, crypt, MIME, PEM, and custom alphabets must use an explicit
373    /// engine or profile.
374    ///
375    /// # Security
376    ///
377    /// This idiomatic conversion uses the strict standard decoder, not the
378    /// constant-time-oriented decoder. It may branch or return early on
379    /// malformed input and reports exact [`DecodeError`] positions. For
380    /// secret-bearing tokens or key material where malformed-input timing
381    /// matters, use [`crate::ct::CtEngine::decode_buffer`] through
382    /// [`crate::ct::STANDARD`] instead.
383    fn from_str(input: &str) -> Result<Self, Self::Err> {
384        Self::try_from(input)
385    }
386}