Skip to main content

base64_ng/v2/assurance/
protected.rs

1//! Allocation-specific protected typestate owner and ordered teardown.
2
3use core::{cell::UnsafeCell, marker::PhantomData};
4
5use super::{
6    AccountingPosture, AllocationPresence, AssuranceContext, AssuranceGenerations, AssuranceLevel,
7    AssuranceToken, AttestationEvidence, CleanupError, CleanupOutcome, CleanupReport,
8    DisposalResult, LifecyclePosture, PendingStage, PhysicalProtection, ProtectedMemoryProvider,
9    ProtectedOperationReport, ProtectionError, ProtectionRequest, ProviderAccess, ProviderHealth,
10    ProviderOperationResult, QuarantineRecord, SecretOperation, TeardownCursor, TeardownOperation,
11    ThreadMovableProvider, WipeEvidence,
12};
13
14/// Protected allocation has not yet accepted plaintext.
15pub struct Uninitialized {
16    _private: (),
17}
18
19/// Protected allocation contains plaintext not yet released by a result gate.
20pub struct Unvalidated {
21    _private: (),
22}
23
24/// Protected allocation passed its assured operation and result gate.
25pub struct Validated {
26    _private: (),
27}
28
29/// Exact provider-owned protected allocation in one typestate.
30///
31/// Moving this handle may not move the provider's backing bytes. The type is
32/// `!Sync`, `!UnwindSafe`, and `!RefUnwindSafe`. Those traits communicate API
33/// friction only; cleanup remains correct when callers use `AssertUnwindSafe`.
34pub struct ProtectedSecret<'provider, P, State, Level>
35where
36    P: ProtectedMemoryProvider,
37    Level: AssuranceLevel,
38{
39    provider: &'provider P,
40    handle: Option<P::Handle>,
41    context: &'provider AssuranceContext,
42    generations: AssuranceGenerations,
43    attestation: Option<AttestationEvidence>,
44    provider_generation: usize,
45    health_generation: usize,
46    protection_generation: usize,
47    initialized_len: usize,
48    operation: SecretOperation,
49    _state: PhantomData<State>,
50    _level: PhantomData<Level>,
51    _not_sync_or_unwind_safe: PhantomData<(UnsafeCell<()>, &'provider mut dyn FnMut())>,
52}
53
54impl<'provider, P, Level> ProtectedSecret<'provider, P, Uninitialized, Level>
55where
56    P: ProtectedMemoryProvider,
57    Level: AssuranceLevel,
58{
59    /// Reserves all bounded resources and creates zeroed protected storage.
60    pub fn try_new(
61        provider: &'provider P,
62        token: &AssuranceToken<'provider, Level>,
63        logical_bytes: usize,
64    ) -> Result<Self, ProtectionError> {
65        token
66            .revalidate()
67            .map_err(|_| ProtectionError::StaleAssurance)?;
68        if provider.health() != ProviderHealth::Healthy {
69            return Err(ProtectionError::ProviderUnavailable);
70        }
71        if let Some(evidence) = token.evidence()
72            && (evidence.provider_identity() != provider.provider_identity()
73                || evidence.provider_generation() != provider.provider_generation())
74        {
75            return Err(ProtectionError::StaleAssurance);
76        }
77        let request = ProtectionRequest::new(
78            logical_bytes,
79            provider.limits().page_size,
80            AssuranceToken::<Level>::requires_attestation(),
81        )?;
82        let access = ProviderAccess::new();
83        let reservation = provider.reserve(&access, request)?;
84        let handle = provider.materialize(&access, reservation)?;
85        if provider.logical_len(&access, &handle) != logical_bytes {
86            let mut owner = Self::from_handle(provider, token, handle);
87            let _ = owner.close_inner();
88            return Err(ProtectionError::ActualRangeExceededReservation);
89        }
90        if AssuranceToken::<Level>::requires_attestation()
91            && provider.physical_protection(&access, &handle)
92                != PhysicalProtection::ProtectionAttested
93        {
94            let mut owner = Self::from_handle(provider, token, handle);
95            let _ = owner.close_inner();
96            return Err(ProtectionError::ProtectionUnavailable);
97        }
98        Ok(Self::from_handle(provider, token, handle))
99    }
100
101    fn from_handle(
102        provider: &'provider P,
103        token: &AssuranceToken<'provider, Level>,
104        handle: P::Handle,
105    ) -> Self {
106        Self {
107            provider,
108            handle: Some(handle),
109            context: token.context(),
110            generations: token.generations(),
111            attestation: token.evidence(),
112            provider_generation: provider.provider_generation(),
113            health_generation: provider.health_generation(),
114            protection_generation: provider.protection_generation(),
115            initialized_len: 0,
116            operation: SecretOperation::NotStarted,
117            _state: PhantomData,
118            _level: PhantomData,
119            _not_sync_or_unwind_safe: PhantomData,
120        }
121    }
122
123    pub(crate) fn begin_unvalidated(
124        mut self,
125        token: &AssuranceToken<'provider, Level>,
126        operation: SecretOperation,
127    ) -> Result<ProtectedSecret<'provider, P, Unvalidated, Level>, (ProtectionError, Self)> {
128        if let Err(error) = self.revalidate(token) {
129            return Err((error, self));
130        }
131        self.operation = operation;
132        match self.transition() {
133            Ok(next) => Ok(next),
134            Err(error) => Err((error, self)),
135        }
136    }
137}
138
139impl<'provider, P, Level> ProtectedSecret<'provider, P, Unvalidated, Level>
140where
141    P: ProtectedMemoryProvider,
142    Level: AssuranceLevel,
143{
144    pub(crate) fn bytes_mut(&mut self) -> Result<&mut [u8], ProtectionError> {
145        let handle = self
146            .handle
147            .as_mut()
148            .ok_or(ProtectionError::ProviderUnavailable)?;
149        Ok(self.provider.bytes_mut(&ProviderAccess::new(), handle))
150    }
151
152    pub(crate) fn set_initialized_len(&mut self, len: usize) -> Result<(), ProtectionError> {
153        if len > self.capacity() {
154            return Err(ProtectionError::ActualRangeExceededReservation);
155        }
156        self.initialized_len = len;
157        Ok(())
158    }
159
160    pub(crate) fn validate(
161        mut self,
162        token: &AssuranceToken<'provider, Level>,
163    ) -> Result<ProtectedSecret<'provider, P, Validated, Level>, (ProtectionError, Self)> {
164        if let Err(error) = self.revalidate(token) {
165            return Err((error, self));
166        }
167        match self.transition() {
168            Ok(next) => Ok(next),
169            Err(error) => Err((error, self)),
170        }
171    }
172}
173
174impl<P, Level> ProtectedSecret<'_, P, Validated, Level>
175where
176    P: ProtectedMemoryProvider,
177    Level: AssuranceLevel,
178{
179    /// Creates an explicit redacted exposure guard over validated bytes.
180    #[must_use]
181    pub fn expose_secret(&self) -> ExposedProtectedSecret<'_> {
182        let bytes = self.handle.as_ref().map_or(&[][..], |handle| {
183            self.provider.bytes(&ProviderAccess::new(), handle)
184        });
185        ExposedProtectedSecret {
186            bytes: &bytes[..self.initialized_len],
187        }
188    }
189}
190
191impl<'provider, P, State, Level> ProtectedSecret<'provider, P, State, Level>
192where
193    P: ProtectedMemoryProvider,
194    Level: AssuranceLevel,
195{
196    /// Complete logical allocation capacity.
197    #[must_use]
198    pub fn capacity(&self) -> usize {
199        self.handle.as_ref().map_or(0, |handle| {
200            self.provider.logical_len(&ProviderAccess::new(), handle)
201        })
202    }
203
204    /// Initialized plaintext bytes tracked by this typestate.
205    #[must_use]
206    pub const fn initialized_len(&self) -> usize {
207        self.initialized_len
208    }
209
210    /// Reports posture only for this exact allocation and its recorded
211    /// operation. Stale tokens fail instead of returning historical assurance.
212    pub fn operation_report(
213        &self,
214        token: &AssuranceToken<'provider, Level>,
215    ) -> Result<ProtectedOperationReport, ProtectionError> {
216        self.revalidate(token)?;
217        let physical_protection =
218            self.handle
219                .as_ref()
220                .map_or(PhysicalProtection::ProtectionUnknown, |handle| {
221                    self.provider
222                        .physical_protection(&ProviderAccess::new(), handle)
223                });
224        Ok(ProtectedOperationReport::live(
225            token.report(),
226            self.operation,
227            physical_protection,
228            self.provider.health(),
229            self.provider.health_generation(),
230            self.provider.protection_generation(),
231        ))
232    }
233
234    /// Consumes the public owner and reports the ordered cleanup result.
235    pub fn try_close(mut self) -> Result<CleanupReport, CleanupError> {
236        self.close_inner()
237    }
238
239    fn transition<Next>(
240        &mut self,
241    ) -> Result<ProtectedSecret<'provider, P, Next, Level>, ProtectionError> {
242        let handle = self
243            .handle
244            .take()
245            .ok_or(ProtectionError::ProviderUnavailable)?;
246        Ok(ProtectedSecret {
247            provider: self.provider,
248            handle: Some(handle),
249            context: self.context,
250            generations: self.generations,
251            attestation: self.attestation,
252            provider_generation: self.provider_generation,
253            health_generation: self.health_generation,
254            protection_generation: self.protection_generation,
255            initialized_len: self.initialized_len,
256            operation: self.operation,
257            _state: PhantomData,
258            _level: PhantomData,
259            _not_sync_or_unwind_safe: PhantomData,
260        })
261    }
262
263    fn revalidate(&self, token: &AssuranceToken<'provider, Level>) -> Result<(), ProtectionError> {
264        token
265            .revalidate()
266            .and_then(|()| self.context.revalidate_snapshot::<Level>(self.generations))
267            .map_err(|_| ProtectionError::StaleAssurance)?;
268        if !core::ptr::eq(token.context(), self.context)
269            || token.generations() != self.generations
270            || self.provider.provider_generation() != self.provider_generation
271            || self.provider.health_generation() != self.health_generation
272            || self.provider.protection_generation() != self.protection_generation
273            || self.provider.health() != ProviderHealth::Healthy
274        {
275            return Err(ProtectionError::StaleAssurance);
276        }
277        Ok(())
278    }
279
280    fn close_inner(&mut self) -> Result<CleanupReport, CleanupError> {
281        let Some(mut handle) = self.handle.take() else {
282            return Ok(closed_report(WipeEvidence::WipedBestEffort));
283        };
284        let mut cursor = TeardownCursor::new();
285        cursor.begin(TeardownOperation::Wipe);
286        let access = ProviderAccess::new();
287        crate::wipe_bytes(self.provider.bytes_mut(&access, &mut handle));
288        let context_current = self
289            .context
290            .revalidate_wipe_snapshot::<Level>(self.generations)
291            .is_ok();
292        let attestation_current = self.attestation.is_some_and(|evidence| {
293            evidence.provider_identity() == self.provider.provider_identity()
294                && evidence.provider_generation() == self.provider_generation
295                && self.provider.provider_generation() == self.provider_generation
296                && self.provider.health_generation() == self.health_generation
297                && self.provider.protection_generation() == self.protection_generation
298                && self.provider.health() == ProviderHealth::Healthy
299        });
300        let confirmation =
301            self.provider
302                .confirm_wipe(&access, &handle, self.attestation, &mut cursor);
303        cursor.disposition = confirmation.result.journal_disposition();
304        let required_attested = AssuranceToken::<Level>::requires_attestation();
305        let wipe_sufficient = confirmation.result == ProviderOperationResult::Applied
306            && context_current
307            && (!required_attested
308                || (confirmation.evidence == WipeEvidence::WipedAttested && attestation_current));
309        if !wipe_sufficient {
310            let wipe = if confirmation.result == ProviderOperationResult::Applied {
311                if required_attested && (!context_current || !attestation_current) {
312                    WipeEvidence::WipedBestEffort
313                } else {
314                    confirmation.evidence
315                }
316            } else {
317                WipeEvidence::WipeNotCompleted
318            };
319            return Err(self.quarantine(handle, PendingStage::Wipe, wipe, cursor));
320        }
321        let wipe = confirmation.evidence;
322
323        cursor.begin(TeardownOperation::ProtectionRemoval);
324        let removal = self
325            .provider
326            .remove_protection(&access, &mut handle, &mut cursor);
327        cursor.disposition = removal.journal_disposition();
328        if removal != ProviderOperationResult::Applied {
329            let physical = if removal == ProviderOperationResult::Indeterminate {
330                PhysicalProtection::ProtectionUnknown
331            } else {
332                self.provider.physical_protection(&access, &handle)
333            };
334            return Err(self.quarantine_with_physical(
335                handle,
336                PendingStage::ProtectionRemoval,
337                wipe,
338                physical,
339                cursor,
340            ));
341        }
342
343        cursor.begin(TeardownOperation::AccountingReconciliation);
344        let accounting = self
345            .provider
346            .reconcile_accounting(&access, &mut handle, &mut cursor);
347        cursor.disposition = accounting.journal_disposition();
348        if accounting != ProviderOperationResult::Applied {
349            return Err(self.quarantine_with_physical(
350                handle,
351                PendingStage::AccountingReconciliation,
352                wipe,
353                PhysicalProtection::ProtectionConfirmedAbsent,
354                cursor,
355            ));
356        }
357
358        cursor.begin(TeardownOperation::Disposal);
359        match self.provider.dispose(&access, handle, &mut cursor) {
360            DisposalResult::Applied => Ok(closed_report(wipe)),
361            DisposalResult::NotApplied(handle) => Err(self.quarantine_with_physical(
362                handle,
363                PendingStage::Disposal,
364                wipe,
365                PhysicalProtection::ProtectionConfirmedAbsent,
366                cursor,
367            )),
368            DisposalResult::AllocationPresenceUnknown => Err(CleanupError {
369                pending_stage: PendingStage::Disposal,
370                wipe,
371                physical_protection: PhysicalProtection::ProtectionUnknown,
372                accounting: AccountingPosture::Charged,
373                lifecycle: LifecyclePosture::Tombstoned {
374                    last_stage: PendingStage::Disposal,
375                    disposition: AllocationPresence::Unknown,
376                },
377                pending_substage: super::JournalDisposition::Indeterminate,
378                retry_attempt: 1,
379                provider_health: ProviderHealth::Shutdown,
380            }),
381        }
382    }
383
384    fn quarantine(
385        &self,
386        handle: P::Handle,
387        pending_stage: PendingStage,
388        wipe: WipeEvidence,
389        cursor: TeardownCursor,
390    ) -> CleanupError {
391        let physical = self
392            .provider
393            .physical_protection(&ProviderAccess::new(), &handle);
394        self.quarantine_with_physical(handle, pending_stage, wipe, physical, cursor)
395    }
396
397    fn quarantine_with_physical(
398        &self,
399        handle: P::Handle,
400        pending_stage: PendingStage,
401        wipe: WipeEvidence,
402        physical_protection: PhysicalProtection,
403        cursor: TeardownCursor,
404    ) -> CleanupError {
405        let record = QuarantineRecord {
406            pending_stage,
407            wipe,
408            physical_protection,
409            accounting: AccountingPosture::Charged,
410            cursor,
411            retry_attempt: 1,
412        };
413        self.provider
414            .quarantine(&ProviderAccess::new(), handle, record);
415        let health = self.provider.health();
416        CleanupError {
417            pending_stage,
418            wipe,
419            physical_protection,
420            accounting: AccountingPosture::Charged,
421            lifecycle: LifecyclePosture::Quarantined { pending_stage },
422            pending_substage: cursor.disposition,
423            retry_attempt: 1,
424            provider_health: health,
425        }
426    }
427}
428
429impl<P, State, Level> Drop for ProtectedSecret<'_, P, State, Level>
430where
431    P: ProtectedMemoryProvider,
432    Level: AssuranceLevel,
433{
434    fn drop(&mut self) {
435        let _ = self.close_inner();
436    }
437}
438
439impl<P, State, Level> core::fmt::Debug for ProtectedSecret<'_, P, State, Level>
440where
441    P: ProtectedMemoryProvider,
442    Level: AssuranceLevel,
443{
444    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
445        formatter
446            .debug_struct("ProtectedSecret")
447            .field("bytes", &"<redacted>")
448            .field("initialized_len", &self.initialized_len)
449            .field("capacity", &self.capacity())
450            .finish_non_exhaustive()
451    }
452}
453
454#[allow(unsafe_code)]
455unsafe impl<P, State, Level> Send for ProtectedSecret<'_, P, State, Level>
456where
457    P: ThreadMovableProvider + Sync,
458    P::Handle: Send,
459    State: Send,
460    Level: AssuranceLevel + Send,
461{
462}
463
464/// Explicit borrowed exposure of one validated protected secret.
465pub struct ExposedProtectedSecret<'a> {
466    bytes: &'a [u8],
467}
468
469impl ExposedProtectedSecret<'_> {
470    /// Returns the explicitly exposed bytes.
471    #[must_use]
472    pub const fn as_bytes(&self) -> &[u8] {
473        self.bytes
474    }
475}
476
477impl AsRef<[u8]> for ExposedProtectedSecret<'_> {
478    fn as_ref(&self) -> &[u8] {
479        self.bytes
480    }
481}
482
483impl core::fmt::Debug for ExposedProtectedSecret<'_> {
484    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
485        formatter.write_str("ExposedProtectedSecret(<redacted>)")
486    }
487}
488
489fn closed_report(wipe: WipeEvidence) -> CleanupReport {
490    CleanupReport {
491        outcome: CleanupOutcome::Closed,
492        wipe,
493        physical_protection: PhysicalProtection::ProtectionConfirmedAbsent,
494        accounting: AccountingPosture::Reconciled,
495        lifecycle: LifecyclePosture::Closed,
496    }
497}