Skip to main content

gmcrypto_core/
sm3.rs

1//! SM3 hash function (GB/T 32905-2016).
2//!
3//! 256-bit Merkle-Damgård hash. Block size 512 bits, output 256 bits.
4
5use core::convert::TryInto;
6
7const IV: [u32; 8] = [
8    0x7380_166f,
9    0x4914_b2b9,
10    0x1724_42d7,
11    0xda8a_0600,
12    0xa96f_30bc,
13    0x1631_38aa,
14    0xe38d_ee4d,
15    0xb0fb_0e4e,
16];
17
18const T_J_LOW: u32 = 0x79cc_4519; // T_j for 0..=15
19const T_J_HIGH: u32 = 0x7a87_9d8a; // T_j for 16..=63
20
21/// SM3 digest output size in bytes (32 = 256 bits).
22pub const DIGEST_SIZE: usize = 32;
23
24/// Internal block size in bytes (64 = 512 bits).
25pub const BLOCK_SIZE: usize = 64;
26
27/// One-shot SM3 hash. Returns the 32-byte digest.
28#[must_use]
29pub fn hash(message: &[u8]) -> [u8; DIGEST_SIZE] {
30    let mut hasher = Sm3::new();
31    hasher.update(message);
32    hasher.finalize()
33}
34
35/// Streaming SM3 hasher.
36#[derive(Clone, Debug)]
37pub struct Sm3 {
38    state: [u32; 8],
39    buffer: [u8; BLOCK_SIZE],
40    buffer_len: usize,
41    total_len: u64, // total input bytes; SM3 length field is 64 bits big-endian
42}
43
44impl Default for Sm3 {
45    fn default() -> Self {
46        Self::new()
47    }
48}
49
50impl Sm3 {
51    /// Create a fresh SM3 hasher.
52    #[must_use]
53    pub const fn new() -> Self {
54        Self {
55            state: IV,
56            buffer: [0u8; BLOCK_SIZE],
57            buffer_len: 0,
58            total_len: 0,
59        }
60    }
61
62    /// Absorb input bytes.
63    #[allow(clippy::missing_panics_doc)]
64    pub fn update(&mut self, mut data: &[u8]) {
65        self.total_len = self.total_len.wrapping_add(data.len() as u64);
66
67        // Fill the partial buffer first if there is one.
68        if self.buffer_len > 0 {
69            let need = BLOCK_SIZE - self.buffer_len;
70            let take = need.min(data.len());
71            self.buffer[self.buffer_len..self.buffer_len + take].copy_from_slice(&data[..take]);
72            self.buffer_len += take;
73            data = &data[take..];
74            if self.buffer_len == BLOCK_SIZE {
75                let block = self.buffer;
76                compress(&mut self.state, &block);
77                self.buffer_len = 0;
78            }
79        }
80
81        // Process full blocks directly out of `data`.
82        while data.len() >= BLOCK_SIZE {
83            let (block, rest) = data.split_at(BLOCK_SIZE);
84            compress(
85                &mut self.state,
86                block.try_into().expect("BLOCK_SIZE-len slice"),
87            );
88            data = rest;
89        }
90
91        // Stash the trailing partial block.
92        if !data.is_empty() {
93            self.buffer[..data.len()].copy_from_slice(data);
94            self.buffer_len = data.len();
95        }
96    }
97
98    /// Produce the final digest, consuming the hasher.
99    #[must_use]
100    pub fn finalize(mut self) -> [u8; DIGEST_SIZE] {
101        // SM3 padding: append 0x80, then zeros, then 64-bit big-endian bit length.
102        let bit_len = self.total_len.wrapping_mul(8);
103        self.buffer[self.buffer_len] = 0x80;
104        self.buffer_len += 1;
105
106        if self.buffer_len > BLOCK_SIZE - 8 {
107            // Not enough room for the length field; flush this block, then a final
108            // block of zeros + length.
109            for byte in &mut self.buffer[self.buffer_len..] {
110                *byte = 0;
111            }
112            let block = self.buffer;
113            compress(&mut self.state, &block);
114            self.buffer = [0u8; BLOCK_SIZE];
115            self.buffer_len = 0;
116        }
117
118        for byte in &mut self.buffer[self.buffer_len..BLOCK_SIZE - 8] {
119            *byte = 0;
120        }
121        self.buffer[BLOCK_SIZE - 8..].copy_from_slice(&bit_len.to_be_bytes());
122        let block = self.buffer;
123        compress(&mut self.state, &block);
124
125        let mut out = [0u8; DIGEST_SIZE];
126        for (i, w) in self.state.iter().enumerate() {
127            out[i * 4..(i + 1) * 4].copy_from_slice(&w.to_be_bytes());
128        }
129        out
130    }
131}
132
133#[inline]
134const fn p0(x: u32) -> u32 {
135    x ^ x.rotate_left(9) ^ x.rotate_left(17)
136}
137#[inline]
138const fn p1(x: u32) -> u32 {
139    x ^ x.rotate_left(15) ^ x.rotate_left(23)
140}
141#[inline]
142const fn ff_low(x: u32, y: u32, z: u32) -> u32 {
143    x ^ y ^ z
144}
145#[inline]
146const fn ff_high(x: u32, y: u32, z: u32) -> u32 {
147    (x & y) | (x & z) | (y & z)
148}
149#[inline]
150const fn gg_low(x: u32, y: u32, z: u32) -> u32 {
151    x ^ y ^ z
152}
153#[inline]
154const fn gg_high(x: u32, y: u32, z: u32) -> u32 {
155    (x & y) | (!x & z)
156}
157
158#[allow(clippy::many_single_char_names)]
159fn compress(state: &mut [u32; 8], block: &[u8; BLOCK_SIZE]) {
160    // Message expansion: W[0..16] from block, W[16..68] derived, W'[0..64] = W[j] XOR W[j+4].
161    let mut w = [0u32; 68];
162    for j in 0..16 {
163        w[j] = u32::from_be_bytes(block[j * 4..(j + 1) * 4].try_into().expect("4-byte slice"));
164    }
165    for j in 16..68 {
166        w[j] = p1(w[j - 16] ^ w[j - 9] ^ w[j - 3].rotate_left(15))
167            ^ w[j - 13].rotate_left(7)
168            ^ w[j - 6];
169    }
170    let mut wp = [0u32; 64];
171    for j in 0..64 {
172        wp[j] = w[j] ^ w[j + 4];
173    }
174
175    let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h] = *state;
176
177    for j in 0..64 {
178        let t_j = if j < 16 { T_J_LOW } else { T_J_HIGH };
179        #[allow(clippy::cast_possible_truncation)]
180        let ss1 = a
181            .rotate_left(12)
182            .wrapping_add(e)
183            .wrapping_add(t_j.rotate_left((j % 32) as u32))
184            .rotate_left(7);
185        let ss2 = ss1 ^ a.rotate_left(12);
186        let (ff, gg) = if j < 16 {
187            (ff_low(a, b, c), gg_low(e, f, g))
188        } else {
189            (ff_high(a, b, c), gg_high(e, f, g))
190        };
191        let tt1 = ff.wrapping_add(d).wrapping_add(ss2).wrapping_add(wp[j]);
192        let tt2 = gg.wrapping_add(h).wrapping_add(ss1).wrapping_add(w[j]);
193        d = c;
194        c = b.rotate_left(9);
195        b = a;
196        a = tt1;
197        h = g;
198        g = f.rotate_left(19);
199        f = e;
200        e = p0(tt2);
201    }
202
203    state[0] ^= a;
204    state[1] ^= b;
205    state[2] ^= c;
206    state[3] ^= d;
207    state[4] ^= e;
208    state[5] ^= f;
209    state[6] ^= g;
210    state[7] ^= h;
211}
212
213#[cfg(feature = "digest-traits")]
214mod digest_impl {
215    //! `digest::Digest`-compatible impl for [`Sm3`] (v0.4 W2; Q4.3).
216    //!
217    //! Behind the `digest-traits` feature flag. Default-features build
218    //! does not pull `digest` into the dep graph.
219
220    use super::{DIGEST_SIZE, Sm3};
221    use digest::{
222        FixedOutput, FixedOutputReset, HashMarker, Output, OutputSizeUser, Reset, Update,
223        consts::U32,
224    };
225
226    impl HashMarker for Sm3 {}
227
228    impl OutputSizeUser for Sm3 {
229        type OutputSize = U32;
230    }
231
232    impl Update for Sm3 {
233        fn update(&mut self, data: &[u8]) {
234            Self::update(self, data);
235        }
236    }
237
238    impl FixedOutput for Sm3 {
239        fn finalize_into(self, out: &mut Output<Self>) {
240            let digest: [u8; DIGEST_SIZE] = Self::finalize(self);
241            out.copy_from_slice(&digest);
242        }
243    }
244
245    impl Reset for Sm3 {
246        fn reset(&mut self) {
247            *self = Self::new();
248        }
249    }
250
251    impl FixedOutputReset for Sm3 {
252        fn finalize_into_reset(&mut self, out: &mut Output<Self>) {
253            let mut taken = Self::new();
254            core::mem::swap(self, &mut taken);
255            let digest: [u8; DIGEST_SIZE] = Self::finalize(taken);
256            out.copy_from_slice(&digest);
257        }
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264    use hex_literal::hex;
265
266    /// GB/T 32905-2016 Appendix A.1 — empty input.
267    #[test]
268    fn hash_empty() {
269        assert_eq!(
270            hash(&[]),
271            hex!("1ab21d8355cfa17f8e61194831e81a8f22bec8c728fefb747ed035eb5082aa2b"),
272        );
273    }
274
275    /// GB/T 32905-2016 Appendix A.1 — input "abc".
276    #[test]
277    fn hash_abc() {
278        assert_eq!(
279            hash(b"abc"),
280            hex!("66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0"),
281        );
282    }
283
284    /// GB/T 32905-2016 Appendix A.2 — 64-byte input ("abcd" repeated 16 times).
285    #[test]
286    fn hash_sixteen_abcd() {
287        let input = b"abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd";
288        assert_eq!(
289            hash(input),
290            hex!("debe9ff92275b8a138604889c18e5a4d6fdb70e5387e5765293dcba39c0c5732"),
291        );
292    }
293
294    /// 63 zero bytes (just below block boundary). Source: gmssl CLI output.
295    #[test]
296    fn hash_sixty_three_zeroes() {
297        let zeroes = [0u8; 63];
298        assert_eq!(
299            hash(&zeroes),
300            hex!("5241dc10cb3c700e46446943d27b971fefa7e88115f866d6f83d502ff1bc06c2"),
301        );
302    }
303
304    /// Streaming API: feeding "ab" then "c" must equal one-shot hash("abc").
305    #[test]
306    fn streaming_matches_one_shot() {
307        let mut h = Sm3::new();
308        h.update(b"ab");
309        h.update(b"c");
310        assert_eq!(h.finalize(), hash(b"abc"));
311    }
312}