Skip to main content

encryption/guomi/
sm4_gcm.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3//! 国密 SM4-GCM(认证加密);载荷:`v1 | nonce(12) | tag(16) | 密文`
4//! (与 aes-256-gcm 完全一致)。密钥 16 字节(SM4 的密钥长度),AAD 固定为空。
5//!
6//! 不依赖现成的 SM4-GCM crate,按 GB/T 38636-2020 附录 A 的 GCM-AEK 组装:
7//!
8//! - `H = E_K(0^128)`;
9//! - `J0 = nonce‖0^31‖1`(96 位 nonce 的特例);
10//! - 密文 `C = GCTR_K(inc32(J0), P)`,计数器块自 `inc32(J0)` 起;
11//! - `S = GHASH_H(A‖0^v‖C‖0^u‖[len(A)]64‖[len(C)]64)`,本库 `A` 为空;
12//! - `T = E_K(J0) ⊕ S`(128 位 tag);验签恒时比较、先验后解。
13//!
14//! 定值测试(KAT)用 RFC 8998 附录 A.1 的 SM4-GCM 向量逐字节对齐——
15//! GB/T 38636-2020 附录 A 只有模式定义(算法 4 / 5),未附数值向量。
16
17use ctr::Ctr32BE;
18use ctr::CtrCore;
19use ctr::cipher::{Block, BlockEncrypt, InnerIvInit, KeyInit, StreamCipher};
20use ghash::GHash;
21use ghash::universal_hash::UniversalHash;
22use rand_core::{OsRng, RngCore};
23use sm4::Sm4;
24
25use crate::contract::{Identified, SymmetricCipher};
26use crate::error::{Error, Result};
27use crate::key::Key;
28
29const LABEL: &str = "SM4-GCM";
30const PREFIX: &[u8] = b"v1";
31const NONCE_LEN: usize = 12;
32const TAG_LEN: usize = 16;
33
34/// SM4-GCM 加密器。
35///
36/// SM4 实例在构造时就建立(轮密钥只展开一次),加解密共用:J0 掩码与 GHASH
37/// 直接引用它,CTR 流克隆它(克隆只是复制已展开的轮密钥)。此前每次
38/// `seal` / `open` 都要重建 SM4,`seal` 一次调用甚至建两遍。
39#[derive(Debug)]
40pub struct Sm4GcmEncryptor {
41    cipher: Sm4,
42}
43
44impl Sm4GcmEncryptor {
45    pub const IDENTIFIER: &'static str = "sm4-gcm";
46
47    pub fn new(key: [u8; 16]) -> Self {
48        Self {
49            cipher: sm4_cipher(&key),
50        }
51    }
52
53    pub fn from_slice(key: &[u8]) -> Result<Self> {
54        // 借 Key<N> 做长度校验;临时副本随其 Drop 清零。
55        let key = Key::<16>::from_slice(key, LABEL)?;
56        Ok(Self {
57            cipher: sm4_cipher(key.as_bytes()),
58        })
59    }
60
61    /// 固定 nonce 加密(仅测试用),返回与 [`SymmetricCipher::encrypt`] 相同的整帧。
62    #[cfg(test)]
63    pub(crate) fn encrypt_with_nonce(&self, nonce: &[u8; NONCE_LEN], plaintext: &[u8]) -> Vec<u8> {
64        let (ciphertext, tag) = seal(&self.cipher, nonce, b"", plaintext);
65        pack(nonce, &tag, &ciphertext)
66    }
67}
68
69impl Identified for Sm4GcmEncryptor {
70    fn identifier(&self) -> &str {
71        Self::IDENTIFIER
72    }
73}
74
75impl SymmetricCipher for Sm4GcmEncryptor {
76    fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
77        let mut nonce = [0u8; NONCE_LEN];
78        OsRng.fill_bytes(&mut nonce);
79
80        let (ciphertext, tag) = seal(&self.cipher, &nonce, b"", plaintext);
81        Ok(pack(&nonce, &tag, &ciphertext))
82    }
83
84    fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
85        let body = ciphertext
86            .strip_prefix(PREFIX)
87            .ok_or(Error::InvalidPrefix { label: LABEL })?;
88        if body.len() < NONCE_LEN + TAG_LEN {
89            return Err(Error::TooShort { label: LABEL });
90        }
91
92        let (nonce, rest) = body.split_at(NONCE_LEN);
93        let (tag, ct) = rest.split_at(TAG_LEN);
94        let nonce: &[u8; NONCE_LEN] = nonce.try_into().expect("length was verified");
95        let tag: &[u8; TAG_LEN] = tag.try_into().expect("length was verified");
96
97        open(&self.cipher, nonce, b"", ct, tag).ok_or(Error::DecryptionFailed { label: LABEL })
98    }
99}
100
101/// 整帧组装:`v1 | nonce(12) | tag(16) | 密文`。
102fn pack(nonce: &[u8; NONCE_LEN], tag: &[u8; TAG_LEN], ciphertext: &[u8]) -> Vec<u8> {
103    let mut blob = Vec::with_capacity(PREFIX.len() + NONCE_LEN + TAG_LEN + ciphertext.len());
104    blob.extend_from_slice(PREFIX);
105    blob.extend_from_slice(nonce);
106    blob.extend_from_slice(tag);
107    blob.extend_from_slice(ciphertext);
108    blob
109}
110
111/// SM4 实例;密钥长度在构造侧已保证为 16 字节。
112fn sm4_cipher(key: &[u8; 16]) -> Sm4 {
113    Sm4::new_from_slice(key).expect("SM4 key length is fixed at 16")
114}
115
116/// `E_K(J0)`:tag 的掩码(`T = E_K(J0) ⊕ S`)。J0 = nonce‖0^31‖1。
117fn j0_mask(cipher: &Sm4, nonce: &[u8; NONCE_LEN]) -> [u8; 16] {
118    let mut j0 = [0u8; 16];
119    j0[..NONCE_LEN].copy_from_slice(nonce);
120    j0[15] = 1;
121    cipher.encrypt_block(Block::<Sm4>::from_mut_slice(&mut j0));
122    j0
123}
124
125/// CTR 起始计数器块 `inc32(J0)`:J0 的 32 位计数字为 1,GCTR 自其 +1 起。
126fn ctr_start_block(nonce: &[u8; NONCE_LEN]) -> [u8; 16] {
127    let mut block = [0u8; 16];
128    block[..NONCE_LEN].copy_from_slice(nonce);
129    block[15] = 2; // inc32(J0)
130    block
131}
132
133/// `inc32(J0)` 起点的 CTR 密钥流:把缓存实例的轮密钥克隆给 CTR 核,
134/// 不再重跑一遍 SM4 密钥编排(克隆只是复制已展开的轮密钥)。
135fn ctr_keystream(cipher: &Sm4, nonce: &[u8; NONCE_LEN]) -> Ctr32BE<Sm4> {
136    let counter = CtrCore::inner_iv_slice_init(cipher.clone(), &ctr_start_block(nonce))
137        .expect("counter block length is fixed at 16");
138    Ctr32BE::from_core(counter)
139}
140
141/// GHASH 状态:喂入 A(零填充)、C(零填充)与 128 位长度块
142/// (`[len(A)]64‖[len(C)]64`,比特数,大端)。
143fn ghash_state(cipher: &Sm4, aad: &[u8], ciphertext: &[u8]) -> GHash {
144    let mut h = ghash::Key::default();
145    cipher.encrypt_block(&mut h);
146
147    let mut state = GHash::new(&h);
148    state.update_padded(aad);
149    state.update_padded(ciphertext);
150
151    let mut lengths = [0u8; 16];
152    lengths[..8].copy_from_slice(&(8 * aad.len() as u64).to_be_bytes());
153    lengths[8..].copy_from_slice(&(8 * ciphertext.len() as u64).to_be_bytes());
154    state.update(&[lengths.into()]);
155    state
156}
157
158/// GCM-AEK(GB/T 38636-2020 附录 A 算法 4):返回(密文, tag)。
159fn seal(
160    cipher: &Sm4,
161    nonce: &[u8; NONCE_LEN],
162    aad: &[u8],
163    plaintext: &[u8],
164) -> (Vec<u8>, [u8; TAG_LEN]) {
165    // ponytail: apply_keystream 在密钥流耗尽(约 2^36 字节)时 panic;能驻留
166    // 内存的单次载荷远达不到该上限,真要分片加密时改走 try_apply_keystream。
167    let mut ciphertext = plaintext.to_vec();
168    ctr_keystream(cipher, nonce).apply_keystream(&mut ciphertext);
169
170    let mask = j0_mask(cipher, nonce);
171    let s = ghash_state(cipher, aad, &ciphertext).finalize();
172    let mut tag = [0u8; TAG_LEN];
173    for ((byte, m), s) in tag.iter_mut().zip(mask.iter()).zip(s.iter()) {
174        *byte = m ^ s;
175    }
176    (ciphertext, tag)
177}
178
179/// GCM-ADK(GB/T 38636-2020 附录 A 算法 5):tag 不符返回 `None`
180/// (恒时比较,不向前端泄露「哪一步错了」)。先验 tag,通过后才解密。
181fn open(
182    cipher: &Sm4,
183    nonce: &[u8; NONCE_LEN],
184    aad: &[u8],
185    ciphertext: &[u8],
186    tag: &[u8; TAG_LEN],
187) -> Option<Vec<u8>> {
188    // 期望的 GHASH 输出 S = T ⊕ E_K(J0)(由 T = E_K(J0) ⊕ S 移项)。
189    let mask = j0_mask(cipher, nonce);
190    let mut expected = [0u8; TAG_LEN];
191    for ((byte, t), m) in expected.iter_mut().zip(tag.iter()).zip(mask.iter()) {
192        *byte = t ^ m;
193    }
194    ghash_state(cipher, aad, ciphertext)
195        .verify(Block::<GHash>::from_slice(&expected))
196        .ok()?;
197
198    let mut plaintext = ciphertext.to_vec();
199    ctr_keystream(cipher, nonce).apply_keystream(&mut plaintext);
200    Some(plaintext)
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    /// 十六进制常量 → 定长数组(逐字节对齐 KAT 用)。
208    fn hex_array<const N: usize>(hex: &str) -> [u8; N] {
209        crate::internal::hex_decode(hex)
210            .unwrap()
211            .try_into()
212            .unwrap()
213    }
214
215    /// RFC 8998 附录 A.1 的 SM4-GCM 向量(IETF,权威):
216    /// <https://www.rfc-editor.org/rfc/rfc8998.txt>。GB/T 38636-2020 附录 A
217    /// 给出同一组装(A.1.3.5 GCTR、A.1.4.1 GCM-AEK),但不含数值向量。
218    const KAT_KEY: &str = "0123456789abcdeffedcba9876543210";
219    const KAT_NONCE: &str = "00001234567800000000abcd";
220    const KAT_PLAINTEXT: &str = concat!(
221        "AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB",
222        "CCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDD",
223        "EEEEEEEEEEEEEEEEFFFFFFFFFFFFFFFF",
224        "EEEEEEEEEEEEEEEEAAAAAAAAAAAAAAAA"
225    );
226    const KAT_AAD: &str = "FEEDFACEDEADBEEFFEEDFACEDEADBEEFABADDAD2";
227    const KAT_CIPHERTEXT: &str = concat!(
228        "17F399F08C67D5EE19D0DC9969C4BB7D",
229        "5FD46FD3756489069157B282BB200735",
230        "D82710CA5C22F0CCFA7CBF93D496AC15",
231        "A56834CBCF98C397B4024A2691233B8D"
232    );
233    const KAT_TAG: &str = "83DE3541E4C2B58177E065A9BF7B62EC";
234
235    #[test]
236    fn rfc8998_kat_with_aad_is_byte_exact() {
237        // 完整向量(含 20 字节 AAD):密文与 tag 都要逐字节一致。
238        let key: [u8; 16] = hex_array(KAT_KEY);
239        let nonce: [u8; 12] = hex_array(KAT_NONCE);
240        let plaintext: [u8; 64] = hex_array(KAT_PLAINTEXT);
241        let aad: [u8; 20] = hex_array(KAT_AAD);
242        let expected_ciphertext: [u8; 64] = hex_array(KAT_CIPHERTEXT);
243        let expected_tag: [u8; 16] = hex_array(KAT_TAG);
244        // seal / open 改为吃缓存好的 SM4 实例(与生产路径一致)。
245        let cipher = sm4_cipher(&key);
246
247        let (ciphertext, tag) = seal(&cipher, &nonce, &aad, &plaintext);
248        assert_eq!(ciphertext, expected_ciphertext);
249        assert_eq!(tag, expected_tag);
250
251        // 解密侧同样对齐:正确 tag 通过、改一位即拒绝。
252        let plaintext_back = open(&cipher, &nonce, &aad, &ciphertext, &tag).unwrap();
253        assert_eq!(plaintext_back, plaintext);
254
255        let mut tampered = expected_tag;
256        tampered[0] ^= 0x01;
257        assert_eq!(open(&cipher, &nonce, &aad, &ciphertext, &tampered), None);
258    }
259
260    #[test]
261    fn rfc8998_ciphertext_matches_with_empty_aad() {
262        // AAD 不参与 GCTR:本库公开 API 约定(空 AAD)产出的密文应与
263        // RFC 8998 向量逐字节一致;tag 因 AAD 不同而不同,不做数值断言,
264        // 改用「同帧能自洽解密」验证。
265        let encryptor = Sm4GcmEncryptor::new(hex_array(KAT_KEY));
266        let nonce: [u8; 12] = hex_array(KAT_NONCE);
267        let plaintext: [u8; 64] = hex_array(KAT_PLAINTEXT);
268        let expected_ciphertext: [u8; 64] = hex_array(KAT_CIPHERTEXT);
269
270        let frame = encryptor.encrypt_with_nonce(&nonce, &plaintext);
271        assert_eq!(&frame[..2], PREFIX);
272        assert_eq!(&frame[2..14], &nonce);
273        assert_eq!(&frame[30..], &expected_ciphertext[..]);
274        assert_eq!(encryptor.decrypt(&frame).unwrap(), plaintext);
275    }
276
277    #[test]
278    fn roundtrip_and_tamper_detection() {
279        let encryptor = Sm4GcmEncryptor::new([7u8; 16]);
280        for plaintext in [
281            b"".as_slice(),
282            b"plain",
283            b"0123456789abcdef",
284            &[0u8; 64][..],
285        ] {
286            let blob = encryptor.encrypt(plaintext).unwrap();
287            assert_eq!(blob.len(), 2 + NONCE_LEN + TAG_LEN + plaintext.len());
288            assert_eq!(encryptor.decrypt(&blob).unwrap(), plaintext);
289        }
290
291        // 密文被改。
292        let mut blob = encryptor.encrypt(b"data").unwrap();
293        let last = blob.len() - 1;
294        blob[last] ^= 0x01;
295        assert_eq!(
296            encryptor.decrypt(&blob).unwrap_err(),
297            Error::DecryptionFailed { label: LABEL }
298        );
299
300        // tag 被改。
301        let mut blob = encryptor.encrypt(b"data").unwrap();
302        blob[2 + NONCE_LEN] ^= 0x01;
303        assert_eq!(
304            encryptor.decrypt(&blob).unwrap_err(),
305            Error::DecryptionFailed { label: LABEL }
306        );
307    }
308
309    #[test]
310    fn every_encrypt_uses_a_fresh_nonce() {
311        let encryptor = Sm4GcmEncryptor::new([7u8; 16]);
312        let first = encryptor.encrypt(b"same").unwrap();
313        let second = encryptor.encrypt(b"same").unwrap();
314        assert_ne!(first, second);
315        assert_ne!(first[2..14], second[2..14]);
316    }
317
318    #[test]
319    fn wrong_key_and_bad_frame_fail() {
320        let blob = Sm4GcmEncryptor::new([7u8; 16]).encrypt(b"x").unwrap();
321        let other = Sm4GcmEncryptor::new([8u8; 16]);
322        assert_eq!(
323            other.decrypt(&blob).unwrap_err(),
324            Error::DecryptionFailed { label: LABEL }
325        );
326        assert_eq!(
327            other.decrypt(b"v9....").unwrap_err(),
328            Error::InvalidPrefix { label: LABEL }
329        );
330        assert_eq!(
331            other.decrypt(b"v1short").unwrap_err(),
332            Error::TooShort { label: LABEL }
333        );
334    }
335
336    #[test]
337    fn key_length_is_checked() {
338        assert_eq!(
339            Sm4GcmEncryptor::from_slice(&[0u8; 32]).unwrap_err(),
340            Error::InvalidKeyLength {
341                label: LABEL,
342                expected: 16,
343                got: 32
344            }
345        );
346    }
347
348    #[test]
349    fn identifier_matches_registered_name() {
350        assert_eq!(Sm4GcmEncryptor::new([0u8; 16]).identifier(), "sm4-gcm");
351    }
352}