Skip to main content

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;