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/// On `wasm32` targets, the wipe barrier uses only a compiler fence. The wasm
14/// runtime JIT may still optimize or retain cleared bytes in ways this crate
15/// cannot control. `wasm32` builds fail closed by default; enable
16/// `allow-wasm32-best-effort-wipe` only when the deployment explicitly accepts
17/// this limitation and applies its own memory strategy around stack-backed
18/// buffers.
19pub struct DecodedBuffer<const CAP: usize> {
20    bytes: [u8; CAP],
21    len: usize,
22}
23
24/// Owned stack array extracted from [`DecodedBuffer`].
25///
26/// This wrapper keeps the extracted decoded bytes on the crate's best-effort
27/// drop-time cleanup path. Use
28/// [`Self::into_exposed_unprotected_array_caller_must_zeroize`] only when a
29/// bare array is unavoidable and the caller will handle cleanup.
30pub struct ExposedDecodedArray<const CAP: usize> {
31    bytes: [u8; CAP],
32    len: usize,
33}
34
35impl<const CAP: usize> ExposedDecodedArray<CAP> {
36    /// Wraps a decoded backing array and visible length.
37    ///
38    /// # Panics
39    ///
40    /// Panics if `len` is greater than `CAP`.
41    #[must_use]
42    pub const fn from_array(bytes: [u8; CAP], len: usize) -> Self {
43        assert!(len <= CAP, "visible length exceeds array capacity");
44        Self { bytes, len }
45    }
46
47    /// Returns the visible decoded bytes.
48    #[must_use]
49    pub fn as_bytes(&self) -> &[u8] {
50        &self.bytes[..self.len]
51    }
52
53    /// Returns the number of visible decoded bytes.
54    #[must_use]
55    pub const fn len(&self) -> usize {
56        self.len
57    }
58
59    /// Returns whether there are no visible decoded bytes.
60    #[must_use]
61    pub const fn is_empty(&self) -> bool {
62        self.len == 0
63    }
64
65    /// Returns the backing array capacity.
66    #[must_use]
67    pub const fn capacity(&self) -> usize {
68        CAP
69    }
70
71    /// Consumes the wrapper and returns a bare array plus visible length.
72    ///
73    /// This is an unprotected escape hatch. The returned array will not be
74    /// cleared by this crate on drop. Callers must clear it with their own
75    /// approved zeroization policy.
76    ///
77    /// # Security
78    ///
79    /// Treat this as a cleanup-boundary API. Failing to clear the returned
80    /// array leaves decoded bytes, which may be secret-bearing, in ordinary
81    /// caller-owned memory until overwritten by later stack or heap activity.
82    #[must_use = "caller must zeroize the returned array"]
83    pub fn into_exposed_unprotected_array_caller_must_zeroize(mut self) -> ([u8; CAP], usize) {
84        let len = self.len;
85        self.len = 0;
86        (core::mem::replace(&mut self.bytes, [0u8; CAP]), len)
87    }
88}
89
90impl<const CAP: usize> Drop for ExposedDecodedArray<CAP> {
91    fn drop(&mut self) {
92        wipe_bytes(&mut self.bytes);
93        self.len = 0;
94    }
95}
96
97impl<const CAP: usize> core::fmt::Debug for ExposedDecodedArray<CAP> {
98    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
99        formatter
100            .debug_struct("ExposedDecodedArray")
101            .field("bytes", &"<redacted>")
102            .field("len", &self.len)
103            .field("capacity", &CAP)
104            .finish()
105    }
106}
107
108impl<const CAP: usize> DecodedBuffer<CAP> {
109    /// Creates an empty decoded buffer.
110    #[must_use]
111    pub const fn new() -> Self {
112        Self {
113            bytes: [0u8; CAP],
114            len: 0,
115        }
116    }
117
118    /// Returns the full backing array as an output slice for crate-internal
119    /// decode paths.
120    pub(crate) fn as_mut_capacity(&mut self) -> &mut [u8] {
121        &mut self.bytes
122    }
123
124    /// Sets the visible length after a crate-internal decode path succeeds.
125    pub(crate) fn set_filled(&mut self, written: usize) -> Result<(), DecodeError> {
126        debug_assert!(
127            written <= CAP,
128            "decoder wrote past stack-backed buffer capacity"
129        );
130        if written > CAP {
131            self.clear();
132            return Err(DecodeError::OutputTooSmall {
133                required: written,
134                available: CAP,
135            });
136        }
137        self.len = written;
138        Ok(())
139    }
140
141    /// Returns the number of visible decoded bytes.
142    #[must_use]
143    pub const fn len(&self) -> usize {
144        self.len
145    }
146
147    /// Returns whether the buffer has no visible decoded bytes.
148    #[must_use]
149    pub const fn is_empty(&self) -> bool {
150        self.len == 0
151    }
152
153    /// Returns whether the visible decoded bytes fill the stack backing array.
154    #[must_use]
155    pub const fn is_full(&self) -> bool {
156        self.len == CAP
157    }
158
159    /// Returns the stack capacity in bytes.
160    #[must_use]
161    pub const fn capacity(&self) -> usize {
162        CAP
163    }
164
165    /// Returns the number of unused bytes in the stack backing array.
166    #[must_use]
167    pub const fn remaining_capacity(&self) -> usize {
168        CAP - self.len
169    }
170
171    /// Returns the visible decoded bytes.
172    #[must_use]
173    pub fn as_bytes(&self) -> &[u8] {
174        &self.bytes[..self.len]
175    }
176
177    /// Returns the visible decoded bytes as UTF-8 text.
178    ///
179    /// Decoded Base64 output is arbitrary bytes, so this method is fallible.
180    /// Use [`Self::as_bytes`] when the decoded payload is binary or when text
181    /// validation belongs to a higher protocol layer.
182    pub fn as_utf8(&self) -> Result<&str, core::str::Utf8Error> {
183        core::str::from_utf8(self.as_bytes())
184    }
185
186    /// Compares this decoded output to `other` without short-circuiting on the
187    /// first differing byte.
188    ///
189    /// Length and the final equality result remain public. Different lengths
190    /// return `false` immediately; use this helper only when the compared
191    /// lengths are public protocol facts or have been normalized by the
192    /// caller. For equal-length inputs, this helper scans every byte before
193    /// returning. It is constant-time-oriented best effort, not a formal
194    /// cryptographic constant-time guarantee. This comparison is deliberately
195    /// explicit: redacted buffer types do not implement [`PartialEq`] because
196    /// `==` would make a best-effort helper look like a formal token/MAC
197    /// comparison primitive.
198    ///
199    /// Do not use this helper as the sole MAC, bearer-token, password-hash, or
200    /// authentication-secret comparison primitive in high-assurance systems.
201    /// Applications that can admit dependencies should use a reviewed
202    /// constant-time comparison primitive, such as `subtle`, at the protocol
203    /// boundary.
204    #[doc(alias = "constant_time_eq")]
205    #[must_use]
206    pub fn constant_time_eq_public_len(&self, other: &[u8]) -> bool {
207        constant_time_eq_public_len(self.as_bytes(), other)
208    }
209
210    /// Consumes the wrapper and returns the backing array plus visible length
211    /// inside a drop-wiping exposed wrapper.
212    ///
213    /// This is an explicit escape hatch for no-alloc interop with APIs that
214    /// require ownership of a fixed array. The returned
215    /// [`ExposedDecodedArray`] remains redacted by formatting and clears its
216    /// backing array on drop.
217    #[must_use]
218    pub fn into_exposed_array(mut self) -> ExposedDecodedArray<CAP> {
219        let len = self.len;
220        self.len = 0;
221        ExposedDecodedArray::from_array(core::mem::replace(&mut self.bytes, [0u8; CAP]), len)
222    }
223
224    /// Clears the visible bytes and the full backing array.
225    pub fn clear(&mut self) {
226        wipe_bytes(&mut self.bytes);
227        self.len = 0;
228    }
229
230    /// Clears bytes after the visible prefix.
231    pub fn clear_tail(&mut self) {
232        wipe_tail(&mut self.bytes, self.len);
233    }
234}
235
236impl<const CAP: usize> AsRef<[u8]> for DecodedBuffer<CAP> {
237    fn as_ref(&self) -> &[u8] {
238        self.as_bytes()
239    }
240}
241
242impl<const CAP: usize> Clone for DecodedBuffer<CAP> {
243    /// Clones the visible decoded bytes into a second stack-backed buffer.
244    ///
245    /// Security note: cloning duplicates decoded bytes in memory. Both the
246    /// original and the clone must be dropped or explicitly cleared before the
247    /// duplicated bytes are gone on the crate's best-effort cleanup path. The
248    /// compiler may also create temporary stack copies while performing the
249    /// copy; those intermediates are outside this crate's cleanup boundary. For
250    /// high-assurance applications, avoid cloning decoded key material and use
251    /// `SecretBuffer` for heap-owned secrets without a `Clone` implementation.
252    fn clone(&self) -> Self {
253        let mut output = Self::new();
254        output.bytes[..self.len].copy_from_slice(self.as_bytes());
255        output.len = self.len;
256        output
257    }
258}
259
260impl<const CAP: usize> core::fmt::Debug for DecodedBuffer<CAP> {
261    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
262        formatter
263            .debug_struct("DecodedBuffer")
264            .field("bytes", &"<redacted>")
265            .field("len", &self.len)
266            .field("capacity", &CAP)
267            .finish()
268    }
269}
270
271impl<const CAP: usize> Default for DecodedBuffer<CAP> {
272    fn default() -> Self {
273        Self::new()
274    }
275}
276
277impl<const CAP: usize> Drop for DecodedBuffer<CAP> {
278    fn drop(&mut self) {
279        self.clear();
280    }
281}
282
283impl<const CAP: usize> TryFrom<&[u8]> for DecodedBuffer<CAP> {
284    type Error = DecodeError;
285
286    /// Decodes strict standard padded Base64 into a stack-backed buffer.
287    ///
288    /// Use [`crate::Engine::decode_buffer`] or [`crate::Profile::decode_buffer`]
289    /// when a different alphabet, padding mode, or line-wrapping profile is
290    /// required. These conversions always use [`crate::STANDARD`]; URL-safe,
291    /// bcrypt, crypt, MIME, PEM, and custom alphabets must use an explicit
292    /// engine or profile.
293    ///
294    /// # Security
295    ///
296    /// This idiomatic conversion uses the strict standard decoder, not the
297    /// constant-time-oriented decoder. It may branch or return early on
298    /// malformed input and reports exact [`DecodeError`] positions. For
299    /// secret-bearing tokens or key material where malformed-input timing
300    /// matters, use [`crate::ct::CtEngine::decode_buffer`] through
301    /// [`crate::ct::STANDARD`] instead.
302    fn try_from(input: &[u8]) -> Result<Self, Self::Error> {
303        STANDARD.decode_buffer(input)
304    }
305}
306
307impl<const CAP: usize, const N: usize> TryFrom<&[u8; N]> for DecodedBuffer<CAP> {
308    type Error = DecodeError;
309
310    /// Decodes a strict standard padded Base64 byte array into a stack-backed
311    /// buffer.
312    ///
313    /// Use [`crate::Engine::decode_buffer`] or [`crate::Profile::decode_buffer`]
314    /// when a different alphabet, padding mode, or line-wrapping profile is
315    /// required. These conversions always use [`crate::STANDARD`]; URL-safe,
316    /// bcrypt, crypt, MIME, PEM, and custom alphabets must use an explicit
317    /// engine or profile.
318    ///
319    /// # Security
320    ///
321    /// This idiomatic conversion uses the strict standard decoder, not the
322    /// constant-time-oriented decoder. It may branch or return early on
323    /// malformed input and reports exact [`DecodeError`] positions. For
324    /// secret-bearing tokens or key material where malformed-input timing
325    /// matters, use [`crate::ct::CtEngine::decode_buffer`] through
326    /// [`crate::ct::STANDARD`] instead.
327    fn try_from(input: &[u8; N]) -> Result<Self, Self::Error> {
328        Self::try_from(&input[..])
329    }
330}
331
332impl<const CAP: usize> TryFrom<&str> for DecodedBuffer<CAP> {
333    type Error = DecodeError;
334
335    /// Decodes strict standard padded Base64 text into a stack-backed buffer.
336    ///
337    /// Use [`crate::Engine::decode_buffer`] or [`crate::Profile::decode_buffer`]
338    /// when a different alphabet, padding mode, or line-wrapping profile is
339    /// required. These conversions always use [`crate::STANDARD`]; URL-safe,
340    /// bcrypt, crypt, MIME, PEM, and custom alphabets must use an explicit
341    /// engine or profile.
342    ///
343    /// # Security
344    ///
345    /// This idiomatic conversion uses the strict standard decoder, not the
346    /// constant-time-oriented decoder. It may branch or return early on
347    /// malformed input and reports exact [`DecodeError`] positions. For
348    /// secret-bearing tokens or key material where malformed-input timing
349    /// matters, use [`crate::ct::CtEngine::decode_buffer`] through
350    /// [`crate::ct::STANDARD`] instead.
351    fn try_from(input: &str) -> Result<Self, Self::Error> {
352        Self::try_from(input.as_bytes())
353    }
354}
355
356impl<const CAP: usize> core::str::FromStr for DecodedBuffer<CAP> {
357    type Err = DecodeError;
358
359    /// Decodes strict standard padded Base64 text into a stack-backed buffer.
360    ///
361    /// Use [`crate::Engine::decode_buffer`] or [`crate::Profile::decode_buffer`]
362    /// when a different alphabet, padding mode, or line-wrapping profile is
363    /// required. These conversions always use [`crate::STANDARD`]; URL-safe,
364    /// bcrypt, crypt, MIME, PEM, and custom alphabets must use an explicit
365    /// engine or profile.
366    ///
367    /// # Security
368    ///
369    /// This idiomatic conversion uses the strict standard decoder, not the
370    /// constant-time-oriented decoder. It may branch or return early on
371    /// malformed input and reports exact [`DecodeError`] positions. For
372    /// secret-bearing tokens or key material where malformed-input timing
373    /// matters, use [`crate::ct::CtEngine::decode_buffer`] through
374    /// [`crate::ct::STANDARD`] instead.
375    fn from_str(input: &str) -> Result<Self, Self::Err> {
376        Self::try_from(input)
377    }
378}