base64_ng/ct/mod.rs
1//! Constant-time-oriented scalar decoding APIs.
2//!
3//! This module is separate from the default decoder so callers can opt into a
4//! slower path with a narrower timing target. It avoids lookup tables indexed
5//! by secret input bytes while mapping Base64 symbols and reports malformed
6//! content through one opaque error. It is not documented as a formally
7//! verified cryptographic constant-time API.
8//!
9//! # Security
10//!
11//! Input length, decoded length, selected alphabet, and final success or
12//! failure remain public. The clear-tail methods wipe caller-owned output on
13//! error, but decoded bytes are written during the fixed-shape decode loop
14//! before final validation is reported. In shared-memory, enclave, or HSM-style
15//! threat models where another component can observe the output buffer during
16//! the call, prefer [`crate::ct::CtEngine::decode_slice_staged_clear_tail`]
17//! with a private staging buffer. In those deployments,
18//! [`crate::ct::CtEngine::decode_slice_clear_tail`] is not sufficient by
19//! itself because it wipes caller-owned output only after the internal decode
20//! loop reaches the final error gate. Treat
21//! [`crate::ct::CtEngine::decode_slice_staged_clear_tail`] as the default for
22//! shared-memory, enclave, HSM-adjacent, or multi-principal deployments;
23//! [`crate::ct::CtEngine::decode_slice_clear_tail`] is appropriate only when
24//! the output buffer is not observable during the call.
25//!
26//! # Platform Posture
27//!
28//! The CT result gate uses architecture-specific best-effort barriers where
29//! stable Rust exposes them. On `AArch64`, the emitted CSDB hint is reported as
30//! `hardware-speculation-barrier-unattested` because older cores may treat it
31//! as a no-op; deployments must attest the exact core behavior before relying
32//! on it for high assurance. On RISC-V, `fence rw, rw` is an ordering fence,
33//! not a Spectre-v1 speculation barrier, and the built-in high-assurance
34//! runtime policy intentionally rejects that posture. RISC-V deployments on
35//! speculative cores need platform-level mitigations and startup policy checks
36//! that make the gap explicit.
37//!
38//! The dependency-free comparison helpers on redacted buffers are
39//! constant-time-oriented best effort, not formally audited MAC or token
40//! comparison primitives. Applications that can admit dependencies and need a
41//! reviewed comparison primitive should use one at the protocol boundary.
42//!
43//! The CT decoder exposes only clear-tail and stack-backed decode APIs. The
44//! former non-clear-tail methods were removed before the `1.0` stable boundary
45//! because they could leave decoded plaintext in caller-owned buffers after
46//! malformed input errors.
47//!
48//! ```compile_fail
49//! use base64_ng::ct;
50//!
51//! let mut output = [0u8; 8];
52//! let _ = ct::STANDARD.decode_slice(b"aGk=", &mut output);
53//! ```
54//!
55//! ```compile_fail
56//! use base64_ng::ct;
57//!
58//! let mut buffer = *b"aGk=";
59//! let _ = ct::STANDARD.decode_in_place(&mut buffer);
60//! ```
61#[cfg(feature = "alloc")]
62use crate::SecretBuffer;
63use crate::{Alphabet, DecodeError, DecodedBuffer, Standard, UrlSafe};
64use core::marker::PhantomData;
65
66/// Standard Base64 constant-time-oriented decoder with padding.
67pub const STANDARD: CtEngine<Standard, true> = CtEngine::new();
68
69/// Standard Base64 constant-time-oriented decoder without padding.
70pub const STANDARD_NO_PAD: CtEngine<Standard, false> = CtEngine::new();
71
72/// URL-safe Base64 constant-time-oriented decoder with padding.
73pub const URL_SAFE: CtEngine<UrlSafe, true> = CtEngine::new();
74
75/// URL-safe Base64 constant-time-oriented decoder without padding.
76pub const URL_SAFE_NO_PAD: CtEngine<UrlSafe, false> = CtEngine::new();
77
78/// A zero-sized constant-time-oriented Base64 decoder.
79///
80/// # Security
81///
82/// For ordinary secret-bearing inputs, prefer
83/// [`Self::decode_slice_clear_tail`], [`Self::decode_buffer`], or
84/// [`Self::decode_in_place_clear_tail`]. For shared-memory,
85/// enclave-adjacent, HSM-style, or multi-principal deployments where
86/// another component can observe caller-owned output during the call, use
87/// [`Self::decode_slice_staged_clear_tail`] with a private staging buffer
88/// so malformed input cannot transiently write decoded bytes into the
89/// public output buffer before the final error gate.
90#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
91pub struct CtEngine<A, const PAD: bool> {
92 alphabet: PhantomData<A>,
93}
94
95impl<A, const PAD: bool> CtEngine<A, PAD>
96where
97 A: Alphabet,
98{
99 /// Creates a new constant-time-oriented decoder engine.
100 #[must_use]
101 pub const fn new() -> Self {
102 Self {
103 alphabet: PhantomData,
104 }
105 }
106
107 /// Returns whether this constant-time-oriented decoder expects padded
108 /// input.
109 #[must_use]
110 pub const fn is_padded(&self) -> bool {
111 PAD
112 }
113
114 /// Validates `input` without writing decoded bytes.
115 ///
116 /// This uses the same constant-time-oriented symbol mapping and opaque
117 /// malformed-input error behavior as
118 /// [`Self::decode_slice_clear_tail`]. Input length, padding length, and
119 /// final success or failure remain public.
120 ///
121 /// # Examples
122 ///
123 /// ```
124 /// use base64_ng::ct;
125 ///
126 /// ct::STANDARD.validate_result(b"aGVsbG8=").unwrap();
127 /// assert!(ct::STANDARD.validate_result(b"aGVsbG8").is_err());
128 /// ```
129 pub fn validate_result(&self, input: &[u8]) -> Result<(), DecodeError> {
130 ct_validate_decode::<A, PAD>(input)
131 }
132
133 /// Returns whether `input` is valid for this constant-time-oriented
134 /// decoder.
135 ///
136 /// This is a convenience wrapper around [`Self::validate_result`].
137 ///
138 /// # Examples
139 ///
140 /// ```
141 /// use base64_ng::ct;
142 ///
143 /// assert!(ct::URL_SAFE_NO_PAD.validate(b"-_8"));
144 /// assert!(!ct::URL_SAFE_NO_PAD.validate(b"+/8"));
145 /// ```
146 #[must_use]
147 pub fn validate(&self, input: &[u8]) -> bool {
148 self.validate_result(input).is_ok()
149 }
150
151 /// Returns the exact decoded length for valid input.
152 ///
153 /// This uses the same constant-time-oriented validation policy as
154 /// [`Self::validate_result`] before returning a length. Input length,
155 /// padding length, and final success or failure remain public.
156 pub fn decoded_len(&self, input: &[u8]) -> Result<usize, DecodeError> {
157 ct_decoded_len::<A, PAD>(input)
158 }
159
160 /// Decodes `input` into `output` and clears all bytes after the
161 /// decoded prefix.
162 ///
163 /// If decoding fails, the entire output buffer is cleared before the
164 /// error is returned. Use this variant for sensitive payloads where
165 /// partially decoded bytes from rejected input should not remain in the
166 /// caller-owned output buffer.
167 ///
168 /// # Security: Transient Plaintext Window
169 ///
170 /// Decoded bytes are written to `output` progressively during the
171 /// fixed-shape decode loop before malformed-input detection is
172 /// complete. On error, the entire `output` is wiped before returning,
173 /// but a concurrent same-process observer with access to `output`
174 /// during the call may observe transient partial plaintext from valid
175 /// leading quanta. For shared-memory, enclave-adjacent, HSM-style, or
176 /// multi-principal deployments where even transient writes are
177 /// unacceptable, use [`Self::decode_slice_staged_clear_tail`] with a
178 /// private staging buffer.
179 ///
180 /// # Examples
181 ///
182 /// ```
183 /// use base64_ng::ct;
184 ///
185 /// let mut output = [0xff; 8];
186 /// let written = ct::STANDARD
187 /// .decode_slice_clear_tail(b"aGk=", &mut output)
188 /// .unwrap();
189 ///
190 /// assert_eq!(&output[..written], b"hi");
191 /// assert!(output[written..].iter().all(|byte| *byte == 0));
192 /// ```
193 #[must_use = "handle decode errors; use decode_slice_staged_clear_tail for shared-memory or HSM-style threat models"]
194 pub fn decode_slice_clear_tail(
195 &self,
196 input: &[u8],
197 output: &mut [u8],
198 ) -> Result<usize, DecodeError> {
199 let written = match ct_decode_slice::<A, PAD>(input, output) {
200 Ok(written) => written,
201 Err(err) => {
202 crate::wipe_bytes(output);
203 return Err(err);
204 }
205 };
206 crate::wipe_tail(output, written);
207 Ok(written)
208 }
209
210 /// Decodes through caller-provided private staging before copying into
211 /// `output`.
212 ///
213 /// This variant is for shared-memory or sandboxed deployments where
214 /// the caller-owned `output` buffer must not contain transient decoded
215 /// bytes from malformed input. The `staging` buffer must be at least
216 /// the decoded length of `input` and must not be shared with
217 /// untrusted concurrent observers. On success, decoded bytes are
218 /// copied from `staging` into `output`; on error, both buffers are
219 /// cleared before returning.
220 ///
221 /// Input length, final success or failure, and decoded length remain
222 /// public.
223 #[must_use = "handle decode errors; staged decode is for shared-memory or HSM-style threat models"]
224 pub fn decode_slice_staged_clear_tail(
225 &self,
226 input: &[u8],
227 output: &mut [u8],
228 staging: &mut [u8],
229 ) -> Result<usize, DecodeError> {
230 ct_decode_slice_staged_clear_tail::<A, PAD>(input, output, staging)
231 }
232
233 /// Decodes `input` into a stack-backed buffer.
234 ///
235 /// This uses the same constant-time-oriented scalar decoder as
236 /// [`Self::decode_slice_clear_tail`] and clears the internal backing
237 /// array before returning an error.
238 ///
239 /// # Examples
240 ///
241 /// ```
242 /// use base64_ng::ct;
243 ///
244 /// let decoded = ct::STANDARD.decode_buffer::<5>(b"aGVsbG8=").unwrap();
245 ///
246 /// assert_eq!(decoded.as_bytes(), b"hello");
247 /// ```
248 pub fn decode_buffer<const CAP: usize>(
249 &self,
250 input: &[u8],
251 ) -> Result<DecodedBuffer<CAP>, DecodeError> {
252 let mut output = DecodedBuffer::new();
253 let written = match self.decode_slice_clear_tail(input, output.as_mut_capacity()) {
254 Ok(written) => written,
255 Err(err) => {
256 output.clear();
257 return Err(err);
258 }
259 };
260 output.set_filled(written)?;
261 Ok(output)
262 }
263
264 /// Decodes `input` into an owned byte vector.
265 ///
266 /// This uses the same constant-time-oriented scalar decoder as
267 /// [`Self::decode_slice_clear_tail`]. If decoding fails, the allocated
268 /// output buffer is cleared before the error is returned.
269 ///
270 /// Use [`Self::decode_secret`] for secret-bearing payloads that should stay
271 /// on the crate's redacted, drop-wiping buffer path. Use
272 /// [`Self::decode_secret_staged`] for shared-memory, enclave-adjacent,
273 /// HSM-style, or multi-principal deployments where even transient writes
274 /// into the final heap allocation are unacceptable.
275 #[cfg(feature = "alloc")]
276 #[must_use = "for secret-bearing payloads use decode_secret, which returns a redacted buffer with drop-time cleanup"]
277 pub fn decode_vec(&self, input: &[u8]) -> Result<alloc::vec::Vec<u8>, DecodeError> {
278 let required = self.decoded_len(input)?;
279 let mut output = alloc::vec![0; required];
280 // decode_slice_clear_tail wipes output on error.
281 let written = self.decode_slice_clear_tail(input, &mut output)?;
282 output.truncate(written);
283 Ok(output)
284 }
285
286 /// Decodes `input` into a redacted owned secret buffer.
287 ///
288 /// This is the recommended heap-owning CT decode path for secret-bearing
289 /// payloads. It decodes with [`Self::decode_vec`] and then wraps the result
290 /// in [`SecretBuffer`], which redacts formatting and clears initialized
291 /// bytes plus spare vector capacity on drop.
292 ///
293 /// # Security: Transient Plaintext Window
294 ///
295 /// This function uses the non-staged CT decode path. Decoded bytes are
296 /// written transiently into the heap allocation before the final error
297 /// gate. On error, the allocation is wiped before returning, but a
298 /// concurrent same-process observer with access to that allocation during
299 /// the call may observe transient partial plaintext. For shared-memory,
300 /// enclave-adjacent, HSM-style, or multi-principal deployments where even
301 /// transient writes into the final heap allocation are unacceptable, use
302 /// [`Self::decode_secret_staged`] with a stack-backed private staging
303 /// capacity large enough for the decoded value.
304 ///
305 /// # Examples
306 ///
307 /// ```
308 /// use base64_ng::ct;
309 ///
310 /// let decoded = ct::STANDARD.decode_secret(b"aGVsbG8=").unwrap();
311 /// assert!(decoded.constant_time_eq_public_len(b"hello"));
312 /// ```
313 #[cfg(feature = "alloc")]
314 pub fn decode_secret(&self, input: &[u8]) -> Result<SecretBuffer, DecodeError> {
315 self.decode_vec(input).map(SecretBuffer::from_vec)
316 }
317
318 /// Decodes `input` into a redacted owned secret buffer through private
319 /// stack staging.
320 ///
321 /// `STAGE` must be at least the decoded length of `input`. Decoded bytes
322 /// are written to a stack-backed staging buffer first and copied into the
323 /// returned heap buffer only after the full constant-time-oriented decode
324 /// succeeds. On error, both staging and heap output buffers are wiped before
325 /// returning.
326 ///
327 /// This is the preferred owned decode API for shared-memory,
328 /// enclave-adjacent, HSM-style, or multi-principal deployments where the
329 /// final heap allocation must not contain transient partial plaintext from
330 /// rejected input.
331 ///
332 /// # Errors
333 ///
334 /// Returns [`DecodeError::StagingTooSmall`] if `STAGE` is smaller than the
335 /// decoded length of `input`. `STAGE` is checked at runtime because the
336 /// encoded input length is not a compile-time value.
337 ///
338 /// # Examples
339 ///
340 /// ```
341 /// use base64_ng::ct;
342 ///
343 /// let decoded = ct::STANDARD
344 /// .decode_secret_staged::<5>(b"aGVsbG8=")
345 /// .unwrap();
346 /// assert!(decoded.constant_time_eq_public_len(b"hello"));
347 /// ```
348 #[cfg(feature = "alloc")]
349 pub fn decode_secret_staged<const STAGE: usize>(
350 &self,
351 input: &[u8],
352 ) -> Result<SecretBuffer, DecodeError> {
353 let required = self.decoded_len(input)?;
354 let mut staging = DecodedBuffer::<STAGE>::new();
355 let mut output = alloc::vec![0; required];
356 let written =
357 self.decode_slice_staged_clear_tail(input, &mut output, staging.as_mut_capacity())?;
358 output.truncate(written);
359 Ok(SecretBuffer::from_vec(output))
360 }
361
362 /// Decodes `buffer` in place and clears all bytes after the decoded
363 /// prefix.
364 ///
365 /// If decoding fails, the entire buffer is cleared before the error is
366 /// returned.
367 ///
368 /// # Security: Transient Plaintext Window
369 ///
370 /// This in-place API writes decoded bytes into `buffer` during the
371 /// fixed-shape decode loop before malformed-input detection is
372 /// complete. On error, the entire buffer is wiped before returning,
373 /// but concurrent same-process observers with access to the same memory
374 /// can observe transient partial plaintext. Use
375 /// [`Self::decode_slice_staged_clear_tail`] with a private staging
376 /// buffer when shared-memory or enclave-adjacent deployments cannot
377 /// tolerate that window.
378 ///
379 /// # Examples
380 ///
381 /// ```
382 /// use base64_ng::ct;
383 ///
384 /// let mut buffer = *b"aGk=";
385 /// let decoded = ct::STANDARD.decode_in_place_clear_tail(&mut buffer).unwrap();
386 ///
387 /// assert_eq!(decoded, b"hi");
388 /// ```
389 pub fn decode_in_place_clear_tail<'a>(
390 &self,
391 buffer: &'a mut [u8],
392 ) -> Result<&'a mut [u8], DecodeError> {
393 let len = match ct_decode_in_place::<A, PAD>(buffer) {
394 Ok(len) => len,
395 Err(err) => {
396 crate::wipe_bytes(buffer);
397 return Err(err);
398 }
399 };
400 crate::wipe_tail(buffer, len);
401 Ok(&mut buffer[..len])
402 }
403}
404
405impl<A, const PAD: bool> core::fmt::Display for CtEngine<A, PAD> {
406 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
407 write!(formatter, "ct padded={PAD}")
408 }
409}
410
411mod decode;
412mod equality;
413mod padded;
414mod unpadded;
415
416use decode::{
417 ct_decode_in_place, ct_decode_slice, ct_decode_slice_staged_clear_tail, ct_decoded_len,
418 ct_validate_decode,
419};
420#[cfg(test)]
421pub(crate) use equality::report_ct_error;
422pub(crate) use equality::{
423 constant_time_eq_fixed_width_array, constant_time_eq_public_len, ct_mask_eq_u8, ct_mask_lt_u8,
424};
425#[cfg(test)]
426pub(crate) use padded::ct_padded_final_quantum;