Skip to main content

osdp/secure/
cipher.rs

1//! AES-128-CBC encryption / decryption of `SCS_17`/`SCS_18` DATA payloads.
2//!
3//! # Spec: Annex D.5
4//!
5//! - DATA is **always** 0x80-padded (per [`super::pad::pad_data`]).
6//! - The CBC ICV is the **one's complement** of the last MAC received from
7//!   the *other* device.
8//! - Key is `S-ENC`.
9
10use crate::error::SecureSessionError;
11use crate::secure::pad::{pad_data, unpad_data};
12use aes::cipher::generic_array::GenericArray;
13use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit};
14use alloc::vec::Vec;
15
16type Encryptor = cbc::Encryptor<aes::Aes128>;
17type Decryptor = cbc::Decryptor<aes::Aes128>;
18
19/// Annex D.5 ICV: one's complement of the last MAC from the other side.
20#[inline]
21pub fn complement_icv(other_mac: &[u8; 16]) -> [u8; 16] {
22    core::array::from_fn(|i| !other_mac[i])
23}
24
25/// Encrypt a DATA payload with AES-128-CBC, applying the always-0x80 padding
26/// rule. `s_enc` is the session encryption key, `iv` is the complemented MAC
27/// of the last frame received from the other side.
28///
29/// Output length is `((data.len() + 1).div_ceil(16)) * 16`.
30pub fn encrypt_data(s_enc: &[u8; 16], iv: &[u8; 16], data: &[u8]) -> Vec<u8> {
31    let mut buf = Vec::with_capacity(data.len() + 16);
32    buf.extend_from_slice(data);
33    pad_data(&mut buf);
34    debug_assert_eq!(buf.len() % 16, 0);
35
36    let mut enc = Encryptor::new(
37        GenericArray::from_slice(s_enc),
38        GenericArray::from_slice(iv),
39    );
40    for chunk in buf.chunks_exact_mut(16) {
41        let mut block = *GenericArray::from_slice(chunk);
42        enc.encrypt_block_mut(&mut block);
43        chunk.copy_from_slice(&block);
44    }
45    buf
46}
47
48/// Decrypt a DATA payload and strip the 0x80 padding.
49///
50/// Errors if `ct.len()` is not a positive multiple of 16, or if padding is
51/// malformed after decryption.
52pub fn decrypt_data(
53    s_enc: &[u8; 16],
54    iv: &[u8; 16],
55    ct: &[u8],
56) -> Result<Vec<u8>, SecureSessionError> {
57    if ct.is_empty() || ct.len() % 16 != 0 {
58        return Err(SecureSessionError::BadPadding);
59    }
60    let mut buf = ct.to_vec();
61    let mut dec = Decryptor::new(
62        GenericArray::from_slice(s_enc),
63        GenericArray::from_slice(iv),
64    );
65    for chunk in buf.chunks_exact_mut(16) {
66        let mut block = *GenericArray::from_slice(chunk);
67        dec.decrypt_block_mut(&mut block);
68        chunk.copy_from_slice(&block);
69    }
70    let stripped = unpad_data(&buf)?;
71    Ok(stripped.to_vec())
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    /// Deterministic test fixture bytes derived from a tag — not a real key/IV.
79    fn fixture(tag: u8) -> [u8; 16] {
80        core::array::from_fn(|i| tag.wrapping_add(i as u8))
81    }
82
83    #[test]
84    fn roundtrip_short() {
85        let s_enc = fixture(0x42);
86        let iv = fixture(0xAA);
87        for n in 0..40usize {
88            let plain: Vec<u8> = (0u8..(n as u8)).collect();
89            let ct = encrypt_data(&s_enc, &iv, &plain);
90            assert_eq!(ct.len() % 16, 0);
91            assert!(ct.len() > plain.len());
92            let pt = decrypt_data(&s_enc, &iv, &ct).unwrap();
93            assert_eq!(pt, plain);
94        }
95    }
96
97    #[test]
98    fn complement_icv_is_xor_ff() {
99        let mac = [0xA5u8; 16];
100        let iv = complement_icv(&mac);
101        assert!(iv.iter().all(|&b| b == 0x5A));
102    }
103
104    #[test]
105    fn decrypt_rejects_unaligned() {
106        let key = fixture(0);
107        let iv = fixture(0);
108        assert!(decrypt_data(&key, &iv, &[0u8; 15]).is_err());
109        assert!(decrypt_data(&key, &iv, &[]).is_err());
110    }
111
112    /// Wrong IV → padding will not validate → error.
113    #[test]
114    fn wrong_iv_fails_padding() {
115        let s_enc = fixture(0x42);
116        let iv = fixture(0xAA);
117        let ct = encrypt_data(&s_enc, &iv, &[1, 2, 3, 4]);
118        let bad_iv = fixture(0xFF);
119        assert!(decrypt_data(&s_enc, &bad_iv, &ct).is_err());
120    }
121}