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}