Skip to main content

structured_cms/personal/
process.rs

1//! Scoped import orchestration with explicit transfer of decrypted key storage.
2
3use super::{
4    Bag, BagValue, DATA, FIELD, Integrity, Pfx, PrivateKey, Safe, ShroudedKey, encoding, end,
5    equal_oid, invalid, sequence,
6};
7use crate::{Error, ErrorCode, Octets, Result, content_source::SourceMap, signer::required};
8use structured_asn1::memory::{OwnedVec, Work};
9use structured_asn1::{Encoding, Reader, Span};
10use zeroize::Zeroize;
11
12/// Accounted plaintext which wipes initialized storage before deallocation.
13/// It owns no backend and never implements Debug/Clone. Growth and copies must
14/// use the supplied current Reader execution before wrapping the resulting owner.
15pub struct Plaintext(OwnedVec<u8>);
16impl Plaintext {
17    /// Copies bytes into current accounted storage after funding the eventual
18    /// wipe. Copy/refusal paths keep a wiping guard from the first stored byte.
19    pub fn copy_from(bytes: &[u8], query: &Reader<'_>) -> Result<Self> {
20        let execution = query.execution();
21        execution
22            .charge_work(bytes.len() as u64)
23            .map_err(|e| Error::resource(e, FIELD, query.offset()))?;
24        let mut output = Wiping(Some(execution.vector()));
25        output
26            .bytes()
27            .try_extend_from_slice(bytes)
28            .map_err(|e| Error::resource(e, FIELD, query.offset()))?;
29        Ok(output.into_owned())
30    }
31    /// Copies exact logical octets once for in-place decryption. Metadata,
32    /// capacity, copies and the eventual wipe are paid by the producing query.
33    pub fn copy_octets(octets: &Octets<'_>, query: &Reader<'_>) -> Result<Self> {
34        if !core::ptr::eq(octets.parser(), query.parser()) {
35            return Err(Error::pkix(structured_pkix::Error::new(
36                structured_pkix::ErrorCode::ForeignReader,
37                FIELD,
38                octets.span().start,
39            )));
40        }
41        let execution = query.execution();
42        let fail = |e| Error::resource(e, FIELD, octets.span().start);
43        execution.charge_work(octets.len() as u64).map_err(fail)?;
44        execution
45            .charge_work(octets.pieces().len() as u64)
46            .map_err(fail)?;
47        let mut output = Wiping(Some(execution.vector()));
48        output
49            .bytes()
50            .try_reserve_exact(octets.len())
51            .map_err(fail)?;
52        for piece in octets.pieces() {
53            output
54                .bytes()
55                .try_extend_from_slice(piece.as_bytes())
56                .map_err(fail)?;
57        }
58        Ok(output.into_owned())
59    }
60    /// Immutable plaintext; no allocating conversion is performed.
61    #[must_use]
62    pub fn as_bytes(&self) -> &[u8] {
63        &self.0
64    }
65    /// Mutable existing bytes for in-place cipher operations, without growth.
66    pub fn as_mut_bytes(&mut self) -> &mut [u8] {
67        &mut self.0
68    }
69    /// Wipes and discards padding/trailing initialized bytes, retaining the
70    /// original allocation/account. A length beyond the current length is a
71    /// no-op; this cannot grow or reveal spare capacity.
72    pub fn truncate(&mut self, length: usize) {
73        if length < self.0.len() {
74            self.0[length..].zeroize();
75            while self.0.len() > length {
76                self.0.pop();
77            }
78        }
79    }
80}
81
82/// Encode a legacy PKCS#12 password as big-endian BMP characters plus a single
83/// zero terminator (RFC7292 Appendix B.1). Supplementary Unicode scalars are
84/// refused, never encoded as surrogate pairs. PBMAC1 (RFC9879 ยง6) and PBES2
85/// use the original UTF-8 bytes instead; callers must not apply this conversion
86/// merely because a different protection layer in the same PFX uses legacy PBE.
87/// <https://www.rfc-editor.org/rfc/rfc7292#appendix-B.1>
88pub fn legacy_password(password: &str, query: &Reader<'_>) -> Result<Plaintext> {
89    let execution = query.execution();
90    let fail = |e| Error::resource(e, FIELD, query.offset());
91    // Both Unicode passes are funded before the first scan.
92    execution.charge_work(password.len() as u64).map_err(fail)?;
93    execution.charge_work(password.len() as u64).map_err(fail)?;
94    let mut count = 1_usize;
95    for ch in password.chars() {
96        if u32::from(ch) > u32::from(u16::MAX) {
97            return Err(Error::schema(
98                ErrorCode::MalformedValue,
99                FIELD,
100                query.offset(),
101            ));
102        }
103        count = count
104            .checked_add(1)
105            .ok_or_else(|| Error::schema(ErrorCode::LengthOverflow, FIELD, query.offset()))?;
106    }
107    let size = count
108        .checked_mul(2)
109        .ok_or_else(|| Error::schema(ErrorCode::LengthOverflow, FIELD, query.offset()))?;
110    execution.charge_work(size as u64).map_err(fail)?;
111    let mut output = Wiping(Some(execution.vector()));
112    output.bytes().try_reserve_exact(size).map_err(fail)?;
113    for ch in password.chars() {
114        debug_assert!(u16::try_from(u32::from(ch)).is_ok());
115        let encoded = u32::from(ch).to_be_bytes();
116        output
117            .bytes()
118            .try_extend_from_slice(&encoded[2..])
119            .map_err(fail)?;
120    }
121    output
122        .bytes()
123        .try_extend_from_slice(&[0, 0])
124        .map_err(fail)?;
125    Ok(output.into_owned())
126}
127impl Drop for Plaintext {
128    fn drop(&mut self) {
129        self.0.zeroize();
130    }
131}
132struct Wiping<W: Work>(Option<OwnedVec<u8, W>>);
133impl<W: Work> Wiping<W> {
134    fn bytes(&mut self) -> &mut OwnedVec<u8, W> {
135        self.0
136            .as_mut()
137            .expect("buffer exists until consuming transfer")
138    }
139    fn into_owned(mut self) -> Plaintext {
140        Plaintext(self.0.take().expect("consuming transfer once").into_owned())
141    }
142}
143impl<W: Work> Drop for Wiping<W> {
144    fn drop(&mut self) {
145        if let Some(bytes) = self.0.as_mut() {
146            bytes.zeroize();
147        }
148    }
149}
150
151/// A provider's explicit acceptance/capability result, not an application verdict.
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub enum Decision {
154    /// Authentication succeeded (or the caller explicitly accepted no MAC).
155    Accept,
156    /// The original integrity protection failed or is required but absent.
157    Reject,
158    /// The selected provider does not implement the written mechanism.
159    Unsupported,
160}
161
162/// Scoped coordinates for callback views. Joins map to their enclosing source;
163/// decrypted bytes have their own coordinate space, never invented physical
164/// offsets. A caller can retain/project known locations while the scope is live.
165pub enum Source<'a> {
166    /// Original PFX document coordinates.
167    Document,
168    /// Logical carried bytes with their exact original fragment mapping.
169    Carried {
170        /// Mapping into the parent source coordinates.
171        map: &'a Fragments,
172        /// The source enclosing those fragments.
173        parent: &'a Source<'a>,
174    },
175    /// New plaintext coordinates starting at zero, without a bytewise map to
176    /// ciphertext (encryption is not a source-offset-preserving transformation).
177    Plaintext {
178        /// Ciphertext enclosure in the parent's coordinates.
179        ciphertext: Span,
180        /// Original source of that encrypted container.
181        parent: &'a Source<'a>,
182    },
183}
184/// One independently retained source transformation, innermost first.
185#[derive(Debug)]
186pub enum Origin {
187    /// Map into the next enclosing source, retained without plaintext bytes.
188    Carried(Fragments),
189    /// Offsets name a decrypted source; only its ciphertext enclosure is physical
190    /// in the next layer. This is not a contiguous plaintext-to-ciphertext map.
191    Plaintext(Span),
192}
193
194/// A carried-byte map into its immediate enclosing source. Unlike a standalone
195/// document map, its parent can itself be carried or decrypted; the [`Source`]
196/// / [`Failure::origins`] chain identifies that domain explicitly. No method
197/// claims that a parent-relative offset is already a physical PFX coordinate.
198#[derive(Debug)]
199pub struct Fragments(SourceMap);
200impl Fragments {
201    /// Parent-relative enclosure of the complete carrying OCTET STRING TLV.
202    #[must_use]
203    pub fn enclosure(&self) -> Span {
204        self.0.carrier()
205    }
206    /// Maps one current source point into the immediately enclosing source.
207    /// Repeat through the retained chain until Document, or stop at a Plaintext
208    /// boundary, where only the ciphertext enclosure can be mapped further.
209    pub fn offset_in_parent(&self, at: u64, work: &impl Work) -> Result<u64> {
210        self.0.project_offset(at, work)
211    }
212    /// Maps a current range into its actual parent fragments, without claiming
213    /// a single contiguous physical range across headers or encryption.
214    pub fn ranges_in_parent(&self, span: Span, work: &impl Work) -> Result<OwnedVec<Span>> {
215        self.0.project_span(span, work)
216    }
217    /// Retains just this layer's metadata for a caller-owned diagnostic/result.
218    pub fn try_copy_in(&self, work: &impl Work) -> Result<Self> {
219        self.0.try_copy_in(work).map(Self)
220    }
221}
222/// Terminal failure, preserving the exact cause and its coordinate chain.
223/// At most two carried wrappers and two decryptions occur in this pipeline;
224/// nested `SafeContents` are walked in place and add no transformation layer.
225#[derive(Debug)]
226pub struct Failure<E> {
227    /// Processing or callback cause, without loss of its original type.
228    pub cause: Cause<E>,
229    /// Retained source transformations, innermost to outermost. Empty means
230    /// the cause refers directly to the original PFX document.
231    pub origins: [Option<Origin>; 4],
232}
233/// Processing failures are distinct from provider/consumer callback refusal.
234#[derive(Debug)]
235pub enum Cause<E> {
236    /// A source/parameter/ownership/resource processing failure.
237    Processing(Error),
238    /// The unchanged callback error, with no conversion to invalid/mismatch.
239    Callback(E),
240}
241impl<E> Failure<E> {
242    fn processing(error: Error) -> Self {
243        Self {
244            cause: Cause::Processing(error),
245            origins: core::array::from_fn(|_| None),
246        }
247    }
248    fn callback(error: E) -> Self {
249        Self {
250            cause: Cause::Callback(error),
251            origins: core::array::from_fn(|_| None),
252        }
253    }
254    fn enclosed(mut self, origin: Origin) -> Self {
255        let slot = self
256            .origins
257            .iter_mut()
258            .find(|item| item.is_none())
259            .expect("four pipeline transformations maximum");
260        *slot = Some(origin);
261        self
262    }
263}
264
265/// A selected backend and extraction visitor. It must fund all backend work,
266/// temporary/output capacity and key/password handling through the supplied
267/// Reader before work. There is no default implementation or implicit engine.
268pub trait Backend {
269    /// Owned error type, independent of temporary decrypted input lifetimes.
270    type Error;
271    /// Check the entire original PFX integrity. For Password mode, MAC input is
272    /// exactly `safe` logical octets. Signed mode uses the existing CMS verifier
273    /// over the actual carried data. A missing MAC must be explicitly accepted
274    /// or rejected here; it never gains an authentication proof from parsing.
275    fn authenticate(
276        &mut self,
277        integrity: &Integrity<'_>,
278        query: &Reader<'_>,
279    ) -> core::result::Result<Decision, Self::Error>;
280    /// Decrypt one complete password-protected or enveloped safe. The model
281    /// retains the original algorithms/recipients; `Safe::Data` is never passed.
282    /// Return a wiping buffer on this current account. A failure terminates
283    /// processing; ciphertext is not interpreted as plaintext on refusal.
284    fn decrypt_safe(
285        &mut self,
286        safe: &Safe<'_>,
287        query: &Reader<'_>,
288        source: &Source<'_>,
289    ) -> core::result::Result<Plaintext, Self::Error>;
290    /// Decrypt one original PKCS#8 shrouded key, including a key inside an
291    /// already encrypted safe. Its KDF is a separate paid operation.
292    fn decrypt_key(
293        &mut self,
294        key: &ShroudedKey<'_>,
295        query: &Reader<'_>,
296        source: &Source<'_>,
297    ) -> core::result::Result<Plaintext, Self::Error>;
298    /// Consume each original bag once in direct sibling/preorder nesting order.
299    /// The parsed bag/source are scoped; owned extraction must fund its own
300    /// retained storage. Unknown bags remain explicit and are not discarded.
301    fn bag(
302        &mut self,
303        bag: &Bag<'_>,
304        query: &Reader<'_>,
305        source: &Source<'_>,
306    ) -> core::result::Result<(), Self::Error>;
307    /// Consume the clear key obtained from a shrouded bag, under its new honest
308    /// plaintext coordinates. The enclosing bag is not replayed to `bag`.
309    /// On success, `retain_key` receives this exact buffer after these scoped
310    /// views end. Inspect/retain metadata here without copying the key bytes.
311    fn key(
312        &mut self,
313        key: &PrivateKey<'_>,
314        query: &Reader<'_>,
315        source: &Source<'_>,
316    ) -> core::result::Result<(), Self::Error>;
317    /// Take the original unpadded key buffer after successful `key` inspection.
318    /// Moving it retains its existing capacity charge and prepaid wiping;
319    /// dropping it releases storage. No temporary Reader/model survives here.
320    /// A refusal terminates processing with the same plaintext source chain.
321    fn retain_key(&mut self, plaintext: Plaintext) -> core::result::Result<(), Self::Error>;
322}
323
324/// Authenticate, then process every original safe and nested bag once.
325///
326/// Reject/Unsupported returns before any privacy or extraction callback. A
327/// processing/callback error returns no successful completion; external visitor
328/// effects cannot be rolled back by this library and belong in caller staging.
329/// The backend's `Accept` is a trusted callback fact, not a core trust verdict.
330#[expect(
331    clippy::result_large_err,
332    reason = "retain opaque cause and four source maps without allocating on the refusal path"
333)]
334pub fn process<B: Backend>(
335    pfx: &Pfx<'_>,
336    query: &Reader<'_>,
337    backend: &mut B,
338) -> core::result::Result<Decision, Failure<B::Error>> {
339    let original = pfx
340        .original()
341        .reader_in(query)
342        .map_err(Failure::processing)?;
343    original
344        .charge(1)
345        .map_err(encoding)
346        .map_err(Failure::processing)?;
347    let decision = backend
348        .authenticate(&pfx.integrity, &original)
349        .map_err(Failure::callback)?;
350    if decision != Decision::Accept {
351        return Ok(decision);
352    }
353    nested(
354        pfx.safe(),
355        &original,
356        &Source::Document,
357        |reader, source| {
358            let root = required(reader, FIELD).map_err(Failure::processing)?;
359            let mut safes = sequence(reader, &root).map_err(Failure::processing)?;
360            end(reader).map_err(Failure::processing)?;
361            while let Some(element) = safes
362                .next_element()
363                .map_err(encoding)
364                .map_err(Failure::processing)?
365            {
366                let safe = Safe::read(&safes, &element).map_err(Failure::processing)?;
367                safes
368                    .charge(1)
369                    .map_err(encoding)
370                    .map_err(Failure::processing)?;
371                match &safe {
372                    Safe::Data(bytes) => nested(bytes, &safes, source, |reader, source| {
373                        bags(reader, source, backend)
374                    })?,
375                    Safe::Encrypted(encrypted) => {
376                        check_data(&safes, encrypted.content.kind, element.span())
377                            .map_err(Failure::processing)?;
378                        let ciphertext =
379                            encrypted.content.ciphertext.as_ref().ok_or_else(|| {
380                                Failure::processing(invalid(ErrorCode::DetachedContent, &element))
381                            })?;
382                        let plain = backend
383                            .decrypt_safe(&safe, &safes, source)
384                            .map_err(Failure::callback)?;
385                        decrypted(&plain, &safes, ciphertext, source, |reader, source| {
386                            bags(reader, source, backend)
387                        })?;
388                    }
389                    Safe::Enveloped(enveloped) => {
390                        check_data(&safes, enveloped.content.kind, element.span())
391                            .map_err(Failure::processing)?;
392                        let ciphertext =
393                            enveloped.content.ciphertext.as_ref().ok_or_else(|| {
394                                Failure::processing(invalid(ErrorCode::DetachedContent, &element))
395                            })?;
396                        let plain = backend
397                            .decrypt_safe(&safe, &safes, source)
398                            .map_err(Failure::callback)?;
399                        decrypted(&plain, &safes, ciphertext, source, |reader, source| {
400                            bags(reader, source, backend)
401                        })?;
402                    }
403                    Safe::Other { .. } => {
404                        return Err(Failure::processing(invalid(
405                            ErrorCode::UnexpectedContentType,
406                            &element,
407                        )));
408                    }
409                }
410            }
411            Ok(())
412        },
413    )?;
414    Ok(decision)
415}
416
417fn check_data(parent: &Reader<'_>, kind: structured_asn1::Oid<'_>, span: Span) -> Result<()> {
418    if !equal_oid(parent, kind, DATA)? {
419        return Err(Error::schema(
420            ErrorCode::UnexpectedContentType,
421            FIELD,
422            span.start,
423        ));
424    }
425    Ok(())
426}
427
428#[expect(
429    clippy::result_large_err,
430    reason = "owned source maps preserve error provenance without refusal-path allocation"
431)]
432fn bags<B: Backend>(
433    parent: &mut Reader<'_>,
434    source: &Source<'_>,
435    backend: &mut B,
436) -> core::result::Result<(), Failure<B::Error>> {
437    let root = required(parent, FIELD).map_err(Failure::processing)?;
438    let reader = sequence(parent, &root).map_err(Failure::processing)?;
439    end(parent).map_err(Failure::processing)?;
440    let execution = parent.execution();
441    let mut frames = execution.vector();
442    frames
443        .try_push(reader)
444        .map_err(|e| Failure::processing(Error::resource(e, FIELD, root.span().start)))?;
445    while let Some(frame) = frames.last_mut() {
446        frame
447            .charge(1)
448            .map_err(encoding)
449            .map_err(Failure::processing)?;
450        let Some(element) = frame
451            .next_element()
452            .map_err(encoding)
453            .map_err(Failure::processing)?
454        else {
455            frames.pop();
456            continue;
457        };
458        let bag = Bag::read(frame, &element).map_err(Failure::processing)?;
459        backend
460            .bag(&bag, frame, source)
461            .map_err(Failure::callback)?;
462        if let BagValue::Contents(value) = &bag.value {
463            // The original value sits inside SafeBag.[0], two levels below
464            // this loop cursor. Reopen its saved context before entering it.
465            let original = value.reader_in(frame).map_err(Failure::processing)?;
466            let reader = sequence(&original, value.element()).map_err(Failure::processing)?;
467            frames.try_push(reader).map_err(|e| {
468                Failure::processing(Error::resource(e, FIELD, element.span().start))
469            })?;
470        } else if let BagValue::Shrouded(key) = &bag.value {
471            let plain = backend
472                .decrypt_key(key, frame, source)
473                .map_err(Failure::callback)?;
474            decrypted(&plain, frame, &key.encrypted, source, |reader, source| {
475                let value = required(reader, FIELD).map_err(Failure::processing)?;
476                let key = PrivateKey::read(reader, &value).map_err(Failure::processing)?;
477                end(reader).map_err(Failure::processing)?;
478                reader
479                    .charge(1)
480                    .map_err(encoding)
481                    .map_err(Failure::processing)?;
482                backend.key(&key, reader, source).map_err(Failure::callback)
483            })?;
484            backend.retain_key(plain).map_err(|error| {
485                Failure::callback(error).enclosed(Origin::Plaintext(key.encrypted.span()))
486            })?;
487        }
488    }
489    Ok(())
490}
491
492#[expect(
493    clippy::result_large_err,
494    reason = "preserve independently owned source chain with the original callback cause"
495)]
496fn nested<R, E>(
497    octets: &Octets<'_>,
498    parent: &Reader<'_>,
499    origin: &Source<'_>,
500    callback: impl for<'scope> FnOnce(
501        &mut Reader<'scope>,
502        &Source<'_>,
503    ) -> core::result::Result<R, Failure<E>>,
504) -> core::result::Result<R, Failure<E>> {
505    if !core::ptr::eq(octets.parser(), parent.parser()) {
506        return Err(Failure::processing(Error::pkix(
507            structured_pkix::Error::new(
508                structured_pkix::ErrorCode::ForeignReader,
509                FIELD,
510                octets.span().start,
511            ),
512        )));
513    }
514    let map = Fragments(SourceMap::of(octets, parent, FIELD).map_err(Failure::processing)?);
515    let joined;
516    let (bytes, base) = if let [piece] = octets.pieces() {
517        (piece.as_bytes(), piece.span().start)
518    } else {
519        joined = Plaintext::copy_octets(octets, parent).map_err(Failure::processing)?;
520        (&*joined.0, 0)
521    };
522    let reader = parent.parser().read_nested_query_in(
523        parent,
524        bytes,
525        base,
526        Encoding::Ber,
527        octets.outer_depth(),
528    );
529    let result = match reader {
530        Ok(mut reader) => callback(
531            &mut reader,
532            &Source::Carried {
533                map: &map,
534                parent: origin,
535            },
536        ),
537        Err(error) => Err(Failure::processing(encoding(error))),
538    };
539    result.map_err(|e| e.enclosed(Origin::Carried(map)))
540}
541
542#[expect(
543    clippy::result_large_err,
544    reason = "preserve source/opaque cause without another allocation on refusal"
545)]
546fn decrypted<R, E>(
547    plain: &Plaintext,
548    parent: &Reader<'_>,
549    encrypted: &Octets<'_>,
550    origin: &Source<'_>,
551    callback: impl for<'scope> FnOnce(
552        &mut Reader<'scope>,
553        &Source<'_>,
554    ) -> core::result::Result<R, Failure<E>>,
555) -> core::result::Result<R, Failure<E>> {
556    let ciphertext = encrypted.span();
557    if !plain.0.memory().same_account(parent.memory()) {
558        return Err(Failure::processing(Error::schema(
559            ErrorCode::InvalidLocation,
560            FIELD,
561            ciphertext.start,
562        )));
563    }
564    let reader = parent.parser().read_nested_query_in(
565        parent,
566        &plain.0,
567        0,
568        Encoding::Ber,
569        encrypted.outer_depth(),
570    );
571    let result = match reader {
572        Ok(mut reader) => callback(
573            &mut reader,
574            &Source::Plaintext {
575                ciphertext,
576                parent: origin,
577            },
578        ),
579        Err(error) => Err(Failure::processing(encoding(error))),
580    };
581    result.map_err(|e| e.enclosed(Origin::Plaintext(ciphertext)))
582}