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