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}