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;