Skip to main content

base64_ng/v2/secret/
frames.rs

1//! Bounded owners around the fixed-work secret decoder state.
2
3#[cfg(feature = "alloc")]
4use super::SecretVec;
5use super::{SecretArray, SecretInput, SecretOutput};
6use crate::v2::{
7    Progress,
8    secret_decoder::{
9        FinalCandidate, MAX_SECRET_STACK_DECODED, SecretDecodeError, SecretDecoderState,
10        require_disjoint,
11    },
12    specifications::{Base64, Codec},
13};
14
15/// Stack-backed secret decode with disjoint private and final arrays.
16pub struct SecretArrayFrame<const N: usize> {
17    state: SecretDecoderState,
18    staging: [u8; N],
19    output: [u8; N],
20}
21
22impl<const N: usize> SecretArrayFrame<N> {
23    const CAPACITY_ASSERT: () = enforce_stack_capacity::<N>();
24
25    /// Creates one empty fixed-capacity secret decode frame.
26    pub fn new<S: Codec>(codec: &Base64<S>) -> Result<Self, SecretDecodeError> {
27        const { enforce_stack_capacity::<N>() }
28        let () = Self::CAPACITY_ASSERT;
29        Ok(Self {
30            state: SecretDecoderState::new(codec.settings(), N)?,
31            staging: [0; N],
32            output: [0; N],
33        })
34    }
35
36    /// Consumes one classified chunk without releasing plaintext.
37    pub fn update(&mut self, input: &SecretInput<'_>) -> Result<Progress, SecretDecodeError> {
38        if let Err(error) = self.check_input(input.classified_bytes()) {
39            self.state.latch_external_failure();
40            self.fail_storage();
41            return Err(error);
42        }
43        match self
44            .state
45            .update(input.classified_bytes(), &mut self.staging)
46        {
47            Ok(progress) => Ok(progress),
48            Err(error) => {
49                self.fail_storage();
50                Err(error)
51            }
52        }
53    }
54
55    /// Applies the result gate and returns secret output only on success.
56    pub fn finish(mut self) -> Result<SecretArray<N>, SecretDecodeError> {
57        let final_candidate = match self.state.finish() {
58            Ok(candidate) => candidate,
59            Err(error) => {
60                self.fail_storage();
61                return Err(error);
62            }
63        };
64        release(&self.staging, &final_candidate, &mut self.output);
65        crate::wipe_bytes(&mut self.staging);
66        let output = core::mem::replace(&mut self.output, [0; N]);
67        SecretArray::from_frame(output, final_candidate.written()).map_err(|error| {
68            SecretDecodeError::OutputFull {
69                required: error.length(),
70                available: error.capacity(),
71            }
72        })
73    }
74
75    /// Returns public decoder metadata without exposing staged bytes.
76    #[must_use]
77    pub const fn state(&self) -> &SecretDecoderState {
78        &self.state
79    }
80
81    fn check_input(&self, input: &[u8]) -> Result<(), SecretDecodeError> {
82        require_disjoint(input, &self.staging)?;
83        require_disjoint(input, &self.output)
84    }
85
86    fn fail_storage(&mut self) {
87        crate::wipe_bytes(&mut self.staging);
88        crate::wipe_bytes(&mut self.output);
89    }
90
91    #[cfg(test)]
92    pub(crate) const fn storage_for_test(&self) -> (&[u8; N], &[u8; N]) {
93        (&self.staging, &self.output)
94    }
95}
96
97#[allow(clippy::manual_assert)]
98const fn enforce_stack_capacity<const N: usize>() {
99    if N > MAX_SECRET_STACK_DECODED {
100        panic!("SecretArrayFrame decoded capacity exceeds 1024-byte stack limit");
101    }
102}
103
104impl<const N: usize> Drop for SecretArrayFrame<N> {
105    fn drop(&mut self) {
106        self.fail_storage();
107    }
108}
109
110impl<const N: usize> core::fmt::Debug for SecretArrayFrame<N> {
111    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
112        formatter
113            .debug_struct("SecretArrayFrame")
114            .field("storage", &"<redacted>")
115            .field("state", &self.state)
116            .finish_non_exhaustive()
117    }
118}
119
120/// Borrowed frame over caller-provided private staging and final output.
121pub struct SecretFrame<'a> {
122    state: SecretDecoderState,
123    staging: &'a mut [u8],
124    output: Option<&'a mut [u8]>,
125}
126
127impl<'a> SecretFrame<'a> {
128    /// Binds disjoint staging and final output before decoding begins.
129    pub fn new<S: Codec>(
130        codec: &Base64<S>,
131        maximum_decoded_len: usize,
132        staging: &'a mut [u8],
133        output: &'a mut [u8],
134    ) -> Result<Self, SecretDecodeError> {
135        let state = SecretDecoderState::new(codec.settings(), maximum_decoded_len)?;
136        require_capacity(maximum_decoded_len, staging.len())?;
137        require_capacity(maximum_decoded_len, output.len())?;
138        require_disjoint(staging, output)?;
139        crate::wipe_bytes(staging);
140        crate::wipe_bytes(output);
141        Ok(Self {
142            state,
143            staging,
144            output: Some(output),
145        })
146    }
147
148    /// Consumes one classified chunk without writing final output.
149    pub fn update(&mut self, input: &SecretInput<'_>) -> Result<Progress, SecretDecodeError> {
150        if let Err(error) = self.check_input(input.classified_bytes()) {
151            self.state.latch_external_failure();
152            self.fail_storage();
153            return Err(error);
154        }
155        match self.state.update(input.classified_bytes(), self.staging) {
156            Ok(progress) => Ok(progress),
157            Err(error) => {
158                self.fail_storage();
159                Err(error)
160            }
161        }
162    }
163
164    /// Applies the result gate and returns a wiping borrowed output guard.
165    pub fn finish(mut self) -> Result<SecretOutput<'a>, SecretDecodeError> {
166        let final_candidate = match self.state.finish() {
167            Ok(candidate) => candidate,
168            Err(error) => {
169                self.fail_storage();
170                return Err(error);
171            }
172        };
173        let Some(output) = self.output.take() else {
174            self.fail_storage();
175            return Err(SecretDecodeError::Failed);
176        };
177        let available = output.len();
178        release(self.staging, &final_candidate, output);
179        crate::wipe_bytes(self.staging);
180        SecretOutput::from_initialized(output, final_candidate.written()).map_err(|_| {
181            SecretDecodeError::OutputFull {
182                required: final_candidate.written(),
183                available,
184            }
185        })
186    }
187
188    /// Returns public decoder metadata without exposing staged bytes.
189    #[must_use]
190    pub const fn state(&self) -> &SecretDecoderState {
191        &self.state
192    }
193
194    fn check_input(&self, input: &[u8]) -> Result<(), SecretDecodeError> {
195        require_disjoint(input, self.staging)?;
196        require_disjoint(input, self.output.as_deref().unwrap_or(&[]))
197    }
198
199    fn fail_storage(&mut self) {
200        crate::wipe_bytes(self.staging);
201        if let Some(output) = self.output.as_deref_mut() {
202            crate::wipe_bytes(output);
203        }
204    }
205}
206
207impl Drop for SecretFrame<'_> {
208    fn drop(&mut self) {
209        self.fail_storage();
210    }
211}
212
213impl core::fmt::Debug for SecretFrame<'_> {
214    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
215        formatter
216            .debug_struct("SecretFrame")
217            .field("storage", &"<redacted>")
218            .field("state", &self.state)
219            .finish_non_exhaustive()
220    }
221}
222
223/// Heap frame whose staging and final allocation are reserved before update.
224#[cfg(feature = "alloc")]
225pub struct SecretVecFrame {
226    state: SecretDecoderState,
227    staging: alloc::vec::Vec<u8>,
228    output: alloc::vec::Vec<u8>,
229}
230
231#[cfg(feature = "alloc")]
232impl SecretVecFrame {
233    /// Preallocates both bounded ranges before plaintext can materialize.
234    pub fn new<S: Codec>(
235        codec: &Base64<S>,
236        maximum_decoded_len: usize,
237    ) -> Result<Self, SecretDecodeError> {
238        let state = SecretDecoderState::new(codec.settings(), maximum_decoded_len)?;
239        Ok(Self {
240            state,
241            staging: allocate_zeroed(maximum_decoded_len)?,
242            output: allocate_zeroed(maximum_decoded_len)?,
243        })
244    }
245
246    /// Consumes one classified chunk without releasing plaintext.
247    pub fn update(&mut self, input: &SecretInput<'_>) -> Result<Progress, SecretDecodeError> {
248        if let Err(error) = require_disjoint(input.classified_bytes(), &self.staging)
249            .and_then(|()| require_disjoint(input.classified_bytes(), &self.output))
250        {
251            self.state.latch_external_failure();
252            self.fail_storage();
253            return Err(error);
254        }
255        match self
256            .state
257            .update(input.classified_bytes(), &mut self.staging)
258        {
259            Ok(progress) => Ok(progress),
260            Err(error) => {
261                self.fail_storage();
262                Err(error)
263            }
264        }
265    }
266
267    /// Applies the result gate and returns bounded secret heap storage.
268    pub fn finish(mut self) -> Result<SecretVec, SecretDecodeError> {
269        let final_candidate = match self.state.finish() {
270            Ok(candidate) => candidate,
271            Err(error) => {
272                self.fail_storage();
273                return Err(error);
274            }
275        };
276        release(&self.staging, &final_candidate, &mut self.output);
277        crate::wipe_bytes(&mut self.staging);
278        let output = core::mem::take(&mut self.output);
279        Ok(SecretVec::from_frame(output, final_candidate.written()))
280    }
281
282    /// Returns public decoder metadata without exposing staged bytes.
283    #[must_use]
284    pub const fn state(&self) -> &SecretDecoderState {
285        &self.state
286    }
287
288    #[cfg(test)]
289    pub(crate) fn allocation_snapshot(&self) -> ((*const u8, usize), (*const u8, usize)) {
290        (
291            (self.staging.as_ptr(), self.staging.capacity()),
292            (self.output.as_ptr(), self.output.capacity()),
293        )
294    }
295
296    fn fail_storage(&mut self) {
297        crate::wipe_bytes(&mut self.staging);
298        crate::wipe_bytes(&mut self.output);
299    }
300}
301
302#[cfg(feature = "alloc")]
303impl Drop for SecretVecFrame {
304    fn drop(&mut self) {
305        self.fail_storage();
306    }
307}
308
309#[cfg(feature = "alloc")]
310impl core::fmt::Debug for SecretVecFrame {
311    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
312        formatter
313            .debug_struct("SecretVecFrame")
314            .field("storage", &"<redacted>")
315            .field("state", &self.state)
316            .finish_non_exhaustive()
317    }
318}
319
320fn require_capacity(required: usize, available: usize) -> Result<(), SecretDecodeError> {
321    if required > available {
322        Err(SecretDecodeError::OutputFull {
323            required,
324            available,
325        })
326    } else {
327        Ok(())
328    }
329}
330
331fn release(staging: &[u8], final_candidate: &FinalCandidate, output: &mut [u8]) {
332    let staged_len = final_candidate.staged_len;
333    output[..staged_len].copy_from_slice(&staging[..staged_len]);
334    output[staged_len..final_candidate.written()]
335        .copy_from_slice(&final_candidate.bytes[..final_candidate.len]);
336    crate::wipe_tail(output, final_candidate.written());
337}
338
339#[cfg(feature = "alloc")]
340fn allocate_zeroed(capacity: usize) -> Result<alloc::vec::Vec<u8>, SecretDecodeError> {
341    let mut bytes = alloc::vec::Vec::new();
342    bytes
343        .try_reserve_exact(capacity)
344        .map_err(|_| SecretDecodeError::AllocationFailed)?;
345    bytes.resize(capacity, 0);
346    Ok(bytes)
347}
348
349/// Constructs a stack-backed secret frame while enforcing its capacity limit.
350#[macro_export]
351macro_rules! secret_array_frame {
352    ($codec:expr, $capacity:expr) => {{ $crate::secret::SecretArrayFrame::<$capacity>::new(&$codec) }};
353}