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