Skip to main content

gmcrypto_core/sm2/
encrypt.rs

1//! SM2 public-key encryption (GB/T 32918.4-2017 §6).
2//!
3//! # Algorithm
4//!
5//! ```text
6//! Input:  recipient public key P_B, plaintext M
7//! Output: ciphertext (C1 = kG, C3 = SM3(x2 || M || y2), C2 = M XOR KDF(x2 || y2, |M|))
8//!
9//! 1. Pick random k in [1, n-1]
10//! 2. C1 = kG = (x1, y1)
11//! 3. (x2, y2) = k * P_B
12//! 4. t = KDF(x2 || y2, |M| in bits)
13//! 5. If t is all zeros, retry from step 1 (negligible probability for non-empty M)
14//! 6. C2 = M XOR t
15//! 7. C3 = SM3(x2 || M || y2)
16//! 8. Output GM/T 0009 DER encoding of (x1, y1, C3, C2)
17//! ```
18//!
19//! # KDF (GB/T 32918.4 §5.4.3)
20//!
21//! SM3-based counter-mode key-derivation:
22//!
23//! ```text
24//! KDF(Z, klen):
25//!   ct = 1
26//!   while output length < klen:
27//!     output ||= SM3(Z || ct.to_be_bytes())
28//!     ct += 1
29//!   return output truncated to klen bits
30//! ```
31//!
32//! v0.2 places this KDF inside `sm2::encrypt` rather than the top-level
33//! `gmcrypto_core::kdf` module. `kdf.rs` is reserved for PBKDF2.
34//!
35//! # Failure-mode invariant
36//!
37//! [`encrypt`] returns `Result<Vec<u8>, EncryptError>` with a single
38//! `Failed` variant — collapses every retry-budget-exhausted, identity-
39//! point, or KDF-zero outcome to one uninformative shape. With a
40//! [`CryptoRng`], the cumulative-failure probability is `≤ 2^-512` per
41//! call across all plaintext lengths (1-byte through arbitrary), per
42//! the [`ENCRYPT_RETRY_BUDGET`] table — i.e. never observed in
43//! practice.
44//!
45//! # Constant-time stance
46//!
47//! Encrypt operates on the recipient's **public key** and a freshly
48//! sampled `k`; no caller-controlled secret is touched. The only
49//! secret-derived intermediates are `(x2, y2) = kP_B` and the KDF
50//! output, both of which are wiped before return. v0.2's dudect
51//! harness covers the secret-touching path on the **decrypt** side
52//! (`ct_sm2_decrypt`); a `ct_sm2_encrypt` target is optional and
53//! deferred until v0.3.
54
55use crate::asn1::ciphertext::{Sm2Ciphertext, encode};
56use crate::sm2::curve::{Fn, Fp, b};
57use crate::sm2::point::ProjectivePoint;
58use crate::sm2::public_key::Sm2PublicKey;
59use crate::sm2::scalar_mul::{mul_g, mul_var};
60use crate::sm2::sign::sample_nonzero_scalar;
61use crate::sm3::{DIGEST_SIZE, Sm3};
62use alloc::vec::Vec;
63use crypto_bigint::U256;
64use rand_core::{CryptoRng, Rng};
65use subtle::ConstantTimeEq;
66use zeroize::Zeroize;
67
68/// Retry budget for the KDF-zero rejection step.
69///
70/// **Per-iteration KDF-zero probability is length-dependent**, not the
71/// asymptotic `2^-256` figure that v0.2's first cut assumed. For a
72/// plaintext of `L` bytes the KDF output is `L` bytes long and
73/// `P(all-zero) = 2^(-8·L)`. For very short plaintexts the per-call
74/// probability is non-negligible:
75///
76/// | `|M|` (bytes) | per-iteration P(zero) | budget=4 P(fail) | budget=64 P(fail) |
77/// |---:|---:|---:|---:|
78/// | 1  | `2^-8`   | `2^-32`  | `2^-512`  |
79/// | 2  | `2^-16`  | `2^-64`  | `2^-1024` |
80/// | 4  | `2^-32`  | `2^-128` | `2^-2048` |
81/// | 32 | `2^-256` | `2^-1024`| `2^-16384`|
82///
83/// A budget of 64 makes the cumulative failure probability negligible
84/// at any plaintext length while keeping the loop bounded for liveness
85/// under degenerate RNGs. GB/T 32918.4 specifies the retry as
86/// indefinite; the 64-step bound is a defense-in-depth ceiling, never
87/// reached in practice with a uniform CSPRNG.
88const ENCRYPT_RETRY_BUDGET: usize = 64;
89
90/// Encrypt failure — single uninformative variant per the project's
91/// failure-mode invariant.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum EncryptError {
94    /// The retry budget was exhausted, or the recipient public key is
95    /// the identity point. Effectively unreachable with a uniform
96    /// CSPRNG and a real public key.
97    Failed,
98}
99
100/// Encrypt `plaintext` to recipient `public`, returning a GM/T 0009
101/// DER-encoded ciphertext.
102///
103/// `rng` must be a [`CryptoRng`]. With a CSPRNG, encrypt failure
104/// probability is `≤ 2^-512` for any plaintext length — see the
105/// [`ENCRYPT_RETRY_BUDGET`] table for the per-length math.
106///
107/// # Errors
108///
109/// Returns [`EncryptError::Failed`] if the recipient public key is the
110/// identity point (a malicious caller could construct one via
111/// [`Sm2PublicKey::from_point`]) or if every retry produced an
112/// all-zeros KDF output.
113pub fn encrypt<R: CryptoRng + Rng>(
114    public: &Sm2PublicKey,
115    plaintext: &[u8],
116    rng: &mut R,
117) -> Result<Vec<u8>, EncryptError> {
118    if bool::from(public.point().is_identity()) {
119        return Err(EncryptError::Failed);
120    }
121    for _ in 0..ENCRYPT_RETRY_BUDGET {
122        let k = sample_nonzero_scalar(rng);
123        if let Some(ct) = try_encrypt_once(public, plaintext, &k) {
124            return Ok(encode(&ct));
125        }
126    }
127    Err(EncryptError::Failed)
128}
129
130/// Single encrypt attempt. Returns `None` when the KDF output is
131/// all-zeros (caller retries with a fresh `k`).
132fn try_encrypt_once(public: &Sm2PublicKey, plaintext: &[u8], k: &Fn) -> Option<Sm2Ciphertext> {
133    // C1 = kG; (x1, y1) = affine(C1)
134    let c1 = mul_g(k);
135    let (x1, y1) = c1.to_affine()?;
136
137    // (x2, y2) = k * P_B; affine
138    let kp = mul_var(k, &public.point());
139    let (x2, y2) = kp.to_affine()?;
140
141    // Z = x2 || y2 (64 bytes), the KDF input.
142    let mut z = [0u8; 64];
143    z[..32].copy_from_slice(&x2.retrieve().to_be_bytes());
144    z[32..].copy_from_slice(&y2.retrieve().to_be_bytes());
145
146    // t = KDF(Z, |plaintext|)
147    let mut t = alloc::vec![0u8; plaintext.len()];
148    kdf(&z, &mut t);
149
150    // KDF-zero rejection: spec requires retry on all-zeros KDF output.
151    // Vacuously satisfied for empty plaintext (no output bytes to check).
152    if !plaintext.is_empty() && all_zero_ct(&t) {
153        // Wipe the all-zero buffer (defensive; it carries no secret
154        // since it's all zeros, but the KDF input Z is secret-derived).
155        z.zeroize();
156        t.zeroize();
157        return None;
158    }
159
160    // C2 = M XOR t (in place, reusing the t buffer).
161    for (i, byte) in plaintext.iter().enumerate() {
162        t[i] ^= byte;
163    }
164    let c2 = t; // rename: it now holds C2.
165
166    // C3 = SM3(x2 || M || y2)
167    let mut h = Sm3::new();
168    h.update(&z[..32]);
169    h.update(plaintext);
170    h.update(&z[32..]);
171    let c3 = h.finalize();
172
173    // Wipe the secret-derived (x2 || y2) buffer.
174    z.zeroize();
175
176    Some(Sm2Ciphertext {
177        x: x1.retrieve(),
178        y: y1.retrieve(),
179        hash: c3,
180        ciphertext: c2,
181    })
182}
183
184/// SM3 counter-mode KDF per GB/T 32918.4 §5.4.3.
185///
186/// Writes `output.len()` bytes of derived material into `output` from
187/// the input `z`. `output` may be any length, including empty (in
188/// which case the function is a no-op).
189///
190/// Visible to `sm2::decrypt` via `pub(super)`; not part of the public
191/// API and not SemVer-stable.
192pub(super) fn kdf(z: &[u8], output: &mut [u8]) {
193    let mut counter: u32 = 1;
194    let mut written = 0;
195    while written < output.len() {
196        let mut h = Sm3::new();
197        h.update(z);
198        h.update(&counter.to_be_bytes());
199        let digest = h.finalize();
200        let block_remaining = output.len() - written;
201        let copy_len = block_remaining.min(DIGEST_SIZE);
202        output[written..written + copy_len].copy_from_slice(&digest[..copy_len]);
203        written += copy_len;
204        counter += 1;
205    }
206}
207
208/// Constant-time all-zero test: `acc |= byte` over the whole buffer,
209/// then check `acc == 0`. The final equality is on a non-secret
210/// summary value (the OR of all bytes), so the bool result is the
211/// only timing signal — and that signal is the "is the KDF output
212/// all-zero?" question, which is itself an explicit spec-mandated
213/// branch (the retry).
214fn all_zero_ct(buf: &[u8]) -> bool {
215    let mut acc: u8 = 0;
216    for b in buf {
217        acc |= b;
218    }
219    bool::from(acc.ct_eq(&0u8))
220}
221
222/// Validate that `(x, y)` lies on the SM2 curve `y² ≡ x³ - 3x + b
223/// (mod p)`. Defense against invalid-curve attacks on `decrypt` —
224/// without this check, an attacker submitting `C1` on a different
225/// curve could leak bits of the recipient's private key via
226/// `d_B * C1`.
227///
228/// Visible to `sm2::decrypt`; not part of the public API.
229pub(super) fn point_on_curve(x: &Fp, y: &Fp) -> bool {
230    let three = Fp::new(&U256::from_u64(3));
231    let lhs = *y * *y;
232    let rhs = (*x) * (*x) * (*x) - three * (*x) + b();
233    bool::from(lhs.retrieve().ct_eq(&rhs.retrieve()))
234}
235
236/// Construct a [`ProjectivePoint`] from validated affine `(x, y)`
237/// coordinates. Visible to `sm2::decrypt`; not part of the public API.
238pub(super) const fn projective_from_affine(x: Fp, y: Fp) -> ProjectivePoint {
239    ProjectivePoint {
240        x,
241        y,
242        z: Fp::new(&U256::ONE),
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249    use crate::sm2::private_key::Sm2PrivateKey;
250    use core::convert::Infallible;
251    use rand_core::{TryCryptoRng, TryRng};
252
253    /// Test-only RNG that emits a fixed 32-byte value on every
254    /// `fill_bytes` call. Used to drive `encrypt` with a known `k` for
255    /// KAT-style tests.
256    struct FixedScalarRng {
257        bytes: [u8; 32],
258    }
259
260    impl FixedScalarRng {
261        const fn new(bytes: [u8; 32]) -> Self {
262            Self { bytes }
263        }
264    }
265
266    impl TryRng for FixedScalarRng {
267        type Error = Infallible;
268
269        fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
270            Ok(0)
271        }
272        fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
273            Ok(0)
274        }
275        fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
276            assert_eq!(dst.len(), 32);
277            dst.copy_from_slice(&self.bytes);
278            Ok(())
279        }
280    }
281
282    impl TryCryptoRng for FixedScalarRng {}
283
284    /// Build a deterministic 64-byte test `Z` for the KDF cross-checks.
285    /// Content doesn't matter — the goal is exact-length, reproducible bytes.
286    fn synthetic_z() -> [u8; 64] {
287        let mut z = [0u8; 64];
288        for (i, b) in z.iter_mut().enumerate() {
289            #[allow(clippy::cast_possible_truncation)]
290            {
291                *b = (i as u8).wrapping_mul(7);
292            }
293        }
294        z
295    }
296
297    /// Single-block KDF cross-check: 32-byte output equals
298    /// `SM3(z || 0x00000001)`.
299    #[test]
300    fn kdf_single_block_matches_manual_sm3() {
301        let z = synthetic_z();
302        let mut out = [0u8; 32];
303        kdf(&z, &mut out);
304
305        let mut h = Sm3::new();
306        h.update(&z);
307        h.update(&1u32.to_be_bytes());
308        let expected = h.finalize();
309        assert_eq!(out, expected);
310    }
311
312    /// Two-block KDF cross-check: 40 bytes spans two SM3 invocations
313    /// with `ct = 1` then `ct = 2`.
314    #[test]
315    fn kdf_two_block_matches_manual_sm3() {
316        let z = synthetic_z();
317        let mut out = [0u8; 40];
318        kdf(&z, &mut out);
319
320        let mut h1 = Sm3::new();
321        h1.update(&z);
322        h1.update(&1u32.to_be_bytes());
323        let block1 = h1.finalize();
324        let mut h2 = Sm3::new();
325        h2.update(&z);
326        h2.update(&2u32.to_be_bytes());
327        let block2 = h2.finalize();
328
329        assert_eq!(&out[..32], &block1);
330        assert_eq!(&out[32..40], &block2[..8]);
331    }
332
333    /// Empty-output KDF is a no-op.
334    #[test]
335    fn kdf_empty_output_is_noop() {
336        let z = b"whatever";
337        let mut out: [u8; 0] = [];
338        kdf(z, &mut out);
339        // (no assertion needed — just verifying it doesn't panic and
340        // returns a 0-length output)
341    }
342
343    /// `point_on_curve` accepts the SM2 generator `G`.
344    #[test]
345    fn point_on_curve_accepts_generator() {
346        let g = ProjectivePoint::generator();
347        let (gx, gy) = g.to_affine().expect("G is finite");
348        assert!(point_on_curve(&gx, &gy));
349    }
350
351    /// `point_on_curve` rejects an arbitrary off-curve point.
352    #[test]
353    fn point_on_curve_rejects_off_curve() {
354        // `(1, 1)` is almost certainly not on SM2 (overwhelmingly
355        // likely false; cross-checking the on-curve guard is the
356        // point of the test).
357        let x = Fp::new(&U256::ONE);
358        let y = Fp::new(&U256::ONE);
359        assert!(!point_on_curve(&x, &y));
360    }
361
362    /// Encrypt rejects an identity-point public key. (Same-style
363    /// hardening as `verify_with_id`'s identity-rejection from v0.1.)
364    #[test]
365    fn encrypt_rejects_identity_pubkey() {
366        let pk = Sm2PublicKey::from_point(ProjectivePoint::identity());
367        let mut rng = rand_core::UnwrapErr(getrandom::SysRng);
368        assert_eq!(
369            encrypt(&pk, b"any plaintext", &mut rng),
370            Err(EncryptError::Failed)
371        );
372    }
373
374    /// Fixed-`k` smoke test: encrypt with a deterministic RNG produces
375    /// a deterministic ciphertext (round-trip is in `sm2::decrypt`'s
376    /// tests).
377    #[test]
378    fn encrypt_with_fixed_k_is_deterministic() {
379        let d =
380            U256::from_be_hex("1649AB77A00637BD5E2EFE283FBF353534AA7F7CB89463F208DDBC2920BB0DA0");
381        let key = Sm2PrivateKey::new(d).expect("valid d");
382        let pk = Sm2PublicKey::from_point(key.public_key());
383        let k_bytes =
384            U256::from_be_hex("4C62EEFD6ECFC2B95B92FD6C3D9575148AFA17425546D49018E5388D49DD7B4F")
385                .to_be_bytes();
386        let mut bytes = [0u8; 32];
387        bytes.copy_from_slice(&k_bytes);
388        let mut rng_a = rand_core::UnwrapErr(FixedScalarRng::new(bytes));
389        let mut rng_b = rand_core::UnwrapErr(FixedScalarRng::new(bytes));
390        let der_a = encrypt(&pk, b"encryption standard", &mut rng_a).expect("encrypt a");
391        let der_b = encrypt(&pk, b"encryption standard", &mut rng_b).expect("encrypt b");
392        assert_eq!(der_a, der_b, "fixed-k encrypt must be deterministic");
393    }
394}