Skip to main content

encryption/encryptor/
cbc_hmac.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3//! CBC + HMAC-SHA256(encrypt-then-MAC,PKCS#7 填充)的**唯一实现**。
4//!
5//! `aes-256-cbc-hmac` / `camellia-256-cbc-hmac` / `aria-256-cbc-hmac` /
6//! `kuznyechik-256-cbc-hmac` / `threefish-512-cbc-hmac` / `sm4-cbc` 六个标识
7//! 共用这一个泛型:它们此前是六份逐字节相同的实现(只有块密码类型、标识符
8//! 与密钥长度不同),任何一处边界修正都要改六遍。现在泛型参数 `C` 是块密码、
9//! `KEY_LEN` 是密钥长度,载荷统一为 `v1 | IV | MAC(32) | 密文`,
10//! IV 长度取自 `C` 的块长(16 字节,Threefish-512 为 64 字节)。
11//!
12//! 泛型对使用者的可见面为零:各算法的公开类型名由下面的 `type` 别名保持原样,
13//! `new([u8; N])` / `from_slice(&[u8])` 签名不变。
14
15use cbc::cipher::{
16    Block, BlockCipher, BlockDecryptMut, BlockEncryptMut, BlockSizeUser, KeyIvInit, Unsigned,
17    block_padding::Pkcs7,
18};
19use cbc::{Decryptor, Encryptor};
20use rand_core::{OsRng, RngCore};
21
22use crate::contract::{Identified, SymmetricCipher};
23use crate::error::{Error, Result};
24use crate::internal::etm::{self, EtmKeys};
25
26/// CBC-HMAC 加密器:`C` 为块密码,`KEY_LEN` 为其密钥长度(字节)。
27///
28/// 直接在结构体里持有派生好的 EtM 密钥对(密文密钥 + MAC 密钥,均 Drop 清零),
29/// 因此每次调用不再重复派生 MAC 密钥。
30pub struct CbcHmacEncryptor<
31    C: BlockCipher + BlockEncryptMut + BlockDecryptMut + cbc::cipher::KeyInit,
32    const KEY_LEN: usize,
33> {
34    keys: EtmKeys<KEY_LEN>,
35    identifier: &'static str,
36    label: &'static str,
37    // `PhantomData<fn() -> C>` 而不是 `PhantomData<C>`:后者会把 `C` 的
38    // Send/Sync 约束传染给加密器,导致 `SymmetricCipher`(要求 Send + Sync)
39    // 实现不出来;`fn() -> C` 是函数指针,永远 Send + Sync,且对 C 协变。
40    _cipher: std::marker::PhantomData<fn() -> C>,
41}
42
43impl<C, const KEY_LEN: usize> CbcHmacEncryptor<C, KEY_LEN>
44where
45    C: BlockCipher + BlockEncryptMut + BlockDecryptMut + cbc::cipher::KeyInit,
46{
47    /// 具名构造:`identifier` 为注册表标识,`label` 为错误消息里的算法名。
48    pub fn with_names(key: [u8; KEY_LEN], identifier: &'static str, label: &'static str) -> Self {
49        Self {
50            keys: EtmKeys::new(key),
51            identifier,
52            label,
53            _cipher: std::marker::PhantomData,
54        }
55    }
56
57    /// 从切片构造;长度不符返回 [`Error::InvalidKeyLength`]。
58    pub fn with_names_from_slice(
59        key: &[u8],
60        identifier: &'static str,
61        label: &'static str,
62    ) -> Result<Self> {
63        Ok(Self {
64            keys: EtmKeys::from_slice(key, label)?,
65            identifier,
66            label,
67            _cipher: std::marker::PhantomData,
68        })
69    }
70
71    /// CBC 的 IV 长度即块密码的块长(AES/Camellia/ARIA/Kuznyechik/SM4 为 16,
72    /// Threefish-512 为 64)。
73    pub const fn iv_len() -> usize {
74        <C as BlockSizeUser>::BlockSize::USIZE
75    }
76}
77
78impl<C, const KEY_LEN: usize> SymmetricCipher for CbcHmacEncryptor<C, KEY_LEN>
79where
80    C: BlockCipher + BlockEncryptMut + BlockDecryptMut + cbc::cipher::KeyInit,
81{
82    fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
83        let mut iv = Block::<C>::default();
84        OsRng.fill_bytes(&mut iv);
85
86        let ciphertext = Encryptor::<C>::new_from_slices(self.keys.cipher(), &iv)
87            .expect("key and iv lengths are fixed")
88            .encrypt_padded_vec_mut::<Pkcs7>(plaintext);
89
90        Ok(etm::pack(&iv, &ciphertext, self.keys.mac()))
91    }
92
93    fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
94        let (iv, body) =
95            etm::unpack_and_verify(ciphertext, Self::iv_len(), self.keys.mac(), self.label)?;
96
97        Decryptor::<C>::new_from_slices(self.keys.cipher(), iv)
98            .expect("key and iv lengths were verified")
99            .decrypt_padded_vec_mut::<Pkcs7>(body)
100            .map_err(|_| Error::DecryptionFailed { label: self.label })
101    }
102}
103
104impl<C, const KEY_LEN: usize> Identified for CbcHmacEncryptor<C, KEY_LEN>
105where
106    C: BlockCipher + BlockEncryptMut + BlockDecryptMut + cbc::cipher::KeyInit,
107{
108    fn identifier(&self) -> &str {
109        self.identifier
110    }
111}
112
113/// 只打印标识符——密钥材质不出现在 `Debug` 里。
114impl<C, const KEY_LEN: usize> std::fmt::Debug for CbcHmacEncryptor<C, KEY_LEN>
115where
116    C: BlockCipher + BlockEncryptMut + BlockDecryptMut + cbc::cipher::KeyInit,
117{
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        f.debug_struct("CbcHmacEncryptor")
120            .field("identifier", &self.identifier)
121            .finish_non_exhaustive()
122    }
123}
124
125/// 为每个块密码生成公开类型别名与单独的 `new` / `IDENTIFIER`。
126///
127/// 别名保持原类型名与构造签名,因此工厂、测试与使用者的既有代码无需改动。
128macro_rules! cbc_hmac_aliases {
129    ($( $(#[$doc:meta])* $name:ident = $cipher:ty, $key_len:literal, $label:literal, $identifier:literal; )*) => {
130        $(
131            $(#[$doc])*
132            pub type $name = CbcHmacEncryptor<$cipher, $key_len>;
133
134            impl CbcHmacEncryptor<$cipher, $key_len> {
135                /// 注册表标识。
136                pub const IDENTIFIER: &'static str = $identifier;
137
138                /// 以定长密钥构造(长度由类型保证,不会失败)。
139                pub fn new(key: [u8; $key_len]) -> Self {
140                    Self::with_names(key, $identifier, $label)
141                }
142
143                /// 从切片构造;长度不符返回 [`Error::InvalidKeyLength`]。
144                pub fn from_slice(key: &[u8]) -> Result<Self> {
145                    Self::with_names_from_slice(key, $identifier, $label)
146                }
147            }
148        )*
149    };
150}
151
152cbc_hmac_aliases! {
153    /// AES-256-CBC + HMAC-SHA256(兼容旧环境)。
154    Aes256CbcEncryptor = aes::Aes256, 32, "AES-256-CBC", "aes-256-cbc-hmac";
155
156    /// Camellia-256-CBC + HMAC-SHA256(ISO/IEC 18033-3)。
157    Camellia256CbcEncryptor = camellia::Camellia256, 32, "Camellia-256-CBC", "camellia-256-cbc-hmac";
158
159    /// ARIA-256-CBC + HMAC-SHA256(RFC 5794)。
160    Aria256CbcEncryptor = aria::Aria256, 32, "ARIA-256-CBC", "aria-256-cbc-hmac";
161
162    /// Kuznyechik-256-CBC + HMAC-SHA256(GOST R 34.12-2015)。
163    Kuznyechik256CbcEncryptor = kuznyechik::Kuznyechik, 32, "Kuznyechik-256-CBC", "kuznyechik-256-cbc-hmac";
164
165    /// Threefish-512-CBC + HMAC-SHA256(块与 IV 均 64 字节)。
166    Threefish512CbcEncryptor = threefish::Threefish512, 64, "Threefish-512-CBC", "threefish-512-cbc-hmac";
167
168    /// SM4-CBC + HMAC-SHA256(国密,GB/T 32907)。
169    Sm4CbcEncryptor = sm4::Sm4, 16, "SM4", "sm4-cbc";
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    /// 六个别名共享同一实现:逐个往返 + 篡改必败。
177    ///
178    /// 边界载荷按**块长**取,不能写死 16:PKCS#7 在「明文恰为块长整数倍」时会
179    /// 补出整整一个填充块,Threefish-512 的块是 64 字节——只喂 16/17 字节的话,
180    /// 它的填充分支根本没被走到。
181    #[test]
182    fn every_cbc_hmac_alias_roundtrips_and_detects_tampering() {
183        fn check<E: SymmetricCipher>(cipher: &E, label: &str, block_len: usize) {
184            let exact = vec![b'0'; block_len];
185            let one_over = vec![b'1'; block_len + 1];
186            let multi = vec![0xa5u8; block_len * 3 + 7];
187
188            for plaintext in [
189                b"".as_slice(),
190                b"x",
191                exact.as_slice(),
192                one_over.as_slice(),
193                multi.as_slice(),
194            ] {
195                let blob = cipher.encrypt(plaintext).unwrap();
196                assert_eq!(cipher.decrypt(&blob).unwrap(), plaintext, "{label}");
197
198                let mut tampered = blob.clone();
199                let last = tampered.len() - 1;
200                tampered[last] ^= 0x01;
201                assert!(cipher.decrypt(&tampered).is_err(), "{label} 篡改未被发现");
202            }
203        }
204
205        check(&Sm4CbcEncryptor::new([5u8; 16]), "sm4-cbc", 16);
206        check(&Aes256CbcEncryptor::new([5u8; 32]), "aes-256-cbc-hmac", 16);
207        check(&Camellia256CbcEncryptor::new([5u8; 32]), "camellia", 16);
208        check(&Aria256CbcEncryptor::new([5u8; 32]), "aria", 16);
209        check(&Kuznyechik256CbcEncryptor::new([5u8; 32]), "kuznyechik", 16);
210        check(
211            &Threefish512CbcEncryptor::new([5u8; 64]),
212            "threefish-512",
213            64,
214        );
215    }
216
217    /// IV 长度必须等于块密码块长——Threefish-512 是 64,其余是 16。
218    #[test]
219    fn iv_length_follows_the_block_cipher_block_size() {
220        assert_eq!(Aes256CbcEncryptor::iv_len(), 16);
221        assert_eq!(Camellia256CbcEncryptor::iv_len(), 16);
222        assert_eq!(Aria256CbcEncryptor::iv_len(), 16);
223        assert_eq!(Kuznyechik256CbcEncryptor::iv_len(), 16);
224        assert_eq!(Sm4CbcEncryptor::iv_len(), 16);
225        assert_eq!(Threefish512CbcEncryptor::iv_len(), 64);
226    }
227
228    /// 标识符保持历史取值(注册表键不能变)。
229    #[test]
230    fn identifiers_are_stable() {
231        assert_eq!(
232            Aes256CbcEncryptor::new([0u8; 32]).identifier(),
233            "aes-256-cbc-hmac"
234        );
235        assert_eq!(
236            Camellia256CbcEncryptor::new([0u8; 32]).identifier(),
237            "camellia-256-cbc-hmac"
238        );
239        assert_eq!(
240            Aria256CbcEncryptor::new([0u8; 32]).identifier(),
241            "aria-256-cbc-hmac"
242        );
243        assert_eq!(
244            Kuznyechik256CbcEncryptor::new([0u8; 32]).identifier(),
245            "kuznyechik-256-cbc-hmac"
246        );
247        assert_eq!(
248            Threefish512CbcEncryptor::new([0u8; 64]).identifier(),
249            "threefish-512-cbc-hmac"
250        );
251        assert_eq!(Sm4CbcEncryptor::new([0u8; 16]).identifier(), "sm4-cbc");
252    }
253
254    /// IV 每次加密都必须是新的随机值。
255    #[test]
256    fn iv_is_fresh_per_encryption() {
257        let cipher = Aes256CbcEncryptor::new([5u8; 32]);
258        let first = cipher.encrypt(b"same").unwrap();
259        let second = cipher.encrypt(b"same").unwrap();
260        assert_ne!(first, second);
261        assert_ne!(first[2..18], second[2..18]);
262    }
263
264    /// 换密钥必须 MAC 失败(不是填充错),前缀/长度错误各自报对应错误。
265    #[test]
266    fn wrong_key_and_bad_frames_are_rejected() {
267        let blob = Aes256CbcEncryptor::new([5u8; 32])
268            .encrypt(b"secret")
269            .unwrap();
270        let other = Aes256CbcEncryptor::new([6u8; 32]);
271        assert_eq!(
272            other.decrypt(&blob).unwrap_err(),
273            Error::MacVerificationFailed {
274                label: "AES-256-CBC"
275            }
276        );
277        assert_eq!(
278            other.decrypt(b"v9 nope").unwrap_err(),
279            Error::InvalidPrefix {
280                label: "AES-256-CBC"
281            }
282        );
283        assert_eq!(
284            other.decrypt(b"v1 short").unwrap_err(),
285            Error::TooShort {
286                label: "AES-256-CBC"
287            }
288        );
289    }
290
291    #[test]
292    fn key_length_is_checked_from_slice() {
293        assert_eq!(
294            Aes256CbcEncryptor::from_slice(&[0u8; 16]).unwrap_err(),
295            Error::InvalidKeyLength {
296                label: "AES-256-CBC",
297                expected: 32,
298                got: 16
299            }
300        );
301        assert_eq!(
302            Sm4CbcEncryptor::from_slice(&[0u8; 32]).unwrap_err(),
303            Error::InvalidKeyLength {
304                label: "SM4",
305                expected: 16,
306                got: 32
307            }
308        );
309        assert_eq!(
310            Threefish512CbcEncryptor::from_slice(&[0u8; 32]).unwrap_err(),
311            Error::InvalidKeyLength {
312                label: "Threefish-512-CBC",
313                expected: 64,
314                got: 32
315            }
316        );
317    }
318
319    /// 裸块密码必须与各自标准的示例向量一致——往返测试抓不出「轮函数整体错位」。
320    #[test]
321    fn bare_block_ciphers_match_their_standard_vectors() {
322        use cbc::cipher::{BlockEncrypt, KeyInit};
323
324        fn unhex(text: &str) -> Vec<u8> {
325            crate::internal::hex_decode(text).unwrap()
326        }
327        fn block_encrypt<C: BlockEncrypt + KeyInit>(key: &[u8], plaintext: &[u8]) -> Vec<u8> {
328            let cipher = C::new_from_slice(key).unwrap();
329            let mut block = Block::<C>::clone_from_slice(plaintext);
330            cipher.encrypt_block(&mut block);
331            block.to_vec()
332        }
333
334        // SM4:GB/T 32907-2016 附录 A.1。
335        assert_eq!(
336            block_encrypt::<sm4::Sm4>(
337                &unhex("0123456789abcdeffedcba9876543210"),
338                &unhex("0123456789abcdeffedcba9876543210")
339            ),
340            unhex("681edf34d206965e86b3e94f536e4246")
341        );
342
343        // Camellia-256:RFC 3713「128-bit key の test data」之外的 256 位示例
344        // (RFC 3713 §2.3 的 256-bit key 向量)。
345        assert_eq!(
346            block_encrypt::<camellia::Camellia256>(
347                &unhex("0123456789abcdeffedcba987654321000112233445566778899aabbccddeeff"),
348                &unhex("0123456789abcdeffedcba9876543210")
349            ),
350            unhex("9acc237dff16d76c20ef7c919e3a7509")
351        );
352
353        // ARIA-256:RFC 5794 附录 A.3。
354        assert_eq!(
355            block_encrypt::<aria::Aria256>(
356                &unhex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"),
357                &unhex("00112233445566778899aabbccddeeff")
358            ),
359            unhex("f92bd7c79fb72e2f2b8f80c1972d24fc")
360        );
361
362        // Kuznyechik:RFC 7801 附录 A.1(256 位密钥)。
363        assert_eq!(
364            block_encrypt::<kuznyechik::Kuznyechik>(
365                &unhex("8899aabbccddeeff0011223344556677fedcba98765432100123456789abcdef"),
366                &unhex("1122334455667700ffeeddccbbaa9988")
367            ),
368            unhex("7f679d90bebc24305a468d42b9d4edcd")
369        );
370
371        // Threefish-512:Skein/Threefish 官方 KAT 的前两条**链式**向量
372        // (第二条以第一条的密文为密钥)。两条都要在:
373        // threefish 0.5.2 自带的 tests/mod.rs 首行是 `#![cfg(featue = "cipher")]`
374        // (feature 拼写错误),整个测试目标被 cfg 掉、从未跑过——这两条向量
375        // 是该实现唯一的事实锚点。
376        let threefish_vectors = [
377            (
378                "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
379                "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
380                concat!(
381                    "b1a2bbc6ef6025bc40eb3822161f36e375d1bb0aee3186fbd19e47c5d479947b",
382                    "7bc2f8586e35f0cff7e7f03084b0b7b1f1ab3961a580a3e97eb41ea14a6d7bbe"
383                ),
384            ),
385            (
386                concat!(
387                    "b1a2bbc6ef6025bc40eb3822161f36e375d1bb0aee3186fbd19e47c5d479947b",
388                    "7bc2f8586e35f0cff7e7f03084b0b7b1f1ab3961a580a3e97eb41ea14a6d7bbe"
389                ),
390                "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
391                concat!(
392                    "f13ca06760dd9bbeab87b6c56f3bbbdb",
393                    "e9d08a77978b942ac02d471dc10268f2",
394                    "261c3d4330d6ca341f4bd4115dee16a2",
395                    "1dcda2a34a0a76fba976174e4cf1e306"
396                ),
397            ),
398        ];
399        for (key, plaintext, expected) in threefish_vectors {
400            assert_eq!(
401                block_encrypt::<threefish::Threefish512>(&unhex(key), &unhex(plaintext)),
402                unhex(expected),
403                "Threefish-512 official KAT"
404            );
405        }
406    }
407}