1#[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 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 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}