//! `PrivacyGate` — monotonic class transitions for `BfldFrame`. ADR-120 §2.4. //! //! The only way a higher-information frame becomes a lower-information frame //! is through [`PrivacyGate::demote`]. This function: //! //! 1. Asserts the target class is **strictly higher in numerical value** (or //! equal) to the current class — going from Derived(1) to Anonymous(2) is //! a demote; going from Anonymous(2) back to Derived(1) is forbidden. //! 2. Zeroes payload sections that are not permitted at the target class, //! using a `black_box`-guarded loop to defeat dead-store elimination. //! 3. Re-syncs `header.privacy_class` and `header.payload_crc32`. //! 4. Returns the new frame. //! //! There is no `promote` operation by design — once a section is zeroed, the //! original bytes are unrecoverable. #![cfg(feature = "std")] use crate::frame::crc32_of_payload; use crate::{BfldError, BfldFrame, BfldPayload, PrivacyClass}; /// Monotonic class transformer. See module docs. pub struct PrivacyGate; impl PrivacyGate { /// Apply a class demotion in-place: returns a new `BfldFrame` whose /// `privacy_class`, payload sections, and CRC match `target`. /// /// Returns [`BfldError::InvalidDemote`] when `target` would *increase* /// the information density (lower class number than the source). pub fn demote( mut frame: BfldFrame, target: PrivacyClass, ) -> Result { let current = PrivacyClass::try_from(frame.header.privacy_class)?; if target.as_u8() < current.as_u8() { return Err(BfldError::InvalidDemote { from: current.as_u8(), to: target.as_u8(), }); } // Strip payload sections not permitted at the target class. We only do // this when the payload parses cleanly; a malformed payload remains // untouched in the bytes (the class byte and CRC still get re-synced). if let Ok(mut payload) = frame.parse_payload() { if target.as_u8() >= PrivacyClass::Anonymous.as_u8() { // Anonymous: drop the compressed angle matrix (identity surface). zeroize_then_clear(&mut payload.compressed_angle_matrix); // Also drop optional sections that may carry identity-leaky // signal under high-separability conditions. if let Some(csi) = payload.csi_delta.as_mut() { zeroize_then_clear(csi); } } if target.as_u8() >= PrivacyClass::Restricted.as_u8() { // Restricted: also drop amplitude + phase proxies. zeroize_then_clear(&mut payload.amplitude_proxy); zeroize_then_clear(&mut payload.phase_proxy); } // Note: csi_delta dropped above implies the flag bit should clear. // from_payload re-derives the flag from csi_delta.is_some(), so // taking the Option out below ensures the bit is cleared. if target.as_u8() >= PrivacyClass::Anonymous.as_u8() { payload.csi_delta = None; } frame = BfldFrame::from_payload(frame.header, &payload); } frame.header.privacy_class = target.as_u8(); // from_payload already recomputed CRC, but recompute again so the // path that skipped payload parsing still produces a consistent frame. frame.header.payload_crc32 = crc32_of_payload(&frame.payload); Ok(frame) } } /// Overwrite `v` with zeros, then truncate. The `black_box` call defeats /// dead-store elimination so the writes are observable. fn zeroize_then_clear(v: &mut Vec) { for b in v.iter_mut() { *b = 0; } core::hint::black_box(v.as_ptr()); v.clear(); } // Convenience constructor: the gate is a unit type, but keeping a Default // makes downstream injection sites (PrivacyGate.demote(...) vs static call) // straightforward. impl Default for PrivacyGate { fn default() -> Self { Self } } /// Discard the rest of an unused (#[allow(dead_code)]) — placeholder so /// `BfldPayload` import isn't unused in builds that strip the implementation. #[allow(dead_code)] fn _unused_payload_marker(_: BfldPayload) {}