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    let mut iv = [0u8; 16];
23    for (d, s) in iv.iter_mut().zip(other_mac.iter()) {
24        *d = !*s;
25    }
26    iv
27}
28
29/// Encrypt a DATA payload with AES-128-CBC, applying the always-0x80 padding
30/// rule. `s_enc` is the session encryption key, `iv` is the complemented MAC
31/// of the last frame received from the other side.
32///
33/// Output length is `((data.len() + 1).div_ceil(16)) * 16`.
34pub fn encrypt_data(s_enc: &[u8; 16], iv: &[u8; 16], data: &[u8]) -> Vec<u8> {
35    let mut buf = Vec::with_capacity(data.len() + 16);
36    buf.extend_from_slice(data);
37    pad_data(&mut buf);
38    debug_assert_eq!(buf.len() % 16, 0);
39
40    let mut enc = Encryptor::new(
41        GenericArray::from_slice(s_enc),
42        GenericArray::from_slice(iv),
43    );
44    for chunk in buf.chunks_exact_mut(16) {
45        let mut block = *GenericArray::from_slice(chunk);
46        enc.encrypt_block_mut(&mut block);
47        chunk.copy_from_slice(&block);
48    }
49    buf
50}
51
52/// Decrypt a DATA payload and strip the 0x80 padding.
53///
54/// Errors if `ct.len()` is not a positive multiple of 16, or if padding is
55/// malformed after decryption.
56pub fn decrypt_data(
57    s_enc: &[u8; 16],
58    iv: &[u8; 16],
59    ct: &[u8],
60) -> Result<Vec<u8>, SecureSessionError> {
61    if ct.is_empty() || ct.len() % 16 != 0 {
62        return Err(SecureSessionError::BadPadding);
63    }
64    let mut buf = ct.to_vec();
65    let mut dec = Decryptor::new(
66        GenericArray::from_slice(s_enc),
67        GenericArray::from_slice(iv),
68    );
69    for chunk in buf.chunks_exact_mut(16) {
70        let mut block = *GenericArray::from_slice(chunk);
71        dec.decrypt_block_mut(&mut block);
72        chunk.copy_from_slice(&block);
73    }
74    let stripped = unpad_data(&buf)?;
75    Ok(stripped.to_vec())
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn roundtrip_short() {
84        let s_enc = [0x42u8; 16];
85        let iv = [0xAAu8; 16];
86        for n in 0..40usize {
87            let plain: Vec<u8> = (0u8..(n as u8)).collect();
88            let ct = encrypt_data(&s_enc, &iv, &plain);
89            assert_eq!(ct.len() % 16, 0);
90            assert!(ct.len() > plain.len());
91            let pt = decrypt_data(&s_enc, &iv, &ct).unwrap();
92            assert_eq!(pt, plain);
93        }
94    }
95
96    #[test]
97    fn complement_icv_is_xor_ff() {
98        let mac = [0xA5u8; 16];
99        let iv = complement_icv(&mac);
100        assert!(iv.iter().all(|&b| b == 0x5A));
101    }
102
103    #[test]
104    fn decrypt_rejects_unaligned() {
105        let key = [0u8; 16];
106        let iv = [0u8; 16];
107        assert!(decrypt_data(&key, &iv, &[0u8; 15]).is_err());
108        assert!(decrypt_data(&key, &iv, &[]).is_err());
109    }
110
111    /// Wrong IV → padding will not validate → error.
112    #[test]
113    fn wrong_iv_fails_padding() {
114        let s_enc = [0x42u8; 16];
115        let iv = [0xAAu8; 16];
116        let ct = encrypt_data(&s_enc, &iv, &[1, 2, 3, 4]);
117        let bad_iv = [0xFFu8; 16];
118        assert!(decrypt_data(&s_enc, &bad_iv, &ct).is_err());
119    }
120}