e2e_protection/e2e.rs
1//! Core E2E traits and the session wrapper.
2
3/// Errors that may happen during protect/check/unwrap.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum E2eError {
6 /// Payload or frame length is out of the profile’s constraints.
7 Length,
8 /// CRC (integrity) verification failed.
9 Crc,
10 /// Data-ID (or Data-ID-List based mapping) check failed.
11 DataId,
12 /// Counter policy violation (gap, wrong step, illegal value).
13 Counter,
14 /// Feature or operation is not supported by the active profile.
15 Unsupported,
16}
17
18/// Minimal report returned by `E2eProfile::check`.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct CheckInfo {
21 /// Extracted counter (zero-extended).
22 pub counter: u32,
23 /// True if profile-level checks (CRC, DataId, basic length) passed.
24 pub ok: bool,
25}
26
27/// How counters should evolve across a session.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum CounterPolicy {
30 /// Monotonic with rollover of `bits` width and fixed `step` (usually 1).
31 Rollover { bits: u8, step: u32 },
32 /// No policy (accept any counter).
33 None,
34}
35impl CounterPolicy {
36 /// Compute the next counter to use for transmission given the previous one.
37 pub fn next(self, prev: Option<u32>) -> u32 {
38 match self {
39 CounterPolicy::None => prev.unwrap_or(0),
40 CounterPolicy::Rollover { bits, step } => {
41 if bits == 32 {
42 // full 32-bit rollover
43 prev.unwrap_or(u32::MAX).wrapping_add(step)
44 } else {
45 let m = (1u32 << bits) - 1;
46 let p = prev.unwrap_or(m);
47 (p + step) & m
48 }
49 }
50 }
51 }
52 /// Validate whether `next` is the correct successor of `prev`.
53 pub fn ok(self, prev: Option<u32>, next: u32) -> bool {
54 match self {
55 CounterPolicy::None => true,
56 CounterPolicy::Rollover { bits, step } => {
57 if bits == 32 {
58 prev.map(|p| p.wrapping_add(step) == next).unwrap_or(true)
59 } else {
60 let m = (1u32 << bits) - 1;
61 prev.map(|p| ((p + step) & m) == (next & m)).unwrap_or(true)
62 }
63 }
64 }
65 }
66}
67
68/// A single E2E profile.
69/// Implementations encapsulate packing, CRC calculation, and validation.
70pub trait E2eProfile {
71 /// Create a protected frame for the given `payload` and `counter`.
72 fn protect(&self, payload: &[u8], counter: u32) -> Result<Vec<u8>, E2eError>;
73
74 /// Validate a received `frame`, returning parsed `CheckInfo` if valid.
75 fn check(&self, frame: &[u8]) -> Result<CheckInfo, E2eError>;
76
77 /// Return the original payload length (for zero-copy `unwrap`) if derivable.
78 fn payload_len(&self, frame: &[u8]) -> Option<usize>;
79}
80
81/// High-level session object that holds a profile and a counter policy.
82pub struct E2eSession<P: E2eProfile> {
83 p: P,
84 pol: CounterPolicy,
85 last: Option<u32>,
86}
87impl<P: E2eProfile> E2eSession<P> {
88 /// Create a new session with a profile and a counter policy.
89 pub fn new(p: P, pol: CounterPolicy) -> Self {
90 Self { p, pol, last: None }
91 }
92
93 /// Wrap `payload` with the next session counter according to the policy.
94 pub fn wrap(&mut self, payload: &[u8]) -> Result<Vec<u8>, E2eError> {
95 let c = self.pol.next(self.last);
96 let f = self.p.protect(payload, c)?;
97 self.last = Some(c);
98 Ok(f)
99 }
100
101 /// Unwrap a `frame`: profile validation + policy check → return payload slice.
102 pub fn unwrap<'a>(&mut self, frame: &'a [u8]) -> Result<&'a [u8], E2eError> {
103 let info = self.p.check(frame)?;
104 if !self.pol.ok(self.last, info.counter) {
105 return Err(E2eError::Counter);
106 }
107 self.last = Some(info.counter);
108 let n = self.p.payload_len(frame).ok_or(E2eError::Unsupported)?;
109 Ok(&frame[..n])
110 }
111
112 /// The last successfully transmitted/accepted counter (if any).
113 pub fn last_counter(&self) -> Option<u32> { self.last }
114}