Skip to main content

base64_ng/v2/
secret_in_place.rs

1//! Fixed-work staged secret decode for finite caller-owned buffers.
2
3#[cfg(test)]
4use super::contracts::BackendFault;
5use super::{
6    in_place::{InPlaceError, require_disjoint_slices, require_input_prefix},
7    specifications::{Base64, Codec, CodecSettings, DecodePadding},
8};
9
10struct SecretDecodeOutcome {
11    written: usize,
12    invalid: u8,
13}
14
15#[derive(Clone, Copy)]
16enum InjectedFault {
17    None,
18    #[cfg(test)]
19    AfterDecode,
20    #[cfg(all(test, feature = "std"))]
21    PanicAfterDecode,
22}
23
24struct StagingWipeGuard<'a> {
25    bytes: &'a mut [u8],
26}
27
28impl<'a> StagingWipeGuard<'a> {
29    fn new(bytes: &'a mut [u8]) -> Self {
30        Self { bytes }
31    }
32
33    fn prefix_mut(&mut self, len: usize) -> &mut [u8] {
34        &mut self.bytes[..len]
35    }
36
37    fn prefix(&self, len: usize) -> &[u8] {
38        &self.bytes[..len]
39    }
40}
41
42impl Drop for StagingWipeGuard<'_> {
43    fn drop(&mut self) {
44        crate::wipe_bytes(self.bytes);
45    }
46}
47
48#[cfg(test)]
49static FIXED_WORK_CALLS: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
50#[cfg(test)]
51static SYMBOL_SCANS: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
52
53impl<S: Codec> Base64<S> {
54    /// Returns the private staging capacity required by secret in-place decode.
55    ///
56    /// The capacity depends only on the public encoded length. It deliberately
57    /// reserves three candidate bytes for every complete or partial encoded
58    /// block so padding validity is not inspected during preflight.
59    pub fn secret_decode_staging_len(&self, input_len: usize) -> Result<usize, InPlaceError> {
60        if !self.settings().permits_secret_processing() {
61            return Err(InPlaceError::SecretPolicyUnsupported);
62        }
63        fixed_work_staging_len(input_len)
64    }
65
66    /// Decodes a secret-bearing encoded prefix through disjoint private staging.
67    ///
68    /// Preflight errors leave both buffers byte-for-byte unchanged. Every
69    /// post-preflight input, valid or invalid, receives the same number of
70    /// alphabet scans for its public length. Invalid input leaves `buffer`
71    /// unchanged and wipes all staging. Success copies the staged plaintext
72    /// into `buffer` only after the result gate, then wipes all staging.
73    ///
74    /// The complete `buffer` and `private_staging` byte ranges must be
75    /// disjoint. Unsafe callers must validate raw ranges before constructing
76    /// mutable references; this check cannot repair pre-existing aliased
77    /// references. Validity and successful release become public after the
78    /// result gate; the success-only copy is outside the fixed-work claim.
79    pub fn decode_in_place_staged(
80        &self,
81        buffer: &mut [u8],
82        input_len: usize,
83        private_staging: &mut [u8],
84    ) -> Result<usize, InPlaceError> {
85        decode_in_place_staged_inner(
86            self,
87            buffer,
88            input_len,
89            private_staging,
90            InjectedFault::None,
91        )
92    }
93}
94
95fn decode_in_place_staged_inner<S: Codec>(
96    codec: &Base64<S>,
97    buffer: &mut [u8],
98    input_len: usize,
99    private_staging: &mut [u8],
100    injected_fault: InjectedFault,
101) -> Result<usize, InPlaceError> {
102    require_input_prefix(input_len, buffer.len())?;
103    let settings = codec.settings();
104    if !settings.permits_secret_processing() {
105        return Err(InPlaceError::SecretPolicyUnsupported);
106    }
107    let staging_len = fixed_work_staging_len(input_len)?;
108    if private_staging.len() < staging_len {
109        return Err(InPlaceError::StagingTooSmall {
110            required: staging_len,
111            available: private_staging.len(),
112        });
113    }
114    require_disjoint_slices(buffer, private_staging)?;
115
116    let mut staging_guard = StagingWipeGuard::new(private_staging);
117    let outcome = decode_secret_fixed_work(
118        settings,
119        &buffer[..input_len],
120        staging_guard.prefix_mut(staging_len),
121    );
122
123    #[cfg(all(test, feature = "std"))]
124    if matches!(injected_fault, InjectedFault::PanicAfterDecode) {
125        std::panic::panic_any("reviewed staged secret cleanup test");
126    }
127
128    #[cfg(test)]
129    if matches!(injected_fault, InjectedFault::AfterDecode) {
130        crate::wipe_bytes(buffer);
131        return Err(InPlaceError::Backend(BackendFault::ImpossibleState));
132    }
133    let _ = injected_fault;
134
135    crate::ct_error_gate_barrier(outcome.invalid, 0);
136    if core::hint::black_box(outcome.invalid) != 0 {
137        return Err(InPlaceError::InvalidSecretInput);
138    }
139
140    buffer[..outcome.written].copy_from_slice(staging_guard.prefix(outcome.written));
141    Ok(outcome.written)
142}
143
144fn fixed_work_staging_len(input_len: usize) -> Result<usize, InPlaceError> {
145    let complete = (input_len / 4)
146        .checked_mul(3)
147        .ok_or(InPlaceError::LengthOverflow)?;
148    if input_len.is_multiple_of(4) {
149        Ok(complete)
150    } else {
151        complete.checked_add(3).ok_or(InPlaceError::LengthOverflow)
152    }
153}
154
155fn decode_secret_fixed_work(
156    settings: CodecSettings,
157    input: &[u8],
158    staging: &mut [u8],
159) -> SecretDecodeOutcome {
160    #[cfg(test)]
161    FIXED_WORK_CALLS.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
162
163    match settings.decode_padding() {
164        DecodePadding::RequireCanonical => decode_padded(settings, input, staging),
165        DecodePadding::Forbid => decode_unpadded(settings, input, staging),
166        DecodePadding::Indifferent => SecretDecodeOutcome {
167            written: 0,
168            invalid: 0xff,
169        },
170    }
171}
172
173fn decode_padded(settings: CodecSettings, input: &[u8], staging: &mut [u8]) -> SecretDecodeOutcome {
174    if input.is_empty() {
175        return SecretDecodeOutcome {
176            written: 0,
177            invalid: 0,
178        };
179    }
180
181    let mut invalid = if input.len().is_multiple_of(4) {
182        0
183    } else {
184        0xff
185    };
186    let mut read = 0;
187    let mut write = 0;
188    let mut final_padding = 0u8;
189
190    while read < input.len() {
191        let actual = (input.len() - read).min(4);
192        let bytes = read_block(input, read);
193        let values = decode_block(settings, bytes);
194        write_candidate(staging, write, values);
195        let final_block = input.len() - read <= 4;
196
197        if final_block {
198            if actual != 4 {
199                invalid = accumulate(invalid, 0xff);
200            }
201            let equals_third = crate::ct_mask_eq_u8(bytes[2], b'=');
202            let equals_fourth = crate::ct_mask_eq_u8(bytes[3], b'=');
203            let no_padding = !equals_third & !equals_fourth;
204            let one_padding = !equals_third & equals_fourth;
205            let two_padding = equals_third & equals_fourth;
206            let malformed_padding = equals_third & !equals_fourth;
207            let require_third = no_padding | one_padding;
208
209            invalid = accumulate(invalid, !values[0].1);
210            invalid = accumulate(invalid, !values[1].1);
211            invalid = accumulate(invalid, !values[2].1 & require_third);
212            invalid = accumulate(invalid, !values[3].1 & no_padding);
213            invalid = accumulate(invalid, malformed_padding);
214            invalid = accumulate(
215                invalid,
216                crate::ct_mask_nonzero_u8(values[1].0 & 0x0f) & two_padding,
217            );
218            invalid = accumulate(
219                invalid,
220                crate::ct_mask_nonzero_u8(values[2].0 & 0x03) & one_padding,
221            );
222            final_padding = (equals_third & 1) + (equals_fourth & 1);
223        } else {
224            invalid = accumulate(invalid, !values[0].1);
225            invalid = accumulate(invalid, !values[1].1);
226            invalid = accumulate(invalid, !values[2].1);
227            invalid = accumulate(invalid, !values[3].1);
228        }
229
230        read += actual;
231        write += 3;
232    }
233
234    SecretDecodeOutcome {
235        written: write - usize::from(final_padding),
236        invalid,
237    }
238}
239
240fn decode_unpadded(
241    settings: CodecSettings,
242    input: &[u8],
243    staging: &mut [u8],
244) -> SecretDecodeOutcome {
245    let mut invalid = 0u8;
246    let mut read = 0;
247    let mut write = 0;
248    let mut visible = 0;
249
250    while read < input.len() {
251        let actual = (input.len() - read).min(4);
252        let bytes = read_block(input, read);
253        let values = decode_block(settings, bytes);
254        write_candidate(staging, write, values);
255
256        invalid = accumulate(invalid, !values[0].1);
257        match actual {
258            4 => {
259                invalid = accumulate(invalid, !values[1].1);
260                invalid = accumulate(invalid, !values[2].1);
261                invalid = accumulate(invalid, !values[3].1);
262                visible += 3;
263            }
264            3 => {
265                invalid = accumulate(invalid, !values[1].1);
266                invalid = accumulate(invalid, !values[2].1);
267                invalid = accumulate(invalid, crate::ct_mask_nonzero_u8(values[2].0 & 0x03));
268                visible += 2;
269            }
270            2 => {
271                invalid = accumulate(invalid, !values[1].1);
272                invalid = accumulate(invalid, crate::ct_mask_nonzero_u8(values[1].0 & 0x0f));
273                visible += 1;
274            }
275            _ => invalid = accumulate(invalid, 0xff),
276        }
277
278        read += actual;
279        write += 3;
280    }
281
282    SecretDecodeOutcome {
283        written: visible,
284        invalid,
285    }
286}
287
288fn read_block(input: &[u8], read: usize) -> [u8; 4] {
289    [
290        input.get(read).copied().unwrap_or(0),
291        input.get(read + 1).copied().unwrap_or(0),
292        input.get(read + 2).copied().unwrap_or(0),
293        input.get(read + 3).copied().unwrap_or(0),
294    ]
295}
296
297fn decode_block(settings: CodecSettings, bytes: [u8; 4]) -> [(u8, u8); 4] {
298    [
299        decode_symbol(settings, bytes[0]),
300        decode_symbol(settings, bytes[1]),
301        decode_symbol(settings, bytes[2]),
302        decode_symbol(settings, bytes[3]),
303    ]
304}
305
306#[inline(never)]
307fn decode_symbol(settings: CodecSettings, byte: u8) -> (u8, u8) {
308    #[cfg(test)]
309    SYMBOL_SCANS.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
310
311    let mut decoded = 0u8;
312    let mut valid = 0u8;
313    let mut candidate = 0u8;
314    while candidate < 64 {
315        let matches = core::hint::black_box(crate::ct_mask_eq_u8(
316            core::hint::black_box(byte),
317            core::hint::black_box(settings.alphabet().as_array()[usize::from(candidate)]),
318        ));
319        decoded = accumulate(decoded, candidate & matches);
320        valid = accumulate(valid, matches);
321        candidate += 1;
322    }
323    (decoded, valid)
324}
325
326fn write_candidate(staging: &mut [u8], write: usize, values: [(u8, u8); 4]) {
327    staging[write] = (values[0].0 << 2) | (values[1].0 >> 4);
328    staging[write + 1] = (values[1].0 << 4) | (values[2].0 >> 2);
329    staging[write + 2] = (values[2].0 << 6) | values[3].0;
330}
331
332fn accumulate(accumulator: u8, value: u8) -> u8 {
333    crate::ct_accumulate_u8(accumulator, value)
334}
335
336#[cfg(test)]
337pub(super) fn decode_with_injected_fault_for_test<S: Codec>(
338    codec: &Base64<S>,
339    buffer: &mut [u8],
340    input_len: usize,
341    private_staging: &mut [u8],
342) -> Result<usize, InPlaceError> {
343    decode_in_place_staged_inner(
344        codec,
345        buffer,
346        input_len,
347        private_staging,
348        InjectedFault::AfterDecode,
349    )
350}
351
352#[cfg(all(test, feature = "std"))]
353pub(super) fn decode_with_injected_panic_for_test<S: Codec>(
354    codec: &Base64<S>,
355    buffer: &mut [u8],
356    input_len: usize,
357    private_staging: &mut [u8],
358) {
359    let _ = decode_in_place_staged_inner(
360        codec,
361        buffer,
362        input_len,
363        private_staging,
364        InjectedFault::PanicAfterDecode,
365    );
366}
367
368#[cfg(test)]
369pub(super) fn reset_work_counters_for_test() {
370    FIXED_WORK_CALLS.store(0, core::sync::atomic::Ordering::Relaxed);
371    SYMBOL_SCANS.store(0, core::sync::atomic::Ordering::Relaxed);
372}
373
374#[cfg(test)]
375pub(super) fn work_counters_for_test() -> (usize, usize) {
376    (
377        FIXED_WORK_CALLS.load(core::sync::atomic::Ordering::Relaxed),
378        SYMBOL_SCANS.load(core::sync::atomic::Ordering::Relaxed),
379    )
380}