Skip to main content

osdp/packet/
scb.rs

1//! Security Control Block — the optional `SEC_BLK_*` field that follows the
2//! [`ControlByte`](crate::ControlByte) when [`CtrlFlags::HAS_SCB`] is set.
3//!
4//! # Spec: §5.9, Annex D
5//!
6//! ```text
7//! +------------+-----------+--------------+
8//! | SEC_BLK_LEN| SEC_BLK_TY| SEC_BLK_DATA |
9//! +------------+-----------+--------------+
10//!  1 byte       1 byte      LEN-2 bytes
11//! ```
12//!
13//! `SEC_BLK_LEN` covers the entire block including the LEN and TYPE bytes
14//! themselves, so `SEC_BLK_DATA` is `SEC_BLK_LEN - 2` bytes long.
15
16use crate::error::Error;
17
18/// Security block type byte (`SEC_BLK_TYPE`).
19///
20/// # Spec: §5.9, Table 3 (cross-referenced via Annex D)
21#[allow(non_camel_case_types)]
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23#[repr(u8)]
24pub enum ScsType {
25    /// `SCS_11` — ACU → PD. Begin SC session, carries `RND.A`.
26    Scs11 = 0x11,
27    /// `SCS_12` — PD → ACU. cUID + `RND.B` + client cryptogram.
28    Scs12 = 0x12,
29    /// `SCS_13` — ACU → PD. Server cryptogram.
30    Scs13 = 0x13,
31    /// `SCS_14` — PD → ACU. Initial R-MAC.
32    Scs14 = 0x14,
33    /// `SCS_15` — ACU → PD. Secure message, MAC only.
34    Scs15 = 0x15,
35    /// `SCS_16` — PD → ACU. Secure reply, MAC only.
36    Scs16 = 0x16,
37    /// `SCS_17` — ACU → PD. Secure message, encrypted DATA + MAC.
38    Scs17 = 0x17,
39    /// `SCS_18` — PD → ACU. Secure reply, encrypted DATA + MAC.
40    Scs18 = 0x18,
41}
42
43impl ScsType {
44    /// Parse from byte.
45    pub const fn from_byte(b: u8) -> Result<Self, Error> {
46        Ok(match b {
47            0x11 => Self::Scs11,
48            0x12 => Self::Scs12,
49            0x13 => Self::Scs13,
50            0x14 => Self::Scs14,
51            0x15 => Self::Scs15,
52            0x16 => Self::Scs16,
53            0x17 => Self::Scs17,
54            0x18 => Self::Scs18,
55            other => return Err(Error::BadSecurityBlock(other)),
56        })
57    }
58
59    /// Raw byte value.
60    pub const fn as_byte(self) -> u8 {
61        self as u8
62    }
63
64    /// `true` if this SCS implies a 4-byte MAC trailer before the checksum / CRC.
65    ///
66    /// # Spec: §5.9
67    pub const fn has_mac(self) -> bool {
68        matches!(self, Self::Scs15 | Self::Scs16 | Self::Scs17 | Self::Scs18)
69    }
70
71    /// `true` if this SCS implies the DATA payload is AES-CBC encrypted.
72    pub const fn is_encrypted(self) -> bool {
73        matches!(self, Self::Scs17 | Self::Scs18)
74    }
75
76    /// `true` if this SCS is part of the handshake (no DATA encryption, no MAC).
77    pub const fn is_handshake(self) -> bool {
78        matches!(self, Self::Scs11 | Self::Scs12 | Self::Scs13 | Self::Scs14)
79    }
80}
81
82/// Borrowed view of a parsed Security Control Block.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub struct ScbView<'a> {
85    /// Block type.
86    pub ty: ScsType,
87    /// `SEC_BLK_DATA` — type-specific payload, `SEC_BLK_LEN - 2` bytes.
88    pub data: &'a [u8],
89}
90
91impl<'a> ScbView<'a> {
92    /// Length on the wire (includes LEN + TYPE bytes).
93    pub fn wire_len(&self) -> usize {
94        self.data.len() + 2
95    }
96
97    /// Decode SCB starting at the LEN byte.
98    pub fn parse(buf: &'a [u8]) -> Result<Self, Error> {
99        if buf.len() < 2 {
100            return Err(Error::Truncated {
101                have: buf.len(),
102                need: 2,
103            });
104        }
105        let len = buf[0] as usize;
106        if len < 2 {
107            return Err(Error::BadSecurityBlockLength(buf[1]));
108        }
109        if buf.len() < len {
110            return Err(Error::Truncated {
111                have: buf.len(),
112                need: len,
113            });
114        }
115        let ty = ScsType::from_byte(buf[1])?;
116        Ok(Self {
117            ty,
118            data: &buf[2..len],
119        })
120    }
121}
122
123#[cfg(feature = "alloc")]
124mod alloc_impls {
125    use super::*;
126    use alloc::vec::Vec;
127
128    /// Owned Security Control Block.
129    #[derive(Debug, Clone, PartialEq, Eq)]
130    pub struct Scb {
131        /// Block type.
132        pub ty: ScsType,
133        /// `SEC_BLK_DATA`.
134        pub data: Vec<u8>,
135    }
136
137    impl Scb {
138        /// New block.
139        pub fn new(ty: ScsType, data: impl Into<Vec<u8>>) -> Self {
140            Self {
141                ty,
142                data: data.into(),
143            }
144        }
145
146        /// Borrow as a [`ScbView`].
147        pub fn as_view(&self) -> ScbView<'_> {
148            ScbView {
149                ty: self.ty,
150                data: &self.data,
151            }
152        }
153
154        /// Encode the SCB onto the back of `out`.
155        pub fn encode(&self, out: &mut Vec<u8>) {
156            let len = (self.data.len() + 2) as u8;
157            out.push(len);
158            out.push(self.ty.as_byte());
159            out.extend_from_slice(&self.data);
160        }
161    }
162
163    impl From<ScbView<'_>> for Scb {
164        fn from(v: ScbView<'_>) -> Self {
165            Self {
166                ty: v.ty,
167                data: v.data.to_vec(),
168            }
169        }
170    }
171}
172
173#[cfg(feature = "alloc")]
174pub use alloc_impls::Scb;
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn scs_type_roundtrip() {
182        for byte in [0x11u8, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18] {
183            assert_eq!(ScsType::from_byte(byte).unwrap().as_byte(), byte);
184        }
185    }
186
187    #[test]
188    fn scs_type_rejects_unknown() {
189        assert!(ScsType::from_byte(0x10).is_err());
190        assert!(ScsType::from_byte(0x19).is_err());
191        assert!(ScsType::from_byte(0x00).is_err());
192    }
193
194    #[test]
195    fn parse_minimal() {
196        let buf = [0x02u8, 0x11];
197        let scb = ScbView::parse(&buf).unwrap();
198        assert_eq!(scb.ty, ScsType::Scs11);
199        assert!(scb.data.is_empty());
200        assert_eq!(scb.wire_len(), 2);
201    }
202
203    #[test]
204    fn parse_with_data() {
205        let buf = [0x05u8, 0x12, 0xAA, 0xBB, 0xCC];
206        let scb = ScbView::parse(&buf).unwrap();
207        assert_eq!(scb.ty, ScsType::Scs12);
208        assert_eq!(scb.data, &[0xAA, 0xBB, 0xCC]);
209    }
210
211    #[test]
212    fn parse_rejects_short_len() {
213        assert!(ScbView::parse(&[0x01u8, 0x11]).is_err());
214        assert!(ScbView::parse(&[]).is_err());
215    }
216
217    #[test]
218    fn mac_predicate() {
219        for ty in [
220            ScsType::Scs15,
221            ScsType::Scs16,
222            ScsType::Scs17,
223            ScsType::Scs18,
224        ] {
225            assert!(ty.has_mac());
226        }
227        for ty in [
228            ScsType::Scs11,
229            ScsType::Scs12,
230            ScsType::Scs13,
231            ScsType::Scs14,
232        ] {
233            assert!(!ty.has_mac());
234        }
235    }
236}