Skip to main content

gmcrypto_c/
lib.rs

1//! C ABI for `gmcrypto-core` (v0.4 W4).
2//!
3//! Exposes SM2 (sign/verify, encrypt/decrypt, and — since v1.2 — the
4//! GM/T 0003.3 key exchange with key confirmation) / SM3 / SM4
5//! (ECB/CBC/CTR/GCM/CCM/XTS) / HMAC-SM3 / PBKDF2-HMAC-SM3 plus SM2 key
6//! import/export to C / C++ / Python / Go / Zig / Ruby callers via
7//! opaque handles and a cbindgen-generated header at
8//! `include/gmcrypto.h`.
9//!
10//! # Failure-mode invariant
11//!
12//! Every entry point returning `c_int` uses the convention:
13//!
14//! - `0` = success.
15//! - Non-zero = failure (single-`Failed`-equivalent per the workspace
16//!   failure-mode invariant; **no enumerated error codes**).
17//!
18//! C callers MUST treat all non-zero returns as opaque failure. Per
19//! Q4.8 in `docs/v0.4-scope.md`, distinguishing failure modes would
20//! introduce a padding-oracle / wrong-password-oracle attack surface.
21//!
22//! # Output buffer convention
23//!
24//! Entry points emitting variable-length output (signatures,
25//! ciphertexts, PKCS#8 blobs) follow the
26//! `(out_ptr, out_capacity, out_actual_len)` shape per Q4.13:
27//!
28//! - `out_ptr`: caller-allocated buffer.
29//! - `out_capacity`: buffer length in bytes.
30//! - `out_actual_len`: pointer to a `size_t` where the entry point
31//!   writes the actual output length (or the required capacity if
32//!   the buffer was too small).
33//!
34//! On too-small buffer: return non-zero, write the required length
35//! to `*out_actual_len`, do not modify `out_ptr`.
36//!
37//! # Handle ownership
38//!
39//! Opaque handles are heap-allocated `Box<T>`s returned as
40//! `*mut T_t`. Callers MUST pair each `_new` with exactly one
41//! `_free` to avoid leaks. Double-free or use-after-free is
42//! undefined behaviour (per `Box::from_raw`'s contract). Calling
43//! `_free(NULL)` is a no-op (mirrors C's `free()` semantics).
44//!
45//! # Constant-time
46//!
47//! The FFI shim itself does not introduce new secret-touching paths.
48//! Every cryptographic operation runs in `gmcrypto-core`'s already-
49//! dudect-gated code. The null-pointer check at each entry point
50//! is constant-time (single integer compare); the return-on-null
51//! early-exit has a different timing signature than a successful
52//! call, but the attacker who could measure this is local-host and
53//! has far more invasive options.
54//!
55//! # Panic discipline
56//!
57//! Every entry point wraps its body in `std::panic::catch_unwind`.
58//! Rust panics unwinding into C are undefined behaviour; on panic
59//! we convert to a non-zero return. Per the failure-mode invariant,
60//! the C caller cannot distinguish panic from other failure modes.
61
62#![warn(missing_docs)]
63#![allow(clippy::missing_safety_doc)]
64// C consumers expect snake_case-named opaque struct types
65// (`gmcrypto_sm3_t`, `gmcrypto_sm2_privkey_t`, ...); the Rust
66// convention warning is suppressed crate-wide for these.
67#![allow(non_camel_case_types)]
68// v0.4 W4 / Q4.7 — this is the FFI shim crate; raw-pointer
69// dereferencing and `Box::from_raw` are inherent. Every `unsafe`
70// block carries a `// SAFETY:` comment naming the caller-side
71// preconditions; the Cargo.toml lint `unsafe_code = "warn"` flags
72// any new `unsafe` for reviewer attention rather than blocking
73// compile. `gmcrypto-core` itself stays `unsafe_code = "forbid"`.
74#![allow(unsafe_code)]
75
76use core::ffi::{c_char, c_int, c_void};
77use core::ptr;
78use core::slice;
79
80use gmcrypto_core::asn1::ciphertext::{
81    decode as ciphertext_der_decode, encode as ciphertext_der_encode,
82};
83use gmcrypto_core::hmac::{HmacSm3 as InnerHmacSm3, hmac_sm3};
84use gmcrypto_core::kdf::pbkdf2_hmac_sm3;
85use gmcrypto_core::sm2::raw_ciphertext::{decode_c1c2c3_legacy, decode_c1c3c2, encode_c1c3c2};
86use gmcrypto_core::sm2::{
87    DEFAULT_SIGNER_ID, Sm2PrivateKey, Sm2PublicKey, decrypt as sm2_decrypt, encrypt as sm2_encrypt,
88    sign_with_id, verify_with_id,
89};
90// v1.2 — SM2 key-exchange (GM/T 0003.3) FFI. Always compiled into the
91// C ABI (the v0.23 W3 posture: `sm2-key-exchange` is enabled
92// unconditionally on the core dep — see Cargo.toml).
93use gmcrypto_core::sm2::key_exchange::{
94    Sm2KxConfirm, Sm2KxEphemeralPoint, Sm2KxInitiator as InnerSm2KxInitiator,
95    Sm2KxInitiatorWaiting as InnerSm2KxInitiatorWaiting, Sm2KxResponder as InnerSm2KxResponder,
96    Sm2KxResponderWaiting as InnerSm2KxResponderWaiting,
97};
98use gmcrypto_core::sm3::{Sm3 as InnerSm3, hash as sm3_hash};
99use gmcrypto_core::sm4::{
100    Sm4CbcDecryptor as InnerSm4CbcDec, Sm4CbcEncryptor as InnerSm4CbcEnc, Sm4Cipher, mode_cbc,
101};
102// v0.9 W4 — single-shot AEAD (SM4-GCM / SM4-CCM) FFI. v0.23 W3 (A-1):
103// always compiled into the C ABI (the forwarding `sm4-aead` feature was
104// removed; `gmcrypto-core` is pulled with `sm4-aead` always enabled).
105use gmcrypto_core::sm4::{
106    GcmTagLen, Sm4GcmDecryptor as InnerSm4GcmDec, Sm4GcmEncryptor as InnerSm4GcmEnc, mode_ccm,
107    mode_gcm,
108};
109// v0.13 — single-shot SM4-XTS FFI. v0.23 W3 (A-1): always compiled into
110// the C ABI (the forwarding `sm4-xts` feature was removed).
111use gmcrypto_core::sm4::mode_xts;
112use gmcrypto_core::{pem, pkcs8};
113use rand_core::TryRng;
114
115// ============================================================
116// Constants exported to the C side.
117// ============================================================
118
119/// Success return code.
120pub const GMCRYPTO_OK: c_int = 0;
121
122/// Generic failure return code. All non-zero returns are equivalent
123/// per the failure-mode invariant; this constant exists only as a
124/// convenience for C callers that want a named symbol for the
125/// not-success case.
126pub const GMCRYPTO_ERR: c_int = -1;
127
128/// SM3 digest output size in bytes (32 = 256 bits).
129pub const GMCRYPTO_SM3_DIGEST_SIZE: usize = 32;
130
131/// SM4 block size in bytes (16 = 128 bits).
132pub const GMCRYPTO_SM4_BLOCK_SIZE: usize = 16;
133
134/// SM4 key size in bytes (16 = 128 bits).
135pub const GMCRYPTO_SM4_KEY_SIZE: usize = 16;
136
137/// SM4-XTS key size in bytes (32 = `Key1 ‖ Key2`, two 128-bit keys).
138pub const GMCRYPTO_SM4_XTS_KEY_SIZE: usize = 2 * GMCRYPTO_SM4_KEY_SIZE;
139
140/// SEC1 uncompressed-point size for SM2 public keys
141/// (`04 || X || Y` = 65 bytes).
142pub const GMCRYPTO_SM2_SEC1_UNCOMPRESSED_SIZE: usize = 65;
143
144/// SM2 private-key scalar size in bytes (32 = 256 bits big-endian).
145pub const GMCRYPTO_SM2_SCALAR_SIZE: usize = 32;
146
147/// SM2 key-exchange confirmation-tag size in bytes (`S_A` / `S_B` are
148/// SM3 digests; v1.2). Ephemeral points `R_A` / `R_B` use
149/// [`GMCRYPTO_SM2_SEC1_UNCOMPRESSED_SIZE`] (65).
150pub const GMCRYPTO_SM2_KX_CONFIRM_SIZE: usize = 32;
151
152// ============================================================
153// Opaque handle types (cbindgen emits as forward-declared structs).
154// ============================================================
155
156/// Opaque handle for a streaming SM3 hasher.
157pub struct gmcrypto_sm3_t {
158    inner: InnerSm3,
159}
160
161/// Opaque handle for a streaming HMAC-SM3 keyed MAC.
162pub struct gmcrypto_hmac_sm3_t {
163    inner: InnerHmacSm3,
164}
165
166/// Opaque handle for an SM4 cipher (key-scheduled).
167pub struct gmcrypto_sm4_t {
168    inner: Sm4Cipher,
169}
170
171/// Opaque handle for a streaming SM4-CBC encryptor (v0.5 W1).
172/// Construct with [`gmcrypto_sm4_cbc_encryptor_new`], feed plaintext via
173/// [`gmcrypto_sm4_cbc_encryptor_update`], emit the trailing PKCS#7-
174/// padded block(s) via [`gmcrypto_sm4_cbc_encryptor_finalize`].
175pub struct gmcrypto_sm4_cbc_encryptor_t {
176    inner: InnerSm4CbcEnc,
177}
178
179/// Opaque handle for a streaming SM4-CBC decryptor (v0.5 W1). Same
180/// buffer-back-by-one padding-oracle defense as the v0.3 W5 Rust
181/// streaming surface: the most recent decrypted block is held back
182/// from emission until [`gmcrypto_sm4_cbc_decryptor_finalize`]
183/// confirms it is the last block and validates the PKCS#7 padding.
184pub struct gmcrypto_sm4_cbc_decryptor_t {
185    inner: InnerSm4CbcDec,
186}
187
188/// Opaque handle for a streaming (incremental-input) SM4-GCM encryptor
189/// (v0.10 W1). Output-streaming: each
190/// [`gmcrypto_sm4_gcm_encryptor_update`] emits the ciphertext for its
191/// chunk; [`gmcrypto_sm4_gcm_encryptor_finalize`] emits the 16-byte tag.
192/// Construct with [`gmcrypto_sm4_gcm_encryptor_new`]; pair with exactly
193/// one finalize (which frees the handle) **or** one
194/// [`gmcrypto_sm4_gcm_encryptor_free`].
195pub struct gmcrypto_sm4_gcm_encryptor_t {
196    inner: InnerSm4GcmEnc,
197}
198
199/// Opaque handle for a streaming (incremental-input, output-BUFFERED)
200/// SM4-GCM decryptor (v0.10 W2). Commit-on-verify:
201/// [`gmcrypto_sm4_gcm_decryptor_update`] buffers ciphertext and emits
202/// **nothing**; [`gmcrypto_sm4_gcm_decryptor_finalize_verify`] releases
203/// the full plaintext only after a constant-time tag check. Memory is
204/// `O(message)`. Construct with [`gmcrypto_sm4_gcm_decryptor_new`].
205pub struct gmcrypto_sm4_gcm_decryptor_t {
206    inner: InnerSm4GcmDec,
207}
208
209/// Opaque handle for an SM2 private key.
210pub struct gmcrypto_sm2_privkey_t {
211    inner: Sm2PrivateKey,
212}
213
214/// Opaque handle for an SM2 public key.
215pub struct gmcrypto_sm2_pubkey_t {
216    inner: Sm2PublicKey,
217}
218
219/// Opaque handle for an SM2 key-exchange INITIATOR (party A; GM/T
220/// 0003.3, v1.2). Born already awaiting the responder's reply —
221/// [`gmcrypto_sm2_kx_initiator_new`] samples the ephemeral internally
222/// and writes `R_A` immediately, so no pre-ephemeral state exists in
223/// C. Pair with exactly one [`gmcrypto_sm2_kx_initiator_confirm`]
224/// (which consumes + frees) **or** one
225/// [`gmcrypto_sm2_kx_initiator_free`].
226pub struct gmcrypto_sm2_kx_initiator_t {
227    inner: InnerSm2KxInitiatorWaiting,
228    klen: usize,
229}
230
231/// Internal state for the responder handle. The Rust `respond`
232/// consumes the responder, so a FAILED respond leaves nothing to
233/// retry with — the handle transitions to `Spent` and every further
234/// call fails (the caller frees it). A successful respond moves to
235/// `Waiting`; a second `_respond` on a `Waiting` handle returns
236/// `GMCRYPTO_ERR` **without** touching the in-flight state.
237enum InnerKxResponderState {
238    // Boxed: the live variants are large (static key + points + Z
239    // hashes) next to the zero-size `Spent` (clippy
240    // large-enum-variant); the handle itself is heap-allocated anyway.
241    Fresh(Box<InnerSm2KxResponder>),
242    Waiting(Box<InnerSm2KxResponderWaiting>),
243    Spent,
244}
245
246/// Opaque handle for an SM2 key-exchange RESPONDER (party B; GM/T
247/// 0003.3, v1.2). Lifecycle: [`gmcrypto_sm2_kx_responder_new`] →
248/// [`gmcrypto_sm2_kx_responder_respond`] (takes `R_A`, emits
249/// `R_B` + `S_B`) → [`gmcrypto_sm2_kx_responder_finish`] (takes
250/// `S_A`, releases `K`, consumes + frees). Pair with exactly one
251/// `_finish` **or** one [`gmcrypto_sm2_kx_responder_free`].
252pub struct gmcrypto_sm2_kx_responder_t {
253    state: InnerKxResponderState,
254    klen: usize,
255}
256
257// ============================================================
258// Helpers — all `unsafe` localized here with SAFETY comments.
259// ============================================================
260
261/// Reconstruct a `&[u8]` from a `(ptr, len)` pair, treating `(NULL, 0)`
262/// as an empty slice.
263///
264/// # Safety
265/// - `ptr` must be valid for reads of `len` bytes, OR `len == 0`.
266/// - The memory must not be mutated for the lifetime of the returned
267///   slice.
268#[allow(unsafe_code)]
269unsafe fn try_slice<'a>(ptr: *const u8, len: usize) -> Option<&'a [u8]> {
270    if len == 0 {
271        // `(NULL, 0)` and `(non-null, 0)` both denote empty input.
272        Some(&[])
273    } else if ptr.is_null() {
274        None
275    } else {
276        // SAFETY: caller guarantees ptr is valid for `len` bytes.
277        Some(unsafe { slice::from_raw_parts(ptr, len) })
278    }
279}
280
281/// Reconstruct a `&mut [u8]` from a `(ptr, len)` pair.
282///
283/// # Safety
284/// - `ptr` must be valid for read+write of `len` bytes, OR `len == 0`.
285/// - The memory must not be aliased.
286#[allow(unsafe_code)]
287unsafe fn try_slice_mut<'a>(ptr: *mut u8, len: usize) -> Option<&'a mut [u8]> {
288    if len == 0 {
289        Some(&mut [])
290    } else if ptr.is_null() {
291        None
292    } else {
293        // SAFETY: caller guarantees ptr is valid + unaliased.
294        Some(unsafe { slice::from_raw_parts_mut(ptr, len) })
295    }
296}
297
298/// Write a slice into a caller-supplied `(out, out_capacity,
299/// out_actual_len)` buffer per the v0.4 W4 / Q4.13 convention.
300/// Returns [`GMCRYPTO_OK`] on success or [`GMCRYPTO_ERR`] if the
301/// buffer is too small (and writes the required length to
302/// `*out_actual_len`).
303///
304/// # Safety
305/// - `out` valid for `out_capacity` bytes (or `out_capacity == 0`).
306/// - `out_actual_len` is a valid `*mut usize`.
307#[allow(unsafe_code)]
308unsafe fn write_output(
309    bytes: &[u8],
310    out: *mut u8,
311    out_capacity: usize,
312    out_actual_len: *mut usize,
313) -> c_int {
314    if out_actual_len.is_null() {
315        return GMCRYPTO_ERR;
316    }
317    // SAFETY: caller-asserted non-null.
318    unsafe { ptr::write(out_actual_len, bytes.len()) };
319    if bytes.len() > out_capacity {
320        return GMCRYPTO_ERR;
321    }
322    if bytes.is_empty() {
323        return GMCRYPTO_OK;
324    }
325    // SAFETY: out valid for at least `bytes.len() <= out_capacity` bytes.
326    let dst = unsafe { try_slice_mut(out, bytes.len()) };
327    match dst {
328        Some(d) => {
329            d.copy_from_slice(bytes);
330            GMCRYPTO_OK
331        }
332        None => GMCRYPTO_ERR,
333    }
334}
335
336/// Catch any panic and convert to a [`GMCRYPTO_ERR`] return. Per the
337/// failure-mode invariant, the C caller cannot distinguish panic
338/// from other failure modes — which is the intended posture.
339#[inline]
340fn ffi_guard<F: FnOnce() -> c_int + std::panic::UnwindSafe>(f: F) -> c_int {
341    std::panic::catch_unwind(f).unwrap_or(GMCRYPTO_ERR)
342}
343
344// ============================================================
345// Version string.
346// ============================================================
347
348/// Returns a NUL-terminated string with the `gmcrypto-c` crate version,
349/// tracking Cargo's `CARGO_PKG_VERSION` at build time (e.g. `"1.0.0"`).
350/// The returned pointer is to a static `&'static CStr` and must NOT be
351/// freed by the caller.
352#[unsafe(no_mangle)]
353pub extern "C" fn gmcrypto_version() -> *const c_char {
354    // The version string lives in the binary; static lifetime. Derived from
355    // CARGO_PKG_VERSION so it auto-tracks the workspace version bump (the
356    // literal previously drifted — it still read "0.4.0" on the 1.0.0 crate).
357    const VERSION: &core::ffi::CStr = match core::ffi::CStr::from_bytes_with_nul(
358        concat!(env!("CARGO_PKG_VERSION"), "\0").as_bytes(),
359    ) {
360        Ok(s) => s,
361        Err(_) => unreachable!(),
362    };
363    VERSION.as_ptr()
364}
365
366// ============================================================
367// SM3 — single-shot + streaming.
368// ============================================================
369
370/// Single-shot SM3 hash. Writes 32 bytes to `out_digest`.
371///
372/// # Returns
373/// [`GMCRYPTO_OK`] on success; [`GMCRYPTO_ERR`] on invalid input
374/// (null `out_digest`, null `msg` with non-zero `msg_len`).
375#[unsafe(no_mangle)]
376pub unsafe extern "C" fn gmcrypto_sm3_hash(
377    msg: *const u8,
378    msg_len: usize,
379    out_digest: *mut u8,
380) -> c_int {
381    ffi_guard(|| {
382        // SAFETY: contract documented on each helper.
383        let input = match unsafe { try_slice(msg, msg_len) } {
384            Some(s) => s,
385            None => return GMCRYPTO_ERR,
386        };
387        let out = match unsafe { try_slice_mut(out_digest, GMCRYPTO_SM3_DIGEST_SIZE) } {
388            Some(s) => s,
389            None => return GMCRYPTO_ERR,
390        };
391        let digest = sm3_hash(input);
392        out.copy_from_slice(&digest);
393        GMCRYPTO_OK
394    })
395}
396
397/// Construct a fresh streaming SM3 hasher. Returns an opaque handle;
398/// must be freed via [`gmcrypto_sm3_free`].
399///
400/// Never returns NULL: construction is infallible apart from heap allocation,
401/// and allocation failure aborts the process via Rust's global allocator
402/// (it does not — and cannot, on stable Rust — return NULL here).
403#[unsafe(no_mangle)]
404pub extern "C" fn gmcrypto_sm3_new() -> *mut gmcrypto_sm3_t {
405    let boxed = Box::new(gmcrypto_sm3_t {
406        inner: InnerSm3::new(),
407    });
408    Box::into_raw(boxed)
409}
410
411/// Absorb `data` into the streaming SM3 hasher.
412#[unsafe(no_mangle)]
413pub unsafe extern "C" fn gmcrypto_sm3_update(
414    hasher: *mut gmcrypto_sm3_t,
415    data: *const u8,
416    data_len: usize,
417) -> c_int {
418    ffi_guard(|| {
419        if hasher.is_null() {
420            return GMCRYPTO_ERR;
421        }
422        let input = match unsafe { try_slice(data, data_len) } {
423            Some(s) => s,
424            None => return GMCRYPTO_ERR,
425        };
426        // SAFETY: `hasher` non-null per check above; caller guarantees
427        // unique access for the duration of this call.
428        let h = unsafe { &mut *hasher };
429        h.inner.update(input);
430        GMCRYPTO_OK
431    })
432}
433
434/// Consume the streaming SM3 hasher and write the digest to
435/// `out_digest`. The handle is **freed** by this call; do not call
436/// [`gmcrypto_sm3_free`] on it afterwards.
437#[unsafe(no_mangle)]
438pub unsafe extern "C" fn gmcrypto_sm3_finalize(
439    hasher: *mut gmcrypto_sm3_t,
440    out_digest: *mut u8,
441) -> c_int {
442    ffi_guard(|| {
443        if hasher.is_null() {
444            return GMCRYPTO_ERR;
445        }
446        let out = match unsafe { try_slice_mut(out_digest, GMCRYPTO_SM3_DIGEST_SIZE) } {
447            Some(s) => s,
448            None => return GMCRYPTO_ERR,
449        };
450        // SAFETY: hasher non-null; take ownership and drop after finalize.
451        let boxed = unsafe { Box::from_raw(hasher) };
452        let digest = boxed.inner.finalize();
453        out.copy_from_slice(&digest);
454        GMCRYPTO_OK
455    })
456}
457
458/// Free a streaming SM3 hasher. Passing NULL is a no-op.
459#[unsafe(no_mangle)]
460pub unsafe extern "C" fn gmcrypto_sm3_free(hasher: *mut gmcrypto_sm3_t) {
461    if hasher.is_null() {
462        return;
463    }
464    // SAFETY: hasher came from `Box::into_raw` and the caller has not
465    // freed it before.
466    drop(unsafe { Box::from_raw(hasher) });
467}
468
469// ============================================================
470// HMAC-SM3 — single-shot + streaming.
471// ============================================================
472
473/// Single-shot HMAC-SM3. Writes 32 bytes to `out_tag`.
474#[unsafe(no_mangle)]
475pub unsafe extern "C" fn gmcrypto_hmac_sm3(
476    key: *const u8,
477    key_len: usize,
478    msg: *const u8,
479    msg_len: usize,
480    out_tag: *mut u8,
481) -> c_int {
482    ffi_guard(|| {
483        let k = match unsafe { try_slice(key, key_len) } {
484            Some(s) => s,
485            None => return GMCRYPTO_ERR,
486        };
487        let m = match unsafe { try_slice(msg, msg_len) } {
488            Some(s) => s,
489            None => return GMCRYPTO_ERR,
490        };
491        let out = match unsafe { try_slice_mut(out_tag, GMCRYPTO_SM3_DIGEST_SIZE) } {
492            Some(s) => s,
493            None => return GMCRYPTO_ERR,
494        };
495        let tag = hmac_sm3(k, m);
496        out.copy_from_slice(&tag);
497        GMCRYPTO_OK
498    })
499}
500
501/// Construct a fresh streaming HMAC-SM3 instance keyed with `key`.
502/// Returns NULL on invalid input.
503#[unsafe(no_mangle)]
504pub unsafe extern "C" fn gmcrypto_hmac_sm3_new(
505    key: *const u8,
506    key_len: usize,
507) -> *mut gmcrypto_hmac_sm3_t {
508    let result = std::panic::catch_unwind(|| {
509        let k = unsafe { try_slice(key, key_len) }?;
510        Some(Box::into_raw(Box::new(gmcrypto_hmac_sm3_t {
511            inner: InnerHmacSm3::new(k),
512        })))
513    });
514    match result {
515        Ok(Some(ptr)) => ptr,
516        _ => ptr::null_mut(),
517    }
518}
519
520/// Absorb `data` into the streaming HMAC-SM3 instance.
521#[unsafe(no_mangle)]
522pub unsafe extern "C" fn gmcrypto_hmac_sm3_update(
523    mac: *mut gmcrypto_hmac_sm3_t,
524    data: *const u8,
525    data_len: usize,
526) -> c_int {
527    ffi_guard(|| {
528        if mac.is_null() {
529            return GMCRYPTO_ERR;
530        }
531        let input = match unsafe { try_slice(data, data_len) } {
532            Some(s) => s,
533            None => return GMCRYPTO_ERR,
534        };
535        let m = unsafe { &mut *mac };
536        m.inner.update(input);
537        GMCRYPTO_OK
538    })
539}
540
541/// Consume the streaming HMAC-SM3 instance and write the 32-byte tag
542/// to `out_tag`. The handle is **freed** by this call.
543#[unsafe(no_mangle)]
544pub unsafe extern "C" fn gmcrypto_hmac_sm3_finalize(
545    mac: *mut gmcrypto_hmac_sm3_t,
546    out_tag: *mut u8,
547) -> c_int {
548    ffi_guard(|| {
549        if mac.is_null() {
550            return GMCRYPTO_ERR;
551        }
552        let out = match unsafe { try_slice_mut(out_tag, GMCRYPTO_SM3_DIGEST_SIZE) } {
553            Some(s) => s,
554            None => return GMCRYPTO_ERR,
555        };
556        let boxed = unsafe { Box::from_raw(mac) };
557        let tag = boxed.inner.finalize();
558        out.copy_from_slice(&tag);
559        GMCRYPTO_OK
560    })
561}
562
563/// Consume the streaming HMAC-SM3 instance and verify the candidate
564/// tag in constant time. Returns [`GMCRYPTO_OK`] on match;
565/// [`GMCRYPTO_ERR`] on mismatch. The handle is **freed** by this call.
566#[unsafe(no_mangle)]
567pub unsafe extern "C" fn gmcrypto_hmac_sm3_verify(
568    mac: *mut gmcrypto_hmac_sm3_t,
569    expected_tag: *const u8,
570) -> c_int {
571    ffi_guard(|| {
572        if mac.is_null() || expected_tag.is_null() {
573            return GMCRYPTO_ERR;
574        }
575        let expected = match unsafe { try_slice(expected_tag, GMCRYPTO_SM3_DIGEST_SIZE) } {
576            Some(s) => s,
577            None => return GMCRYPTO_ERR,
578        };
579        let mut expected_arr = [0u8; GMCRYPTO_SM3_DIGEST_SIZE];
580        expected_arr.copy_from_slice(expected);
581        let boxed = unsafe { Box::from_raw(mac) };
582        if boxed.inner.verify(&expected_arr) {
583            GMCRYPTO_OK
584        } else {
585            GMCRYPTO_ERR
586        }
587    })
588}
589
590/// Free a streaming HMAC-SM3 instance. NULL is a no-op.
591#[unsafe(no_mangle)]
592pub unsafe extern "C" fn gmcrypto_hmac_sm3_free(mac: *mut gmcrypto_hmac_sm3_t) {
593    if mac.is_null() {
594        return;
595    }
596    drop(unsafe { Box::from_raw(mac) });
597}
598
599// ============================================================
600// PBKDF2-HMAC-SM3.
601// ============================================================
602
603/// Derive `out_len` bytes via PBKDF2-HMAC-SM3 over `(pwd, salt,
604/// iterations)`. Writes into the caller-supplied `out` buffer.
605#[unsafe(no_mangle)]
606pub unsafe extern "C" fn gmcrypto_pbkdf2_hmac_sm3(
607    pwd: *const u8,
608    pwd_len: usize,
609    salt: *const u8,
610    salt_len: usize,
611    iterations: u32,
612    out: *mut u8,
613    out_len: usize,
614) -> c_int {
615    ffi_guard(|| {
616        let p = match unsafe { try_slice(pwd, pwd_len) } {
617            Some(s) => s,
618            None => return GMCRYPTO_ERR,
619        };
620        let s = match unsafe { try_slice(salt, salt_len) } {
621            Some(s) => s,
622            None => return GMCRYPTO_ERR,
623        };
624        let o = match unsafe { try_slice_mut(out, out_len) } {
625            Some(s) => s,
626            None => return GMCRYPTO_ERR,
627        };
628        match pbkdf2_hmac_sm3(p, s, iterations, o) {
629            Some(()) => GMCRYPTO_OK,
630            None => GMCRYPTO_ERR,
631        }
632    })
633}
634
635// ============================================================
636// SM4 — block cipher (single-block) + CBC (single-shot).
637// ============================================================
638
639/// Construct an SM4 cipher from a 16-byte key. Returns NULL on null
640/// key.
641#[unsafe(no_mangle)]
642pub unsafe extern "C" fn gmcrypto_sm4_new(key: *const u8) -> *mut gmcrypto_sm4_t {
643    let result = std::panic::catch_unwind(|| {
644        let k = unsafe { try_slice(key, GMCRYPTO_SM4_KEY_SIZE) }?;
645        let k_arr: &[u8; GMCRYPTO_SM4_KEY_SIZE] = k.try_into().ok()?;
646        Some(Box::into_raw(Box::new(gmcrypto_sm4_t {
647            inner: Sm4Cipher::new(k_arr),
648        })))
649    });
650    match result {
651        Ok(Some(p)) => p,
652        _ => ptr::null_mut(),
653    }
654}
655
656/// Encrypt one 16-byte block in place under the SM4 cipher.
657///
658/// WARNING: this is the raw SM4 block, not a cipher mode. Calling it in a
659/// loop over a multi-block buffer is ECB — it leaks plaintext-block equality
660/// and has no semantic security. To encrypt messages use a mode
661/// (`gmcrypto_sm4_gcm_*` / `_ccm_*` authenticated; or `_cbc_*` / `_ctr_*` /
662/// `_xts_*` confidentiality-only with a unique IV/nonce/tweak).
663#[unsafe(no_mangle)]
664pub unsafe extern "C" fn gmcrypto_sm4_encrypt_block(
665    cipher: *const gmcrypto_sm4_t,
666    block: *mut u8,
667) -> c_int {
668    ffi_guard(|| {
669        if cipher.is_null() {
670            return GMCRYPTO_ERR;
671        }
672        let b = match unsafe { try_slice_mut(block, GMCRYPTO_SM4_BLOCK_SIZE) } {
673            Some(s) => s,
674            None => return GMCRYPTO_ERR,
675        };
676        let b_arr: &mut [u8; GMCRYPTO_SM4_BLOCK_SIZE] = match b.try_into() {
677            Ok(a) => a,
678            Err(_) => return GMCRYPTO_ERR,
679        };
680        let c = unsafe { &*cipher };
681        c.inner.encrypt_block(b_arr);
682        GMCRYPTO_OK
683    })
684}
685
686/// Decrypt one 16-byte block in place under the SM4 cipher.
687///
688/// WARNING: raw SM4 block, not a cipher mode (see
689/// `gmcrypto_sm4_encrypt_block`). Looping it over a buffer is ECB
690/// decryption — no semantic security, no authentication. Decrypt real
691/// messages with a mode.
692#[unsafe(no_mangle)]
693pub unsafe extern "C" fn gmcrypto_sm4_decrypt_block(
694    cipher: *const gmcrypto_sm4_t,
695    block: *mut u8,
696) -> c_int {
697    ffi_guard(|| {
698        if cipher.is_null() {
699            return GMCRYPTO_ERR;
700        }
701        let b = match unsafe { try_slice_mut(block, GMCRYPTO_SM4_BLOCK_SIZE) } {
702            Some(s) => s,
703            None => return GMCRYPTO_ERR,
704        };
705        let b_arr: &mut [u8; GMCRYPTO_SM4_BLOCK_SIZE] = match b.try_into() {
706            Ok(a) => a,
707            Err(_) => return GMCRYPTO_ERR,
708        };
709        let c = unsafe { &*cipher };
710        c.inner.decrypt_block(b_arr);
711        GMCRYPTO_OK
712    })
713}
714
715/// Free an SM4 cipher. NULL is a no-op.
716#[unsafe(no_mangle)]
717pub unsafe extern "C" fn gmcrypto_sm4_free(cipher: *mut gmcrypto_sm4_t) {
718    if cipher.is_null() {
719        return;
720    }
721    drop(unsafe { Box::from_raw(cipher) });
722}
723
724/// SM4-CBC single-shot encrypt with PKCS#7 padding. IV must be
725/// caller-supplied and unpredictable (per NIST SP 800-38A
726/// Appendix C). Output length is always `((pt_len / 16) + 1) * 16`.
727#[unsafe(no_mangle)]
728pub unsafe extern "C" fn gmcrypto_sm4_cbc_encrypt(
729    key: *const u8,
730    iv: *const u8,
731    pt: *const u8,
732    pt_len: usize,
733    out: *mut u8,
734    out_capacity: usize,
735    out_actual_len: *mut usize,
736) -> c_int {
737    ffi_guard(|| {
738        let k = match unsafe { try_slice(key, GMCRYPTO_SM4_KEY_SIZE) } {
739            Some(s) => s,
740            None => return GMCRYPTO_ERR,
741        };
742        let iv_slice = match unsafe { try_slice(iv, GMCRYPTO_SM4_BLOCK_SIZE) } {
743            Some(s) => s,
744            None => return GMCRYPTO_ERR,
745        };
746        let p = match unsafe { try_slice(pt, pt_len) } {
747            Some(s) => s,
748            None => return GMCRYPTO_ERR,
749        };
750        let k_arr: &[u8; GMCRYPTO_SM4_KEY_SIZE] = match k.try_into() {
751            Ok(a) => a,
752            Err(_) => return GMCRYPTO_ERR,
753        };
754        let iv_arr: &[u8; GMCRYPTO_SM4_BLOCK_SIZE] = match iv_slice.try_into() {
755            Ok(a) => a,
756            Err(_) => return GMCRYPTO_ERR,
757        };
758        let ciphertext = mode_cbc::encrypt(k_arr, iv_arr, p);
759        unsafe { write_output(&ciphertext, out, out_capacity, out_actual_len) }
760    })
761}
762
763/// SM4-CBC single-shot decrypt. Single-`Failed` return on any
764/// failure mode (length not multiple of 16, bad padding, key/IV
765/// mismatch) per the failure-mode invariant.
766#[unsafe(no_mangle)]
767pub unsafe extern "C" fn gmcrypto_sm4_cbc_decrypt(
768    key: *const u8,
769    iv: *const u8,
770    ct: *const u8,
771    ct_len: usize,
772    out: *mut u8,
773    out_capacity: usize,
774    out_actual_len: *mut usize,
775) -> c_int {
776    ffi_guard(|| {
777        let k = match unsafe { try_slice(key, GMCRYPTO_SM4_KEY_SIZE) } {
778            Some(s) => s,
779            None => return GMCRYPTO_ERR,
780        };
781        let iv_slice = match unsafe { try_slice(iv, GMCRYPTO_SM4_BLOCK_SIZE) } {
782            Some(s) => s,
783            None => return GMCRYPTO_ERR,
784        };
785        let c = match unsafe { try_slice(ct, ct_len) } {
786            Some(s) => s,
787            None => return GMCRYPTO_ERR,
788        };
789        let k_arr: &[u8; GMCRYPTO_SM4_KEY_SIZE] = match k.try_into() {
790            Ok(a) => a,
791            Err(_) => return GMCRYPTO_ERR,
792        };
793        let iv_arr: &[u8; GMCRYPTO_SM4_BLOCK_SIZE] = match iv_slice.try_into() {
794            Ok(a) => a,
795            Err(_) => return GMCRYPTO_ERR,
796        };
797        match mode_cbc::decrypt(k_arr, iv_arr, c) {
798            Some(plaintext) => unsafe {
799                write_output(&plaintext, out, out_capacity, out_actual_len)
800            },
801            None => GMCRYPTO_ERR,
802        }
803    })
804}
805
806// ============================================================
807// SM4-CBC — streaming (v0.5 W1).
808//
809// Wraps `gmcrypto_core::sm4::{Sm4CbcEncryptor, Sm4CbcDecryptor}`.
810// Streaming-emit pattern: each `_update` call may emit zero or more
811// full 16-byte ciphertext / plaintext blocks; `_finalize` emits the
812// final block(s) (encryptor: PKCS#7 padding; decryptor: PKCS#7 strip
813// of the held-back final block). Encryptor and decryptor are
814// independent opaque types — Q5.2 pinned this over a unified `_cbc_t`
815// with mode enum.
816//
817// Output buffer convention matches Q5.3: every `_update` /
818// `_finalize` uses `(out, out_capacity, out_actual_len)`; on too-
819// small capacity we return `GMCRYPTO_ERR` and write the required
820// length to `*out_actual_len` (caller-retry pattern).
821//
822// Buffer-back-by-one padding-oracle defense is preserved across the
823// FFI boundary: the decryptor's `_finalize` never returns plaintext
824// if the final block's padding is invalid.
825// ============================================================
826
827/// Construct a streaming SM4-CBC encryptor. `key` is exactly 16
828/// bytes; `iv` is exactly 16 bytes and MUST be caller-supplied
829/// unpredictable bytes (NIST SP 800-38A Appendix C). Returns NULL
830/// on invalid pointer input.
831#[unsafe(no_mangle)]
832pub unsafe extern "C" fn gmcrypto_sm4_cbc_encryptor_new(
833    key: *const u8,
834    iv: *const u8,
835) -> *mut gmcrypto_sm4_cbc_encryptor_t {
836    let result = std::panic::catch_unwind(|| {
837        let k = unsafe { try_slice(key, GMCRYPTO_SM4_KEY_SIZE) }?;
838        let v = unsafe { try_slice(iv, GMCRYPTO_SM4_BLOCK_SIZE) }?;
839        let k_arr: &[u8; GMCRYPTO_SM4_KEY_SIZE] = k.try_into().ok()?;
840        let v_arr: &[u8; GMCRYPTO_SM4_BLOCK_SIZE] = v.try_into().ok()?;
841        Some(Box::into_raw(Box::new(gmcrypto_sm4_cbc_encryptor_t {
842            inner: InnerSm4CbcEnc::new(k_arr, v_arr),
843        })))
844    });
845    match result {
846        Ok(Some(p)) => p,
847        _ => ptr::null_mut(),
848    }
849}
850
851/// Absorb plaintext into the streaming SM4-CBC encryptor and emit
852/// zero or more full ciphertext blocks. The caller-allocated `out`
853/// buffer MUST be at least `pt_len + 16` bytes — that is the upper
854/// bound on bytes emitted by a single `_update` call (a buffered
855/// partial block from a prior call can produce one extra block when
856/// this call's input fills it). On insufficient capacity, the call
857/// returns [`GMCRYPTO_ERR`] and the encryptor state is left mid-
858/// stream (the ciphertext bytes that would have been emitted are
859/// lost). Callers should size the output buffer correctly up-front.
860#[unsafe(no_mangle)]
861pub unsafe extern "C" fn gmcrypto_sm4_cbc_encryptor_update(
862    enc: *mut gmcrypto_sm4_cbc_encryptor_t,
863    pt: *const u8,
864    pt_len: usize,
865    out: *mut u8,
866    out_capacity: usize,
867    out_actual_len: *mut usize,
868) -> c_int {
869    ffi_guard(|| {
870        if enc.is_null() {
871            return GMCRYPTO_ERR;
872        }
873        let input = match unsafe { try_slice(pt, pt_len) } {
874            Some(s) => s,
875            None => return GMCRYPTO_ERR,
876        };
877        // SAFETY: enc non-null per check above; caller guarantees
878        // unique access for the duration of this call.
879        let e = unsafe { &mut *enc };
880        e.inner.update(input);
881        let emitted = e.inner.take_output();
882        unsafe { write_output(&emitted, out, out_capacity, out_actual_len) }
883    })
884}
885
886/// Apply PKCS#7 padding to the buffered tail and emit the final
887/// ciphertext block(s). Consumes the encryptor — the handle is
888/// **freed** by this call; do NOT call
889/// [`gmcrypto_sm4_cbc_encryptor_free`] on it afterwards.
890///
891/// Output is always exactly one block (16 bytes).
892#[unsafe(no_mangle)]
893pub unsafe extern "C" fn gmcrypto_sm4_cbc_encryptor_finalize(
894    enc: *mut gmcrypto_sm4_cbc_encryptor_t,
895    out: *mut u8,
896    out_capacity: usize,
897    out_actual_len: *mut usize,
898) -> c_int {
899    ffi_guard(|| {
900        if enc.is_null() {
901            return GMCRYPTO_ERR;
902        }
903        // SAFETY: enc came from Box::into_raw; take ownership and drop.
904        let boxed = unsafe { Box::from_raw(enc) };
905        // finalize() returns ALL of self.output (including any bytes
906        // previously drained via take_output) — but we drained those
907        // on prior update calls, so the returned Vec contains only
908        // the new final padded block(s).
909        let final_bytes = boxed.inner.finalize();
910        unsafe { write_output(&final_bytes, out, out_capacity, out_actual_len) }
911    })
912}
913
914/// Free a streaming SM4-CBC encryptor. Passing NULL is a no-op. Do
915/// NOT call after [`gmcrypto_sm4_cbc_encryptor_finalize`] — that
916/// already consumed the handle.
917#[unsafe(no_mangle)]
918pub unsafe extern "C" fn gmcrypto_sm4_cbc_encryptor_free(enc: *mut gmcrypto_sm4_cbc_encryptor_t) {
919    if enc.is_null() {
920        return;
921    }
922    // SAFETY: enc came from Box::into_raw and has not been freed.
923    drop(unsafe { Box::from_raw(enc) });
924}
925
926/// Construct a streaming SM4-CBC decryptor. `key` is exactly 16
927/// bytes; `iv` is exactly 16 bytes and must match the value used
928/// during encryption. Returns NULL on invalid pointer input.
929#[unsafe(no_mangle)]
930pub unsafe extern "C" fn gmcrypto_sm4_cbc_decryptor_new(
931    key: *const u8,
932    iv: *const u8,
933) -> *mut gmcrypto_sm4_cbc_decryptor_t {
934    let result = std::panic::catch_unwind(|| {
935        let k = unsafe { try_slice(key, GMCRYPTO_SM4_KEY_SIZE) }?;
936        let v = unsafe { try_slice(iv, GMCRYPTO_SM4_BLOCK_SIZE) }?;
937        let k_arr: &[u8; GMCRYPTO_SM4_KEY_SIZE] = k.try_into().ok()?;
938        let v_arr: &[u8; GMCRYPTO_SM4_BLOCK_SIZE] = v.try_into().ok()?;
939        Some(Box::into_raw(Box::new(gmcrypto_sm4_cbc_decryptor_t {
940            inner: InnerSm4CbcDec::new(k_arr, v_arr),
941        })))
942    });
943    match result {
944        Ok(Some(p)) => p,
945        _ => ptr::null_mut(),
946    }
947}
948
949/// Absorb ciphertext into the streaming SM4-CBC decryptor and emit
950/// zero or more full plaintext blocks. The final-candidate block is
951/// HELD BACK from emission until `_finalize` validates the trailing
952/// padding (buffer-back-by-one padding-oracle defense). Same buffer-
953/// size contract as the encryptor's `_update`: caller MUST allocate
954/// `out_capacity >= ct_len + 16` (strict upper bound on bytes emitted
955/// in one call). On insufficient capacity returns [`GMCRYPTO_ERR`]
956/// and the decryptor state is left mid-stream; size the buffer
957/// up-front.
958#[unsafe(no_mangle)]
959pub unsafe extern "C" fn gmcrypto_sm4_cbc_decryptor_update(
960    dec: *mut gmcrypto_sm4_cbc_decryptor_t,
961    ct: *const u8,
962    ct_len: usize,
963    out: *mut u8,
964    out_capacity: usize,
965    out_actual_len: *mut usize,
966) -> c_int {
967    ffi_guard(|| {
968        if dec.is_null() {
969            return GMCRYPTO_ERR;
970        }
971        let input = match unsafe { try_slice(ct, ct_len) } {
972            Some(s) => s,
973            None => return GMCRYPTO_ERR,
974        };
975        // SAFETY: dec non-null per check above.
976        let d = unsafe { &mut *dec };
977        d.inner.update(input);
978        let emitted = d.inner.take_output();
979        unsafe { write_output(&emitted, out, out_capacity, out_actual_len) }
980    })
981}
982
983/// Strip PKCS#7 padding from the held-back final block and emit the
984/// last plaintext bytes. Consumes the decryptor — the handle is
985/// **freed** by this call; do NOT call
986/// [`gmcrypto_sm4_cbc_decryptor_free`] on it afterwards.
987///
988/// Returns [`GMCRYPTO_ERR`] on any failure mode (length not multiple
989/// of 16, no full blocks seen, or padding-strip rejection) — single
990/// uninformative failure code per the failure-mode invariant. The
991/// caller-supplied `out_actual_len` is set to `0` on failure.
992#[unsafe(no_mangle)]
993pub unsafe extern "C" fn gmcrypto_sm4_cbc_decryptor_finalize(
994    dec: *mut gmcrypto_sm4_cbc_decryptor_t,
995    out: *mut u8,
996    out_capacity: usize,
997    out_actual_len: *mut usize,
998) -> c_int {
999    ffi_guard(|| {
1000        if dec.is_null() {
1001            return GMCRYPTO_ERR;
1002        }
1003        // SAFETY: dec came from Box::into_raw; take ownership and drop.
1004        let boxed = unsafe { Box::from_raw(dec) };
1005        if let Some(final_bytes) = boxed.inner.finalize() {
1006            // SAFETY: write_output's contract documented at its decl.
1007            unsafe { write_output(&final_bytes, out, out_capacity, out_actual_len) }
1008        } else {
1009            if !out_actual_len.is_null() {
1010                // SAFETY: caller-asserted non-null.
1011                unsafe { ptr::write(out_actual_len, 0) };
1012            }
1013            GMCRYPTO_ERR
1014        }
1015    })
1016}
1017
1018/// Free a streaming SM4-CBC decryptor. Passing NULL is a no-op. Do
1019/// NOT call after [`gmcrypto_sm4_cbc_decryptor_finalize`] — that
1020/// already consumed the handle.
1021#[unsafe(no_mangle)]
1022pub unsafe extern "C" fn gmcrypto_sm4_cbc_decryptor_free(dec: *mut gmcrypto_sm4_cbc_decryptor_t) {
1023    if dec.is_null() {
1024        return;
1025    }
1026    // SAFETY: dec came from Box::into_raw and has not been freed.
1027    drop(unsafe { Box::from_raw(dec) });
1028}
1029
1030// ============================================================
1031// SM4 AEAD — single-shot (v0.9 W4).
1032//
1033// Wraps `gmcrypto_core::sm4::{mode_gcm, mode_ccm}`. Six entry points:
1034// GCM encrypt/decrypt (+ tag-len variants) and CCM encrypt/decrypt.
1035// Every error path returns GMCRYPTO_ERR (single failure code per the
1036// failure-mode invariant — no tag-mismatch vs. bad-length vs. invalid-
1037// nonce distinction across the C boundary). Variable-length outputs
1038// use the (out, out_capacity, out_actual_len) convention; the GCM tag
1039// is a fixed-size bare output buffer the caller sizes (16 bytes, or
1040// `tag_len` for the truncated variant). Streaming AEAD FFI is deferred
1041// to v0.10 (see docs/v0.9-scope.md Q9.6).
1042// ============================================================
1043
1044/// SM4-GCM single-shot encrypt. `ct_out` receives `pt_len` bytes (via
1045/// the capacity/actual-len convention); `tag_out` receives exactly 16
1046/// bytes. Returns [`GMCRYPTO_OK`] / [`GMCRYPTO_ERR`].
1047#[allow(clippy::too_many_arguments)]
1048#[unsafe(no_mangle)]
1049pub unsafe extern "C" fn gmcrypto_sm4_gcm_encrypt(
1050    key: *const u8,
1051    nonce: *const u8,
1052    nonce_len: usize,
1053    aad: *const u8,
1054    aad_len: usize,
1055    pt: *const u8,
1056    pt_len: usize,
1057    ct_out: *mut u8,
1058    ct_capacity: usize,
1059    ct_actual_len: *mut usize,
1060    tag_out: *mut u8,
1061) -> c_int {
1062    ffi_guard(|| {
1063        let k = match unsafe { try_slice(key, GMCRYPTO_SM4_KEY_SIZE) } {
1064            Some(s) => s,
1065            None => return GMCRYPTO_ERR,
1066        };
1067        let n = match unsafe { try_slice(nonce, nonce_len) } {
1068            Some(s) => s,
1069            None => return GMCRYPTO_ERR,
1070        };
1071        let a = match unsafe { try_slice(aad, aad_len) } {
1072            Some(s) => s,
1073            None => return GMCRYPTO_ERR,
1074        };
1075        let p = match unsafe { try_slice(pt, pt_len) } {
1076            Some(s) => s,
1077            None => return GMCRYPTO_ERR,
1078        };
1079        let k_arr: &[u8; GMCRYPTO_SM4_KEY_SIZE] = match k.try_into() {
1080            Ok(a) => a,
1081            Err(_) => return GMCRYPTO_ERR,
1082        };
1083        // SAFETY: caller guarantees `tag_out` is valid for 16 bytes.
1084        let tag_dst = match unsafe { try_slice_mut(tag_out, GMCRYPTO_SM4_BLOCK_SIZE) } {
1085            Some(s) => s,
1086            None => return GMCRYPTO_ERR,
1087        };
1088        // `None` only when `pt_len > 2^36 − 32` (the GCM keystream
1089        // ceiling). Collapses to the single error code like every other
1090        // failure path.
1091        let (ciphertext, tag) = match mode_gcm::encrypt(k_arr, n, a, p) {
1092            Some(out) => out,
1093            None => return GMCRYPTO_ERR,
1094        };
1095        // SAFETY: ct_out valid for ct_capacity bytes; ct_actual_len valid.
1096        let rc = unsafe { write_output(&ciphertext, ct_out, ct_capacity, ct_actual_len) };
1097        if rc != GMCRYPTO_OK {
1098            return rc;
1099        }
1100        tag_dst.copy_from_slice(&tag);
1101        GMCRYPTO_OK
1102    })
1103}
1104
1105/// SM4-GCM single-shot decrypt with a 16-byte tag. `pt_out` receives
1106/// `ct_len` bytes. Returns [`GMCRYPTO_OK`] only if the tag verifies;
1107/// [`GMCRYPTO_ERR`] on any failure (single failure mode).
1108#[allow(clippy::too_many_arguments)]
1109#[unsafe(no_mangle)]
1110pub unsafe extern "C" fn gmcrypto_sm4_gcm_decrypt(
1111    key: *const u8,
1112    nonce: *const u8,
1113    nonce_len: usize,
1114    aad: *const u8,
1115    aad_len: usize,
1116    ct: *const u8,
1117    ct_len: usize,
1118    tag: *const u8,
1119    pt_out: *mut u8,
1120    pt_capacity: usize,
1121    pt_actual_len: *mut usize,
1122) -> c_int {
1123    ffi_guard(|| {
1124        let k = match unsafe { try_slice(key, GMCRYPTO_SM4_KEY_SIZE) } {
1125            Some(s) => s,
1126            None => return GMCRYPTO_ERR,
1127        };
1128        let n = match unsafe { try_slice(nonce, nonce_len) } {
1129            Some(s) => s,
1130            None => return GMCRYPTO_ERR,
1131        };
1132        let a = match unsafe { try_slice(aad, aad_len) } {
1133            Some(s) => s,
1134            None => return GMCRYPTO_ERR,
1135        };
1136        let c = match unsafe { try_slice(ct, ct_len) } {
1137            Some(s) => s,
1138            None => return GMCRYPTO_ERR,
1139        };
1140        let t = match unsafe { try_slice(tag, GMCRYPTO_SM4_BLOCK_SIZE) } {
1141            Some(s) => s,
1142            None => return GMCRYPTO_ERR,
1143        };
1144        let k_arr: &[u8; GMCRYPTO_SM4_KEY_SIZE] = match k.try_into() {
1145            Ok(a) => a,
1146            Err(_) => return GMCRYPTO_ERR,
1147        };
1148        let t_arr: &[u8; GMCRYPTO_SM4_BLOCK_SIZE] = match t.try_into() {
1149            Ok(a) => a,
1150            Err(_) => return GMCRYPTO_ERR,
1151        };
1152        match mode_gcm::decrypt(k_arr, n, a, c, t_arr) {
1153            // SAFETY: pt_out valid for pt_capacity bytes; pt_actual_len valid.
1154            Some(plaintext) => unsafe {
1155                write_output(&plaintext, pt_out, pt_capacity, pt_actual_len)
1156            },
1157            None => GMCRYPTO_ERR,
1158        }
1159    })
1160}
1161
1162/// SM4-GCM encrypt with a truncated tag. `tag_len` must be in
1163/// `{4, 8, 12, 13, 14, 15, 16}`; `tag_out` receives `tag_len` bytes.
1164/// Invalid `tag_len` → [`GMCRYPTO_ERR`].
1165#[allow(clippy::too_many_arguments)]
1166#[unsafe(no_mangle)]
1167pub unsafe extern "C" fn gmcrypto_sm4_gcm_encrypt_with_tag_len(
1168    key: *const u8,
1169    nonce: *const u8,
1170    nonce_len: usize,
1171    aad: *const u8,
1172    aad_len: usize,
1173    pt: *const u8,
1174    pt_len: usize,
1175    tag_len: usize,
1176    ct_out: *mut u8,
1177    ct_capacity: usize,
1178    ct_actual_len: *mut usize,
1179    tag_out: *mut u8,
1180) -> c_int {
1181    ffi_guard(|| {
1182        let tl = match GcmTagLen::new(tag_len) {
1183            Some(t) => t,
1184            None => return GMCRYPTO_ERR,
1185        };
1186        let k = match unsafe { try_slice(key, GMCRYPTO_SM4_KEY_SIZE) } {
1187            Some(s) => s,
1188            None => return GMCRYPTO_ERR,
1189        };
1190        let n = match unsafe { try_slice(nonce, nonce_len) } {
1191            Some(s) => s,
1192            None => return GMCRYPTO_ERR,
1193        };
1194        let a = match unsafe { try_slice(aad, aad_len) } {
1195            Some(s) => s,
1196            None => return GMCRYPTO_ERR,
1197        };
1198        let p = match unsafe { try_slice(pt, pt_len) } {
1199            Some(s) => s,
1200            None => return GMCRYPTO_ERR,
1201        };
1202        let k_arr: &[u8; GMCRYPTO_SM4_KEY_SIZE] = match k.try_into() {
1203            Ok(a) => a,
1204            Err(_) => return GMCRYPTO_ERR,
1205        };
1206        // SAFETY: caller guarantees `tag_out` is valid for `tag_len` bytes.
1207        let tag_dst = match unsafe { try_slice_mut(tag_out, tag_len) } {
1208            Some(s) => s,
1209            None => return GMCRYPTO_ERR,
1210        };
1211        // `None` only when `pt_len > 2^36 − 32` (the GCM keystream
1212        // ceiling). Collapses to the single error code.
1213        let (ciphertext, tag) = match mode_gcm::encrypt_with_tag_len(k_arr, n, a, p, tl) {
1214            Some(out) => out,
1215            None => return GMCRYPTO_ERR,
1216        };
1217        // SAFETY: ct_out valid for ct_capacity bytes; ct_actual_len valid.
1218        let rc = unsafe { write_output(&ciphertext, ct_out, ct_capacity, ct_actual_len) };
1219        if rc != GMCRYPTO_OK {
1220            return rc;
1221        }
1222        tag_dst.copy_from_slice(&tag);
1223        GMCRYPTO_OK
1224    })
1225}
1226
1227/// SM4-GCM decrypt with a truncated tag. `tag` is `tag_len` bytes;
1228/// `tag_len` must be in `{4, 8, 12, 13, 14, 15, 16}`. `pt_out`
1229/// receives `ct_len` bytes. [`GMCRYPTO_ERR`] on any failure.
1230#[allow(clippy::too_many_arguments)]
1231#[unsafe(no_mangle)]
1232pub unsafe extern "C" fn gmcrypto_sm4_gcm_decrypt_with_tag_len(
1233    key: *const u8,
1234    nonce: *const u8,
1235    nonce_len: usize,
1236    aad: *const u8,
1237    aad_len: usize,
1238    ct: *const u8,
1239    ct_len: usize,
1240    tag: *const u8,
1241    tag_len: usize,
1242    pt_out: *mut u8,
1243    pt_capacity: usize,
1244    pt_actual_len: *mut usize,
1245) -> c_int {
1246    ffi_guard(|| {
1247        let k = match unsafe { try_slice(key, GMCRYPTO_SM4_KEY_SIZE) } {
1248            Some(s) => s,
1249            None => return GMCRYPTO_ERR,
1250        };
1251        let n = match unsafe { try_slice(nonce, nonce_len) } {
1252            Some(s) => s,
1253            None => return GMCRYPTO_ERR,
1254        };
1255        let a = match unsafe { try_slice(aad, aad_len) } {
1256            Some(s) => s,
1257            None => return GMCRYPTO_ERR,
1258        };
1259        let c = match unsafe { try_slice(ct, ct_len) } {
1260            Some(s) => s,
1261            None => return GMCRYPTO_ERR,
1262        };
1263        // `decrypt_with_tag_len` validates `tag_len` (= t.len()) itself.
1264        let t = match unsafe { try_slice(tag, tag_len) } {
1265            Some(s) => s,
1266            None => return GMCRYPTO_ERR,
1267        };
1268        let k_arr: &[u8; GMCRYPTO_SM4_KEY_SIZE] = match k.try_into() {
1269            Ok(a) => a,
1270            Err(_) => return GMCRYPTO_ERR,
1271        };
1272        match mode_gcm::decrypt_with_tag_len(k_arr, n, a, c, t) {
1273            // SAFETY: pt_out valid for pt_capacity bytes; pt_actual_len valid.
1274            Some(plaintext) => unsafe {
1275                write_output(&plaintext, pt_out, pt_capacity, pt_actual_len)
1276            },
1277            None => GMCRYPTO_ERR,
1278        }
1279    })
1280}
1281
1282/// SM4-CCM single-shot encrypt. `tag_len` must be in
1283/// `{4, 6, 8, 10, 12, 14, 16}`; `nonce_len` in `[7, 13]`. `out`
1284/// receives `pt_len + tag_len` bytes (`ciphertext ‖ tag`). Invalid
1285/// parameters → [`GMCRYPTO_ERR`].
1286#[allow(clippy::too_many_arguments)]
1287#[unsafe(no_mangle)]
1288pub unsafe extern "C" fn gmcrypto_sm4_ccm_encrypt(
1289    key: *const u8,
1290    nonce: *const u8,
1291    nonce_len: usize,
1292    aad: *const u8,
1293    aad_len: usize,
1294    pt: *const u8,
1295    pt_len: usize,
1296    tag_len: usize,
1297    out: *mut u8,
1298    out_capacity: usize,
1299    out_actual_len: *mut usize,
1300) -> c_int {
1301    ffi_guard(|| {
1302        let k = match unsafe { try_slice(key, GMCRYPTO_SM4_KEY_SIZE) } {
1303            Some(s) => s,
1304            None => return GMCRYPTO_ERR,
1305        };
1306        let n = match unsafe { try_slice(nonce, nonce_len) } {
1307            Some(s) => s,
1308            None => return GMCRYPTO_ERR,
1309        };
1310        let a = match unsafe { try_slice(aad, aad_len) } {
1311            Some(s) => s,
1312            None => return GMCRYPTO_ERR,
1313        };
1314        let p = match unsafe { try_slice(pt, pt_len) } {
1315            Some(s) => s,
1316            None => return GMCRYPTO_ERR,
1317        };
1318        let k_arr: &[u8; GMCRYPTO_SM4_KEY_SIZE] = match k.try_into() {
1319            Ok(a) => a,
1320            Err(_) => return GMCRYPTO_ERR,
1321        };
1322        match mode_ccm::encrypt(k_arr, n, a, p, tag_len) {
1323            // SAFETY: out valid for out_capacity bytes; out_actual_len valid.
1324            Some(ct_with_tag) => unsafe {
1325                write_output(&ct_with_tag, out, out_capacity, out_actual_len)
1326            },
1327            None => GMCRYPTO_ERR,
1328        }
1329    })
1330}
1331
1332/// SM4-CCM single-shot decrypt. Input `ct` is `ct_len` bytes
1333/// (`ciphertext ‖ tag`); `tag_len` must match the value used at
1334/// encrypt time. `pt_out` receives `ct_len - tag_len` bytes.
1335/// [`GMCRYPTO_ERR`] on any failure (single failure mode).
1336#[allow(clippy::too_many_arguments)]
1337#[unsafe(no_mangle)]
1338pub unsafe extern "C" fn gmcrypto_sm4_ccm_decrypt(
1339    key: *const u8,
1340    nonce: *const u8,
1341    nonce_len: usize,
1342    aad: *const u8,
1343    aad_len: usize,
1344    ct: *const u8,
1345    ct_len: usize,
1346    tag_len: usize,
1347    pt_out: *mut u8,
1348    pt_capacity: usize,
1349    pt_actual_len: *mut usize,
1350) -> c_int {
1351    ffi_guard(|| {
1352        let k = match unsafe { try_slice(key, GMCRYPTO_SM4_KEY_SIZE) } {
1353            Some(s) => s,
1354            None => return GMCRYPTO_ERR,
1355        };
1356        let n = match unsafe { try_slice(nonce, nonce_len) } {
1357            Some(s) => s,
1358            None => return GMCRYPTO_ERR,
1359        };
1360        let a = match unsafe { try_slice(aad, aad_len) } {
1361            Some(s) => s,
1362            None => return GMCRYPTO_ERR,
1363        };
1364        let c = match unsafe { try_slice(ct, ct_len) } {
1365            Some(s) => s,
1366            None => return GMCRYPTO_ERR,
1367        };
1368        let k_arr: &[u8; GMCRYPTO_SM4_KEY_SIZE] = match k.try_into() {
1369            Ok(a) => a,
1370            Err(_) => return GMCRYPTO_ERR,
1371        };
1372        match mode_ccm::decrypt(k_arr, n, a, c, tag_len) {
1373            // SAFETY: pt_out valid for pt_capacity bytes; pt_actual_len valid.
1374            Some(plaintext) => unsafe {
1375                write_output(&plaintext, pt_out, pt_capacity, pt_actual_len)
1376            },
1377            None => GMCRYPTO_ERR,
1378        }
1379    })
1380}
1381
1382// ============================================================
1383// SM4-XTS — single-shot tweakable mode (v0.13; GB/T 17964-2021).
1384//
1385// Wraps gmcrypto_core::sm4::mode_xts (always compiled into the C ABI as
1386// of v0.23 W3 / A-1). Stateless per call: key = 32 bytes (Key1‖Key2),
1387// tweak = 16 raw bytes, output length == data_len (length-preserving).
1388// Confidentiality only — NO authentication tag. Single GMCRYPTO_ERR on
1389// every failure (data_len ∉ [16, 16 MiB], Key1==Key2, null, or buffer
1390// too small). The constant-time Key1==Key2 reject + α-doubling live in
1391// core; this shim only reconstructs slices and forwards `None`.
1392// ============================================================
1393
1394/// SM4-XTS single-shot encrypt (GB/T 17964-2021, `xts_standard=GB`).
1395/// `key` is exactly [`GMCRYPTO_SM4_XTS_KEY_SIZE`] (32) bytes (`Key1 ‖
1396/// Key2`); `tweak` is exactly [`GMCRYPTO_SM4_BLOCK_SIZE`] (16) raw bytes
1397/// (the data-unit/sector identifier — caller-unique per key). `out`
1398/// receives `data_len` bytes (length-preserving) via the
1399/// capacity/actual-len convention. Returns [`GMCRYPTO_OK`] /
1400/// [`GMCRYPTO_ERR`] (single failure mode: `data_len` outside
1401/// `[16, 16 MiB]`, `Key1 == Key2`, null pointer, or buffer too small —
1402/// in which case `*out_actual_len` is set to the required length).
1403/// **Confidentiality only — SM4-XTS does not authenticate.**
1404#[unsafe(no_mangle)]
1405pub unsafe extern "C" fn gmcrypto_sm4_xts_encrypt(
1406    key: *const u8,
1407    tweak: *const u8,
1408    data: *const u8,
1409    data_len: usize,
1410    out: *mut u8,
1411    out_capacity: usize,
1412    out_actual_len: *mut usize,
1413) -> c_int {
1414    ffi_guard(|| {
1415        let k = match unsafe { try_slice(key, GMCRYPTO_SM4_XTS_KEY_SIZE) } {
1416            Some(s) => s,
1417            None => return GMCRYPTO_ERR,
1418        };
1419        let tw = match unsafe { try_slice(tweak, GMCRYPTO_SM4_BLOCK_SIZE) } {
1420            Some(s) => s,
1421            None => return GMCRYPTO_ERR,
1422        };
1423        let d = match unsafe { try_slice(data, data_len) } {
1424            Some(s) => s,
1425            None => return GMCRYPTO_ERR,
1426        };
1427        let k_arr: &[u8; GMCRYPTO_SM4_XTS_KEY_SIZE] = match k.try_into() {
1428            Ok(a) => a,
1429            Err(_) => return GMCRYPTO_ERR,
1430        };
1431        let tw_arr: &[u8; GMCRYPTO_SM4_BLOCK_SIZE] = match tw.try_into() {
1432            Ok(a) => a,
1433            Err(_) => return GMCRYPTO_ERR,
1434        };
1435        match mode_xts::encrypt(k_arr, tw_arr, d) {
1436            // SAFETY: out valid for out_capacity bytes; out_actual_len valid.
1437            Some(ct) => unsafe { write_output(&ct, out, out_capacity, out_actual_len) },
1438            None => GMCRYPTO_ERR,
1439        }
1440    })
1441}
1442
1443/// SM4-XTS single-shot decrypt (GB/T 17964-2021, `xts_standard=GB`).
1444/// Inverse of [`gmcrypto_sm4_xts_encrypt`] with the same argument shape;
1445/// `out` receives `data_len` bytes. Returns [`GMCRYPTO_OK`] /
1446/// [`GMCRYPTO_ERR`] (same single failure mode). XTS is unauthenticated,
1447/// so decrypt cannot detect tampering — it only fails on invalid
1448/// parameters (length / weak key / buffer).
1449#[unsafe(no_mangle)]
1450pub unsafe extern "C" fn gmcrypto_sm4_xts_decrypt(
1451    key: *const u8,
1452    tweak: *const u8,
1453    data: *const u8,
1454    data_len: usize,
1455    out: *mut u8,
1456    out_capacity: usize,
1457    out_actual_len: *mut usize,
1458) -> c_int {
1459    ffi_guard(|| {
1460        let k = match unsafe { try_slice(key, GMCRYPTO_SM4_XTS_KEY_SIZE) } {
1461            Some(s) => s,
1462            None => return GMCRYPTO_ERR,
1463        };
1464        let tw = match unsafe { try_slice(tweak, GMCRYPTO_SM4_BLOCK_SIZE) } {
1465            Some(s) => s,
1466            None => return GMCRYPTO_ERR,
1467        };
1468        let d = match unsafe { try_slice(data, data_len) } {
1469            Some(s) => s,
1470            None => return GMCRYPTO_ERR,
1471        };
1472        let k_arr: &[u8; GMCRYPTO_SM4_XTS_KEY_SIZE] = match k.try_into() {
1473            Ok(a) => a,
1474            Err(_) => return GMCRYPTO_ERR,
1475        };
1476        let tw_arr: &[u8; GMCRYPTO_SM4_BLOCK_SIZE] = match tw.try_into() {
1477            Ok(a) => a,
1478            Err(_) => return GMCRYPTO_ERR,
1479        };
1480        match mode_xts::decrypt(k_arr, tw_arr, d) {
1481            // SAFETY: out valid for out_capacity bytes; out_actual_len valid.
1482            Some(pt) => unsafe { write_output(&pt, out, out_capacity, out_actual_len) },
1483            None => GMCRYPTO_ERR,
1484        }
1485    })
1486}
1487
1488// ============================================================
1489// SM4-XTS — multi-sector (disk) helper (v0.16; GB/T 17964-2021).
1490//
1491// Wraps gmcrypto_core::sm4::mode_xts::{encrypt_sectors, decrypt_sectors}
1492// (always compiled into the C ABI as of v0.23 W3 / A-1). Encrypt/decrypt a
1493// contiguous run of equal-size sectors **in place**, deriving sector i's
1494// tweak as the little-endian 128-bit encoding of `start_sector + i` (the
1495// standard disk-XTS data-unit convention). Distinct shape from the
1496// single-shot XTS FFI above: no out/out_capacity/out_actual_len (the
1497// transform is in place and length-preserving), and `start_sector` is a
1498// uint64_t (the block-layer sector_t width; the core's u128 range is a
1499// Rust-only nicety no addressable device reaches). Whole-block sectors
1500// only (no ciphertext stealing). Single GMCRYPTO_ERR on every failure
1501// (sector_size ∉ [16, 16 MiB] or not a multiple of 16; buf_len not a whole
1502// multiple of sector_size; Key1 == Key2; null). All validation is
1503// pre-flighted in core before any mutation, so `buf` is left untouched on
1504// GMCRYPTO_ERR. Confidentiality only — NO authentication tag.
1505// ============================================================
1506
1507/// SM4-XTS in-place multi-sector encrypt (GB/T 17964-2021,
1508/// `xts_standard=GB`). `key` is exactly [`GMCRYPTO_SM4_XTS_KEY_SIZE`] (32)
1509/// bytes (`Key1 ‖ Key2`); `buf` is a contiguous run of `buf_len / sector_size`
1510/// equal-size sectors transformed **in place** (`buf_len` must be a whole
1511/// multiple of `sector_size`). Sector `i` is encrypted under
1512/// tweak = little-endian-128(`start_sector + i`) — the data-unit / LBA
1513/// convention; sector numbers must be unique within the XTS-key namespace
1514/// (caller's contract). `start_sector` is a `uint64_t` (LBA width), so the
1515/// addressable range is `[0, 2^64 − 1]`; for the full u128 sector space use
1516/// the Rust `mode_xts::encrypt_sectors` API. Returns [`GMCRYPTO_OK`] /
1517/// [`GMCRYPTO_ERR`] (single
1518/// failure mode: `sector_size` outside `[16, 16 MiB]` or not a multiple of 16,
1519/// `buf_len` not a whole multiple of `sector_size`, `Key1 == Key2`, or null
1520/// pointer). **`buf` is untouched on [`GMCRYPTO_ERR`].** `buf_len == 0` is a
1521/// vacuous [`GMCRYPTO_OK`] (but the key is still validated, so empty + weak key
1522/// → [`GMCRYPTO_ERR`]). **Confidentiality only — SM4-XTS does not
1523/// authenticate.**
1524#[unsafe(no_mangle)]
1525pub unsafe extern "C" fn gmcrypto_sm4_xts_encrypt_sectors(
1526    key: *const u8,
1527    sector_size: usize,
1528    start_sector: u64,
1529    buf: *mut u8,
1530    buf_len: usize,
1531) -> c_int {
1532    ffi_guard(|| {
1533        let k = match unsafe { try_slice(key, GMCRYPTO_SM4_XTS_KEY_SIZE) } {
1534            Some(s) => s,
1535            None => return GMCRYPTO_ERR,
1536        };
1537        // Copy the key into an OWNED array so no shared borrow of the key memory
1538        // is alive while `buf` is borrowed mutably below. This makes a
1539        // (caller-error) `key`/`buf` overlap a benign copy instead of `&`/`&mut`
1540        // aliasing UB — the in-place path is the only FFI surface that holds a
1541        // `&mut` over caller memory alongside the key. (W0 codex finding.)
1542        let key_owned: [u8; GMCRYPTO_SM4_XTS_KEY_SIZE] = match k.try_into() {
1543            Ok(a) => a,
1544            Err(_) => return GMCRYPTO_ERR,
1545        };
1546        // SAFETY: buf valid for read+write of buf_len bytes, or buf_len == 0.
1547        let b = match unsafe { try_slice_mut(buf, buf_len) } {
1548            Some(s) => s,
1549            None => return GMCRYPTO_ERR,
1550        };
1551        match mode_xts::encrypt_sectors(&key_owned, sector_size, u128::from(start_sector), b) {
1552            Some(()) => GMCRYPTO_OK,
1553            None => GMCRYPTO_ERR,
1554        }
1555    })
1556}
1557
1558/// SM4-XTS in-place multi-sector decrypt (GB/T 17964-2021, `xts_standard=GB`).
1559/// Inverse of [`gmcrypto_sm4_xts_encrypt_sectors`] under the same
1560/// `(key, sector_size, start_sector)`; same in-place contract, single failure
1561/// mode, and `buf`-untouched-on-error guarantee. XTS is unauthenticated, so
1562/// decrypt cannot detect tampering — it only fails on invalid parameters.
1563#[unsafe(no_mangle)]
1564pub unsafe extern "C" fn gmcrypto_sm4_xts_decrypt_sectors(
1565    key: *const u8,
1566    sector_size: usize,
1567    start_sector: u64,
1568    buf: *mut u8,
1569    buf_len: usize,
1570) -> c_int {
1571    ffi_guard(|| {
1572        let k = match unsafe { try_slice(key, GMCRYPTO_SM4_XTS_KEY_SIZE) } {
1573            Some(s) => s,
1574            None => return GMCRYPTO_ERR,
1575        };
1576        // Copy the key into an OWNED array so no shared borrow of the key memory
1577        // is alive while `buf` is borrowed mutably below (see the encrypt-side
1578        // note — avoids `&`/`&mut` aliasing UB on a caller `key`/`buf` overlap).
1579        let key_owned: [u8; GMCRYPTO_SM4_XTS_KEY_SIZE] = match k.try_into() {
1580            Ok(a) => a,
1581            Err(_) => return GMCRYPTO_ERR,
1582        };
1583        // SAFETY: buf valid for read+write of buf_len bytes, or buf_len == 0.
1584        let b = match unsafe { try_slice_mut(buf, buf_len) } {
1585            Some(s) => s,
1586            None => return GMCRYPTO_ERR,
1587        };
1588        match mode_xts::decrypt_sectors(&key_owned, sector_size, u128::from(start_sector), b) {
1589            Some(()) => GMCRYPTO_OK,
1590            None => GMCRYPTO_ERR,
1591        }
1592    })
1593}
1594
1595// ============================================================
1596// SM4-GCM AEAD — streaming / incremental-input (v0.10 W1+W2).
1597//
1598// Wraps gmcrypto_core::sm4::{Sm4GcmEncryptor, Sm4GcmDecryptor}.
1599// Lifecycle mirrors the v0.5 CBC-streaming handles: `_new` ->
1600// Box::into_raw; `_update` -> &mut *; `_finalize*` -> Box::from_raw
1601// (consume + free); `_free` is the abort path (no-op on NULL). Single
1602// GMCRYPTO_ERR on every error. Asymmetry: the encryptor `_update`
1603// emits ciphertext (out triple); the decryptor `_update` emits NOTHING
1604// (commit-on-verify) and plaintext is released only by
1605// `_finalize_verify` after a constant-time tag check. Streaming CCM is
1606// out of scope (CBC-MAC needs total length up-front). See
1607// docs/v0.10-scope.md.
1608// ============================================================
1609
1610/// Construct a streaming SM4-GCM encryptor. `key` is exactly 16 bytes;
1611/// `nonce` is `nonce_len` bytes (12 = canonical; other lengths invoke
1612/// the extra GHASH J0-derivation per NIST SP 800-38D §8.2.2); `aad` is
1613/// the full associated data (the message header, supplied up-front).
1614/// Returns NULL on invalid pointer/length input. **Nonce uniqueness is
1615/// the caller's responsibility** — reusing `(key, nonce)` is
1616/// catastrophic for GCM.
1617#[unsafe(no_mangle)]
1618pub unsafe extern "C" fn gmcrypto_sm4_gcm_encryptor_new(
1619    key: *const u8,
1620    nonce: *const u8,
1621    nonce_len: usize,
1622    aad: *const u8,
1623    aad_len: usize,
1624) -> *mut gmcrypto_sm4_gcm_encryptor_t {
1625    let result = std::panic::catch_unwind(|| {
1626        let k = unsafe { try_slice(key, GMCRYPTO_SM4_KEY_SIZE) }?;
1627        let n = unsafe { try_slice(nonce, nonce_len) }?;
1628        let a = unsafe { try_slice(aad, aad_len) }?;
1629        let k_arr: &[u8; GMCRYPTO_SM4_KEY_SIZE] = k.try_into().ok()?;
1630        Some(Box::into_raw(Box::new(gmcrypto_sm4_gcm_encryptor_t {
1631            inner: InnerSm4GcmEnc::new(k_arr, n, a),
1632        })))
1633    });
1634    match result {
1635        Ok(Some(p)) => p,
1636        _ => ptr::null_mut(),
1637    }
1638}
1639
1640/// Encrypt `pt_len` bytes of plaintext, emitting the ciphertext for
1641/// this chunk (length == `pt_len`; GCM does not pad or buffer). The
1642/// `out` buffer MUST be at least `pt_len` bytes; on insufficient
1643/// capacity returns [`GMCRYPTO_ERR`] (and the required length is written
1644/// to `*out_actual_len`), and the encryptor state is left mid-stream
1645/// (the chunk's ciphertext is lost — size the buffer correctly).
1646/// Returns [`GMCRYPTO_ERR`] once the cumulative plaintext would exceed
1647/// the GCM ceiling (`2^36 − 32` bytes); the encryptor is poisoned and
1648/// all later calls also return [`GMCRYPTO_ERR`].
1649#[unsafe(no_mangle)]
1650pub unsafe extern "C" fn gmcrypto_sm4_gcm_encryptor_update(
1651    enc: *mut gmcrypto_sm4_gcm_encryptor_t,
1652    pt: *const u8,
1653    pt_len: usize,
1654    out: *mut u8,
1655    out_capacity: usize,
1656    out_actual_len: *mut usize,
1657) -> c_int {
1658    ffi_guard(|| {
1659        if enc.is_null() {
1660            return GMCRYPTO_ERR;
1661        }
1662        let input = match unsafe { try_slice(pt, pt_len) } {
1663            Some(s) => s,
1664            None => return GMCRYPTO_ERR,
1665        };
1666        // SAFETY: enc non-null per the check; caller guarantees unique
1667        // access for the duration of this call.
1668        let e = unsafe { &mut *enc };
1669        match e.inner.update(input) {
1670            // SAFETY: out valid for out_capacity; out_actual_len valid.
1671            Some(ct) => unsafe { write_output(&ct, out, out_capacity, out_actual_len) },
1672            None => GMCRYPTO_ERR, // length-ceiling overflow → poisoned
1673        }
1674    })
1675}
1676
1677/// Finish and emit the full 16-byte tag. **Consumes the encryptor —
1678/// the handle is freed by this call** (even on error); do NOT call
1679/// [`gmcrypto_sm4_gcm_encryptor_free`] on it afterwards. `tag_out`
1680/// must be valid for exactly 16 bytes.
1681#[unsafe(no_mangle)]
1682pub unsafe extern "C" fn gmcrypto_sm4_gcm_encryptor_finalize(
1683    enc: *mut gmcrypto_sm4_gcm_encryptor_t,
1684    tag_out: *mut u8,
1685) -> c_int {
1686    ffi_guard(|| {
1687        if enc.is_null() {
1688            return GMCRYPTO_ERR;
1689        }
1690        // SAFETY: enc came from Box::into_raw; take ownership + free
1691        // (consumed even if tag_out is invalid, per the CBC precedent).
1692        let boxed = unsafe { Box::from_raw(enc) };
1693        let tag = boxed.inner.finalize();
1694        // SAFETY: caller guarantees tag_out valid for 16 bytes.
1695        let tag_dst = match unsafe { try_slice_mut(tag_out, GMCRYPTO_SM4_BLOCK_SIZE) } {
1696            Some(s) => s,
1697            None => return GMCRYPTO_ERR,
1698        };
1699        tag_dst.copy_from_slice(&tag);
1700        GMCRYPTO_OK
1701    })
1702}
1703
1704/// Finish and emit a truncated tag of `tag_len` bytes (`MSB_t` per NIST
1705/// SP 800-38D §5.2.1.2). `tag_len` must be in `{4, 8, 12, 13, 14, 15,
1706/// 16}` (else [`GMCRYPTO_ERR`]). **Consumes the encryptor — the handle
1707/// is freed by this call** (even on error); do NOT call
1708/// [`gmcrypto_sm4_gcm_encryptor_free`] afterwards.
1709#[unsafe(no_mangle)]
1710pub unsafe extern "C" fn gmcrypto_sm4_gcm_encryptor_finalize_with_tag_len(
1711    enc: *mut gmcrypto_sm4_gcm_encryptor_t,
1712    tag_len: usize,
1713    out: *mut u8,
1714    out_capacity: usize,
1715    out_actual_len: *mut usize,
1716) -> c_int {
1717    ffi_guard(|| {
1718        if enc.is_null() {
1719            return GMCRYPTO_ERR;
1720        }
1721        // SAFETY: enc came from Box::into_raw; take ownership + free
1722        // (consumed even on invalid tag_len — the handle is spent).
1723        let boxed = unsafe { Box::from_raw(enc) };
1724        let tl = match GcmTagLen::new(tag_len) {
1725            Some(t) => t,
1726            None => return GMCRYPTO_ERR, // boxed dropped here → freed
1727        };
1728        let tag = boxed.inner.finalize_with_tag_len(tl);
1729        // SAFETY: out valid for out_capacity; out_actual_len valid.
1730        unsafe { write_output(&tag, out, out_capacity, out_actual_len) }
1731    })
1732}
1733
1734/// Free a streaming SM4-GCM encryptor without finalizing (abort path).
1735/// Passing NULL is a no-op. Do NOT call after any `_finalize*` — those
1736/// already consumed the handle.
1737#[unsafe(no_mangle)]
1738pub unsafe extern "C" fn gmcrypto_sm4_gcm_encryptor_free(enc: *mut gmcrypto_sm4_gcm_encryptor_t) {
1739    if enc.is_null() {
1740        return;
1741    }
1742    // SAFETY: enc came from Box::into_raw and has not been freed.
1743    drop(unsafe { Box::from_raw(enc) });
1744}
1745
1746/// Construct a streaming SM4-GCM decryptor. Same parameter contract as
1747/// [`gmcrypto_sm4_gcm_encryptor_new`]. Returns NULL on invalid input.
1748#[unsafe(no_mangle)]
1749pub unsafe extern "C" fn gmcrypto_sm4_gcm_decryptor_new(
1750    key: *const u8,
1751    nonce: *const u8,
1752    nonce_len: usize,
1753    aad: *const u8,
1754    aad_len: usize,
1755) -> *mut gmcrypto_sm4_gcm_decryptor_t {
1756    let result = std::panic::catch_unwind(|| {
1757        let k = unsafe { try_slice(key, GMCRYPTO_SM4_KEY_SIZE) }?;
1758        let n = unsafe { try_slice(nonce, nonce_len) }?;
1759        let a = unsafe { try_slice(aad, aad_len) }?;
1760        let k_arr: &[u8; GMCRYPTO_SM4_KEY_SIZE] = k.try_into().ok()?;
1761        Some(Box::into_raw(Box::new(gmcrypto_sm4_gcm_decryptor_t {
1762            inner: InnerSm4GcmDec::new(k_arr, n, a),
1763        })))
1764    });
1765    match result {
1766        Ok(Some(p)) => p,
1767        _ => ptr::null_mut(),
1768    }
1769}
1770
1771/// Buffer `ct_len` bytes of ciphertext and fold them into the running
1772/// GHASH. **Emits no plaintext** (commit-on-verify) — there is no
1773/// output parameter. Returns [`GMCRYPTO_ERR`] only on null handle or
1774/// invalid input pointer; a length-ceiling overflow is latched and
1775/// surfaces as [`GMCRYPTO_ERR`] at
1776/// [`gmcrypto_sm4_gcm_decryptor_finalize_verify`].
1777#[unsafe(no_mangle)]
1778pub unsafe extern "C" fn gmcrypto_sm4_gcm_decryptor_update(
1779    dec: *mut gmcrypto_sm4_gcm_decryptor_t,
1780    ct: *const u8,
1781    ct_len: usize,
1782) -> c_int {
1783    ffi_guard(|| {
1784        if dec.is_null() {
1785            return GMCRYPTO_ERR;
1786        }
1787        let input = match unsafe { try_slice(ct, ct_len) } {
1788            Some(s) => s,
1789            None => return GMCRYPTO_ERR,
1790        };
1791        // SAFETY: dec non-null per the check; unique access for the call.
1792        let d = unsafe { &mut *dec };
1793        d.inner.update(input);
1794        GMCRYPTO_OK
1795    })
1796}
1797
1798/// Verify `tag` (`tag_len` bytes; the length is validated against the
1799/// NIST-permitted set `{4, 8, 12, 13, 14, 15, 16}`) and, on success,
1800/// write the full decrypted plaintext (length == total ciphertext fed)
1801/// to `(out, out_capacity, out_actual_len)`.
1802///
1803/// Returns [`GMCRYPTO_ERR`] in two cases, both of which still **consume
1804/// and free the handle** (do NOT call
1805/// [`gmcrypto_sm4_gcm_decryptor_free`] afterwards):
1806///
1807/// - **Verification failure** (tag mismatch, invalid `tag_len`, or
1808///   length-ceiling overflow): `*out_actual_len` is `0` and no
1809///   plaintext is written (commit-on-verify; single failure mode).
1810/// - **Tag verified but `out` too small**: `*out_actual_len` is set to
1811///   the required plaintext length and no plaintext is written. The
1812///   handle is consumed, so you cannot retry — size `out` to the total
1813///   ciphertext length up-front (GCM plaintext is the same length).
1814#[allow(clippy::too_many_arguments)]
1815#[unsafe(no_mangle)]
1816pub unsafe extern "C" fn gmcrypto_sm4_gcm_decryptor_finalize_verify(
1817    dec: *mut gmcrypto_sm4_gcm_decryptor_t,
1818    tag: *const u8,
1819    tag_len: usize,
1820    out: *mut u8,
1821    out_capacity: usize,
1822    out_actual_len: *mut usize,
1823) -> c_int {
1824    ffi_guard(|| {
1825        if dec.is_null() {
1826            return GMCRYPTO_ERR;
1827        }
1828        // Default the reported length to 0 up-front so *every* failure path
1829        // (bad tag pointer, tag mismatch, invalid tag_len, overflow)
1830        // satisfies the "finalize failure writes 0 to *out_actual_len"
1831        // boundary invariant. On success / capacity-retry, write_output
1832        // overwrites this with the produced / required length.
1833        if !out_actual_len.is_null() {
1834            // SAFETY: caller-asserted valid *mut usize.
1835            unsafe { ptr::write(out_actual_len, 0) };
1836        }
1837        // SAFETY: dec came from Box::into_raw; take ownership + free.
1838        let boxed = unsafe { Box::from_raw(dec) };
1839        let t = match unsafe { try_slice(tag, tag_len) } {
1840            Some(s) => s,
1841            None => return GMCRYPTO_ERR, // boxed dropped → freed; len already 0
1842        };
1843        if let Some(pt) = boxed.inner.finalize_verify(t) {
1844            // SAFETY: out valid for out_capacity; out_actual_len valid.
1845            unsafe { write_output(&pt, out, out_capacity, out_actual_len) }
1846        } else {
1847            GMCRYPTO_ERR // commit-on-verify failure; len already 0
1848        }
1849    })
1850}
1851
1852/// Free a streaming SM4-GCM decryptor without verifying (abort path).
1853/// NULL is a no-op. Do NOT call after
1854/// [`gmcrypto_sm4_gcm_decryptor_finalize_verify`].
1855#[unsafe(no_mangle)]
1856pub unsafe extern "C" fn gmcrypto_sm4_gcm_decryptor_free(dec: *mut gmcrypto_sm4_gcm_decryptor_t) {
1857    if dec.is_null() {
1858        return;
1859    }
1860    // SAFETY: dec came from Box::into_raw and has not been freed.
1861    drop(unsafe { Box::from_raw(dec) });
1862}
1863
1864// ============================================================
1865// SM2 key construction + I/O.
1866// ============================================================
1867
1868/// Construct an SM2 private key from a 32-byte big-endian scalar.
1869/// Returns NULL on out-of-range scalar (must be in `[1, n-2]`).
1870#[unsafe(no_mangle)]
1871pub unsafe extern "C" fn gmcrypto_sm2_privkey_new(d_be: *const u8) -> *mut gmcrypto_sm2_privkey_t {
1872    let result = std::panic::catch_unwind(|| {
1873        let bytes = unsafe { try_slice(d_be, GMCRYPTO_SM2_SCALAR_SIZE) }?;
1874        let arr: &[u8; GMCRYPTO_SM2_SCALAR_SIZE] = bytes.try_into().ok()?;
1875        // Use the byte-array import path — does the constant-time
1876        // `[1, n-2]` range check via `Sm2PrivateKey::from_bytes_be`
1877        // (renamed from `from_sec1_be` in v0.5 W5; the FFI symbol
1878        // name `gmcrypto_sm2_privkey_new` is unchanged for C ABI
1879        // backcompat).
1880        let key_opt: Option<Sm2PrivateKey> = Sm2PrivateKey::from_bytes_be(arr).into_option();
1881        key_opt.map(|inner| Box::into_raw(Box::new(gmcrypto_sm2_privkey_t { inner })))
1882    });
1883    match result {
1884        Ok(Some(p)) => p,
1885        _ => ptr::null_mut(),
1886    }
1887}
1888
1889/// Construct an SM2 public key from a SEC1 uncompressed-point byte
1890/// string (`04 || X || Y`, 65 bytes). Returns NULL on
1891/// invalid input (off-curve, identity point, non-uncompressed
1892/// prefix).
1893#[unsafe(no_mangle)]
1894pub unsafe extern "C" fn gmcrypto_sm2_pubkey_new(
1895    sec1_uncompressed: *const u8,
1896) -> *mut gmcrypto_sm2_pubkey_t {
1897    let result = std::panic::catch_unwind(|| {
1898        let bytes = unsafe { try_slice(sec1_uncompressed, GMCRYPTO_SM2_SEC1_UNCOMPRESSED_SIZE) }?;
1899        let key = Sm2PublicKey::from_sec1_bytes(bytes)?;
1900        Some(Box::into_raw(Box::new(gmcrypto_sm2_pubkey_t {
1901            inner: key,
1902        })))
1903    });
1904    match result {
1905        Ok(Some(p)) => p,
1906        _ => ptr::null_mut(),
1907    }
1908}
1909
1910/// Export the SM2 private key as a 32-byte big-endian scalar.
1911///
1912/// **Caller MUST zeroize the output buffer** after use. Per Q4.19,
1913/// this entry point exists as `#[doc(hidden)]`-equivalent on the
1914/// Rust side and is NOT SemVer-stable across v0.4.x.
1915#[unsafe(no_mangle)]
1916pub unsafe extern "C" fn gmcrypto_sm2_privkey_to_sec1_be(
1917    key: *const gmcrypto_sm2_privkey_t,
1918    out: *mut u8,
1919) -> c_int {
1920    ffi_guard(|| {
1921        if key.is_null() {
1922            return GMCRYPTO_ERR;
1923        }
1924        let o = match unsafe { try_slice_mut(out, GMCRYPTO_SM2_SCALAR_SIZE) } {
1925            Some(s) => s,
1926            None => return GMCRYPTO_ERR,
1927        };
1928        let k = unsafe { &*key };
1929        // `to_bytes_be` is the v0.5 W5 rename of v0.3's
1930        // `#[doc(hidden)] pub fn to_sec1_be(&self)` (now SemVer-
1931        // stable). The FFI symbol name keeps the `sec1` suffix for
1932        // C ABI backcompat.
1933        let bytes = k.inner.to_bytes_be();
1934        o.copy_from_slice(&bytes);
1935        // The caller is responsible for zeroizing `out`. The
1936        // temporary `bytes` is a `[u8; 32]` on the stack; Rust's
1937        // stack lifetime is the wipe boundary.
1938        GMCRYPTO_OK
1939    })
1940}
1941
1942/// Export the SM2 public key as a SEC1 uncompressed-point byte
1943/// string (`04 || X || Y`, 65 bytes).
1944#[unsafe(no_mangle)]
1945pub unsafe extern "C" fn gmcrypto_sm2_pubkey_to_sec1_uncompressed(
1946    key: *const gmcrypto_sm2_pubkey_t,
1947    out: *mut u8,
1948) -> c_int {
1949    ffi_guard(|| {
1950        if key.is_null() {
1951            return GMCRYPTO_ERR;
1952        }
1953        let o = match unsafe { try_slice_mut(out, GMCRYPTO_SM2_SEC1_UNCOMPRESSED_SIZE) } {
1954            Some(s) => s,
1955            None => return GMCRYPTO_ERR,
1956        };
1957        let k = unsafe { &*key };
1958        let bytes = k.inner.to_sec1_uncompressed();
1959        o.copy_from_slice(&bytes);
1960        GMCRYPTO_OK
1961    })
1962}
1963
1964/// Free an SM2 private key. NULL is a no-op. The inner scalar is
1965/// zeroized via `ZeroizeOnDrop` before the heap slot is freed.
1966#[unsafe(no_mangle)]
1967pub unsafe extern "C" fn gmcrypto_sm2_privkey_free(key: *mut gmcrypto_sm2_privkey_t) {
1968    if key.is_null() {
1969        return;
1970    }
1971    drop(unsafe { Box::from_raw(key) });
1972}
1973
1974/// Free an SM2 public key. NULL is a no-op.
1975#[unsafe(no_mangle)]
1976pub unsafe extern "C" fn gmcrypto_sm2_pubkey_free(key: *mut gmcrypto_sm2_pubkey_t) {
1977    if key.is_null() {
1978        return;
1979    }
1980    drop(unsafe { Box::from_raw(key) });
1981}
1982
1983/// Emit a password-encrypted PKCS#8 PEM blob containing the SM2
1984/// private key. PBES2 / PBKDF2-HMAC-SM3 / SM4-CBC per RFC 8018.
1985#[unsafe(no_mangle)]
1986pub unsafe extern "C" fn gmcrypto_sm2_privkey_to_pkcs8(
1987    key: *const gmcrypto_sm2_privkey_t,
1988    password: *const u8,
1989    pwd_len: usize,
1990    pbkdf2_iters: u32,
1991    out_pem: *mut u8,
1992    out_capacity: usize,
1993    out_actual_len: *mut usize,
1994) -> c_int {
1995    ffi_guard(|| {
1996        if key.is_null() {
1997            return GMCRYPTO_ERR;
1998        }
1999        let pwd = match unsafe { try_slice(password, pwd_len) } {
2000            Some(s) => s,
2001            None => return GMCRYPTO_ERR,
2002        };
2003        let k = unsafe { &*key };
2004        // Generate a fresh 16-byte salt and IV from SysRng. PBKDF2's
2005        // salt is public; SM4-CBC's IV must be unpredictable (NIST
2006        // SP 800-38A Appendix C). SysRng satisfies both.
2007        let mut salt = [0u8; 16];
2008        let mut iv = [0u8; 16];
2009        if getrandom::SysRng.try_fill_bytes(&mut salt).is_err() {
2010            return GMCRYPTO_ERR;
2011        }
2012        if getrandom::SysRng.try_fill_bytes(&mut iv).is_err() {
2013            return GMCRYPTO_ERR;
2014        }
2015        let der = match pkcs8::encrypt(&k.inner, pwd, &salt, pbkdf2_iters, &iv) {
2016            Ok(d) => d,
2017            Err(_) => return GMCRYPTO_ERR,
2018        };
2019        let pem_blob = pem::encode("ENCRYPTED PRIVATE KEY", &der);
2020        unsafe { write_output(pem_blob.as_bytes(), out_pem, out_capacity, out_actual_len) }
2021    })
2022}
2023
2024/// Load an SM2 private key from a password-encrypted PKCS#8 PEM blob.
2025/// On success, writes the new handle to `*out_key` and returns
2026/// [`GMCRYPTO_OK`]. Caller MUST free via [`gmcrypto_sm2_privkey_free`].
2027#[unsafe(no_mangle)]
2028pub unsafe extern "C" fn gmcrypto_sm2_privkey_from_pkcs8(
2029    pem: *const u8,
2030    pem_len: usize,
2031    password: *const u8,
2032    pwd_len: usize,
2033    out_key: *mut *mut gmcrypto_sm2_privkey_t,
2034) -> c_int {
2035    ffi_guard(|| {
2036        if out_key.is_null() {
2037            return GMCRYPTO_ERR;
2038        }
2039        let pem_bytes = match unsafe { try_slice(pem, pem_len) } {
2040            Some(s) => s,
2041            None => return GMCRYPTO_ERR,
2042        };
2043        let pwd = match unsafe { try_slice(password, pwd_len) } {
2044            Some(s) => s,
2045            None => return GMCRYPTO_ERR,
2046        };
2047        let pem_str = match core::str::from_utf8(pem_bytes) {
2048            Ok(s) => s,
2049            Err(_) => return GMCRYPTO_ERR,
2050        };
2051        let der = match pem::decode(pem_str, "ENCRYPTED PRIVATE KEY") {
2052            Ok(d) => d,
2053            Err(_) => return GMCRYPTO_ERR,
2054        };
2055        let key = match pkcs8::decrypt(&der, pwd) {
2056            Ok(k) => k,
2057            Err(_) => return GMCRYPTO_ERR,
2058        };
2059        let boxed = Box::into_raw(Box::new(gmcrypto_sm2_privkey_t { inner: key }));
2060        // SAFETY: out_key non-null per check above.
2061        unsafe { ptr::write(out_key, boxed) };
2062        GMCRYPTO_OK
2063    })
2064}
2065
2066// ============================================================
2067// SM2 — sign / verify / encrypt / decrypt.
2068// ============================================================
2069
2070/// Sign `msg` with the SM2 private key using the supplied
2071/// `signer_id` (or [`DEFAULT_SIGNER_ID`] = `"1234567812345678"` if
2072/// `signer_id_len == 0`). Output is DER-encoded
2073/// `SEQUENCE { r, s }`. RNG is sourced from `getrandom::SysRng`.
2074///
2075/// May return [`GMCRYPTO_ERR`] if the system RNG fails (in addition to the
2076/// usual null / short-buffer errors); the error is terminal — do not retry
2077/// on the same inputs expecting success.
2078#[unsafe(no_mangle)]
2079pub unsafe extern "C" fn gmcrypto_sm2_sign(
2080    key: *const gmcrypto_sm2_privkey_t,
2081    signer_id: *const u8,
2082    signer_id_len: usize,
2083    msg: *const u8,
2084    msg_len: usize,
2085    out_der_sig: *mut u8,
2086    out_capacity: usize,
2087    out_actual_len: *mut usize,
2088) -> c_int {
2089    ffi_guard(|| {
2090        if key.is_null() {
2091            return GMCRYPTO_ERR;
2092        }
2093        let id: &[u8] = if signer_id_len == 0 {
2094            DEFAULT_SIGNER_ID
2095        } else {
2096            match unsafe { try_slice(signer_id, signer_id_len) } {
2097                Some(s) => s,
2098                None => return GMCRYPTO_ERR,
2099            }
2100        };
2101        let m = match unsafe { try_slice(msg, msg_len) } {
2102            Some(s) => s,
2103            None => return GMCRYPTO_ERR,
2104        };
2105        let k = unsafe { &*key };
2106        let mut rng = getrandom::SysRng;
2107        let sig = match sign_with_id(&k.inner, id, m, &mut rng) {
2108            Ok(s) => s,
2109            Err(_) => return GMCRYPTO_ERR,
2110        };
2111        unsafe { write_output(&sig, out_der_sig, out_capacity, out_actual_len) }
2112    })
2113}
2114
2115/// Verify a DER-encoded `(r, s)` signature against `msg` using the
2116/// SM2 public key and `signer_id`. Returns [`GMCRYPTO_OK`] on
2117/// valid; [`GMCRYPTO_ERR`] on invalid or any error.
2118#[unsafe(no_mangle)]
2119pub unsafe extern "C" fn gmcrypto_sm2_verify(
2120    key: *const gmcrypto_sm2_pubkey_t,
2121    signer_id: *const u8,
2122    signer_id_len: usize,
2123    msg: *const u8,
2124    msg_len: usize,
2125    der_sig: *const u8,
2126    der_sig_len: usize,
2127) -> c_int {
2128    ffi_guard(|| {
2129        if key.is_null() {
2130            return GMCRYPTO_ERR;
2131        }
2132        let id: &[u8] = if signer_id_len == 0 {
2133            DEFAULT_SIGNER_ID
2134        } else {
2135            match unsafe { try_slice(signer_id, signer_id_len) } {
2136                Some(s) => s,
2137                None => return GMCRYPTO_ERR,
2138            }
2139        };
2140        let m = match unsafe { try_slice(msg, msg_len) } {
2141            Some(s) => s,
2142            None => return GMCRYPTO_ERR,
2143        };
2144        let sig = match unsafe { try_slice(der_sig, der_sig_len) } {
2145            Some(s) => s,
2146            None => return GMCRYPTO_ERR,
2147        };
2148        let k = unsafe { &*key };
2149        if verify_with_id(&k.inner, id, m, sig) {
2150            GMCRYPTO_OK
2151        } else {
2152            GMCRYPTO_ERR
2153        }
2154    })
2155}
2156
2157/// SM2 public-key encrypt. Output is GM/T 0009-2012 DER. RNG from
2158/// `getrandom::SysRng`.
2159///
2160/// May return [`GMCRYPTO_ERR`] if the system RNG fails (in addition to the
2161/// usual null / short-buffer errors); the error is terminal.
2162#[unsafe(no_mangle)]
2163pub unsafe extern "C" fn gmcrypto_sm2_encrypt(
2164    key: *const gmcrypto_sm2_pubkey_t,
2165    pt: *const u8,
2166    pt_len: usize,
2167    out_der_ct: *mut u8,
2168    out_capacity: usize,
2169    out_actual_len: *mut usize,
2170) -> c_int {
2171    ffi_guard(|| {
2172        if key.is_null() {
2173            return GMCRYPTO_ERR;
2174        }
2175        let p = match unsafe { try_slice(pt, pt_len) } {
2176            Some(s) => s,
2177            None => return GMCRYPTO_ERR,
2178        };
2179        let k = unsafe { &*key };
2180        let mut rng = getrandom::SysRng;
2181        let ct = match sm2_encrypt(&k.inner, p, &mut rng) {
2182            Ok(c) => c,
2183            Err(_) => return GMCRYPTO_ERR,
2184        };
2185        unsafe { write_output(&ct, out_der_ct, out_capacity, out_actual_len) }
2186    })
2187}
2188
2189// ============================================================
2190// v0.5 W3 — Caller-supplied RNG callback adapter.
2191//
2192// C ABI:
2193//   typedef int (*gmcrypto_rng_callback)(
2194//       void *context,
2195//       uint8_t *buf,
2196//       size_t buf_len);
2197//
2198// Contract per Q5.6 / Q5.8 / Q5.9:
2199//   - Returns 0 on success, non-zero on failure. On failure, the
2200//     enclosing `gmcrypto_sm2_*_with_rng` call returns
2201//     GMCRYPTO_FAILED.
2202//   - The `context` pointer is opaque to gmcrypto-c — callers stash
2203//     HSM session handles, SDF/SKF context, whatever is needed.
2204//   - Callbacks MUST NOT call back into gmcrypto-c (no re-entrancy).
2205//     The Rust `Rng` adapter does not hold any locks across the
2206//     callback; re-entrancy is technically safe but policy is "don't"
2207//     for clarity. No runtime check in v0.5; may add a debug-build
2208//     assertion in v0.6.
2209//   - The buffer MUST be fully filled with `buf_len` random bytes
2210//     before the callback returns 0. Partial fills are caller error
2211//     and will produce incorrect cryptographic output.
2212// ============================================================
2213
2214/// C ABI function pointer for caller-supplied RNG. Returns `0` on
2215/// success and non-zero on failure. See module-level docs for the
2216/// full contract.
2217pub type gmcrypto_rng_callback =
2218    Option<unsafe extern "C" fn(context: *mut c_void, buf: *mut u8, buf_len: usize) -> c_int>;
2219
2220/// Tiny error type — only used internally for the `TryRng` impl;
2221/// never crosses the FFI boundary. The callback's non-zero return is
2222/// erased to a single `GMCRYPTO_FAILED` per the failure-mode invariant.
2223#[derive(Debug)]
2224struct CallbackRngError;
2225
2226impl core::fmt::Display for CallbackRngError {
2227    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
2228        f.write_str("callback returned non-zero")
2229    }
2230}
2231
2232impl core::error::Error for CallbackRngError {}
2233
2234/// Bridge from the C ABI function pointer + opaque context to
2235/// `rand_core::TryRng + TryCryptoRng`. Passed **directly** (no
2236/// `UnwrapErr`) to the core `sign_with_id` / `encrypt`, whose public
2237/// bound is now the fallible `TryCryptoRng` (v0.23): a callback failure
2238/// surfaces as `Err(CallbackRngError)`, the core collapses it to
2239/// `Error::Failed`, and the FFI maps that to `GMCRYPTO_FAILED` — a
2240/// defined, no-panic RNG-failure path (the previous design panicked via
2241/// `UnwrapErr` and relied on `ffi_guard` to catch it).
2242struct CallbackRng {
2243    callback: unsafe extern "C" fn(context: *mut c_void, buf: *mut u8, buf_len: usize) -> c_int,
2244    context: *mut c_void,
2245}
2246
2247impl rand_core::TryRng for CallbackRng {
2248    type Error = CallbackRngError;
2249
2250    fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
2251        // SAFETY: caller of the FFI fn guarantees the callback is
2252        // either null (rejected upstream) or a valid function pointer.
2253        // `dst` is a valid mutable slice and `dst.len()` is its length.
2254        let rc = unsafe { (self.callback)(self.context, dst.as_mut_ptr(), dst.len()) };
2255        if rc == 0 {
2256            Ok(())
2257        } else {
2258            Err(CallbackRngError)
2259        }
2260    }
2261
2262    fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
2263        let mut buf = [0u8; 4];
2264        self.try_fill_bytes(&mut buf)?;
2265        Ok(u32::from_le_bytes(buf))
2266    }
2267
2268    fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
2269        let mut buf = [0u8; 8];
2270        self.try_fill_bytes(&mut buf)?;
2271        Ok(u64::from_le_bytes(buf))
2272    }
2273}
2274
2275// Trust the caller: their RNG is suitable for cryptographic use.
2276impl rand_core::TryCryptoRng for CallbackRng {}
2277
2278/// SM2 private-key decrypt of a GM/T 0009-2012 DER ciphertext.
2279#[unsafe(no_mangle)]
2280pub unsafe extern "C" fn gmcrypto_sm2_decrypt(
2281    key: *const gmcrypto_sm2_privkey_t,
2282    der_ct: *const u8,
2283    der_ct_len: usize,
2284    out_pt: *mut u8,
2285    out_capacity: usize,
2286    out_actual_len: *mut usize,
2287) -> c_int {
2288    ffi_guard(|| {
2289        if key.is_null() {
2290            return GMCRYPTO_ERR;
2291        }
2292        let c = match unsafe { try_slice(der_ct, der_ct_len) } {
2293            Some(s) => s,
2294            None => return GMCRYPTO_ERR,
2295        };
2296        let k = unsafe { &*key };
2297        match sm2_decrypt(&k.inner, c) {
2298            Ok(pt) => unsafe { write_output(&pt, out_pt, out_capacity, out_actual_len) },
2299            Err(_) => GMCRYPTO_ERR,
2300        }
2301    })
2302}
2303
2304// ============================================================
2305// SM2 — raw byte-concat ciphertext (v0.5 W2).
2306//
2307// Wraps `gmcrypto_core::sm2::raw_ciphertext::{encode_c1c3c2,
2308// decode_c1c3c2, decode_c1c2c3_legacy}`. The DER `gmcrypto_sm2_encrypt`
2309// / `gmcrypto_sm2_decrypt` above remain the recommended path; these
2310// raw-byte entry points exist for interop with legacy Chinese-standard
2311// libraries (older gmssl, certain HSM drivers) that expect raw byte
2312// ordering rather than GM/T 0009 DER.
2313//
2314// **No `gmcrypto_sm2_encrypt_c1c2c3_legacy`** — same posture as the
2315// Rust crate (`encode_c1c2c3_legacy` deliberately doesn't exist).
2316// The legacy `C1 || C2 || C3` ordering is **decrypt-only**; emitting
2317// it would propagate the legacy ordering forever (per CLAUDE.md
2318// "Don't" entry).
2319//
2320// Implementation note: encryption goes
2321//   sm2::encrypt -> DER bytes -> asn1::ciphertext::decode ->
2322//     Sm2Ciphertext -> encode_c1c3c2 -> raw bytes
2323// and decryption goes
2324//   raw bytes -> decode_c1c3c2 (or decode_c1c2c3_legacy) ->
2325//     Sm2Ciphertext -> asn1::ciphertext::encode -> DER bytes ->
2326//     sm2::decrypt
2327// The internal DER round-trip is a few hundred extra nanoseconds vs.
2328// adding new `_ciphertext`-shaped Rust API entry points. The SM2
2329// scalar-multiplication / KDF / SM3-MAC work dominates the cost by
2330// 3+ orders of magnitude, so the round-trip is invisible at the
2331// caller. Avoiding the round-trip requires public-API additions on
2332// gmcrypto-core; deferred to v0.6 if a real workload measures the
2333// difference.
2334// ============================================================
2335
2336/// SM2 public-key encrypt; output in the modern raw byte-concat
2337/// `C1 || C3 || C2` format. `C1` is the 65-byte SEC1-uncompressed
2338/// point (`0x04 || X || Y`); `C3` is the 32-byte SM3 MAC; `C2` is
2339/// `msg_len` bytes of XOR-ed ciphertext. Output length is exactly
2340/// `65 + 32 + msg_len`.
2341///
2342/// RNG is sourced from `getrandom::SysRng` internally (same as
2343/// [`gmcrypto_sm2_encrypt`]). The W3 RNG-callback variant lands as a
2344/// separate workstream.
2345///
2346/// Same failure-mode posture as [`gmcrypto_sm2_encrypt`]: single
2347/// [`GMCRYPTO_ERR`] on any failure mode (identity public key, KDF-
2348/// zero retries exhausted).
2349#[unsafe(no_mangle)]
2350pub unsafe extern "C" fn gmcrypto_sm2_encrypt_c1c3c2(
2351    key: *const gmcrypto_sm2_pubkey_t,
2352    pt: *const u8,
2353    pt_len: usize,
2354    out_raw_ct: *mut u8,
2355    out_capacity: usize,
2356    out_actual_len: *mut usize,
2357) -> c_int {
2358    ffi_guard(|| {
2359        if key.is_null() {
2360            return GMCRYPTO_ERR;
2361        }
2362        let p = match unsafe { try_slice(pt, pt_len) } {
2363            Some(s) => s,
2364            None => return GMCRYPTO_ERR,
2365        };
2366        // SAFETY: key non-null per check above.
2367        let k = unsafe { &*key };
2368        let mut rng = getrandom::SysRng;
2369        let der_bytes = match sm2_encrypt(&k.inner, p, &mut rng) {
2370            Ok(b) => b,
2371            Err(_) => return GMCRYPTO_ERR,
2372        };
2373        // DER → Sm2Ciphertext → raw bytes.
2374        let parsed = match ciphertext_der_decode(&der_bytes) {
2375            Some(ct) => ct,
2376            None => return GMCRYPTO_ERR,
2377        };
2378        let raw_bytes = encode_c1c3c2(&parsed);
2379        unsafe { write_output(&raw_bytes, out_raw_ct, out_capacity, out_actual_len) }
2380    })
2381}
2382
2383/// SM2 private-key decrypt of a modern raw byte-concat
2384/// `C1 || C3 || C2` ciphertext. Input length must be at least
2385/// `65 + 32 + 1 = 98` bytes (C1 + C3 + at least one C2 byte).
2386///
2387/// Same failure-mode posture as [`gmcrypto_sm2_decrypt`]: single
2388/// [`GMCRYPTO_ERR`] on any failure mode (malformed input, off-curve
2389/// C1, identity C1, MAC mismatch, or KDF-zero detection). Caller
2390/// cannot distinguish wrong-key from corrupt-ciphertext via timing
2391/// or return code.
2392#[unsafe(no_mangle)]
2393pub unsafe extern "C" fn gmcrypto_sm2_decrypt_c1c3c2(
2394    key: *const gmcrypto_sm2_privkey_t,
2395    raw_ct: *const u8,
2396    raw_ct_len: usize,
2397    out_pt: *mut u8,
2398    out_capacity: usize,
2399    out_actual_len: *mut usize,
2400) -> c_int {
2401    ffi_guard(|| {
2402        if key.is_null() {
2403            return GMCRYPTO_ERR;
2404        }
2405        let c = match unsafe { try_slice(raw_ct, raw_ct_len) } {
2406            Some(s) => s,
2407            None => return GMCRYPTO_ERR,
2408        };
2409        // Raw bytes → Sm2Ciphertext → DER bytes → sm2::decrypt.
2410        let parsed = match decode_c1c3c2(c) {
2411            Some(ct) => ct,
2412            None => return GMCRYPTO_ERR,
2413        };
2414        let der_bytes = ciphertext_der_encode(&parsed);
2415        // SAFETY: key non-null per check above.
2416        let k = unsafe { &*key };
2417        match sm2_decrypt(&k.inner, &der_bytes) {
2418            Ok(pt) => unsafe { write_output(&pt, out_pt, out_capacity, out_actual_len) },
2419            Err(_) => GMCRYPTO_ERR,
2420        }
2421    })
2422}
2423
2424/// SM2 private-key decrypt of a **legacy** raw byte-concat
2425/// `C1 || C2 || C3` ciphertext. Decrypt-only — there is no emit path
2426/// for the legacy ordering, and there will not be one in any v0.5+
2427/// version (per `CLAUDE.md` "Don't" entry).
2428///
2429/// The two raw byte-concat orderings (`C1 || C3 || C2` modern vs
2430/// `C1 || C2 || C3` legacy) are NOT auto-detected. The caller MUST
2431/// know which format their wire-data follows. Mis-feeding modern
2432/// ciphertext to this entry point or vice-versa will fail at the MAC
2433/// check (`GMCRYPTO_ERR`); the failure-mode invariant precludes the
2434/// caller from distinguishing wrong-format from wrong-key.
2435///
2436/// Same failure-mode posture as [`gmcrypto_sm2_decrypt_c1c3c2`]:
2437/// single [`GMCRYPTO_ERR`] on any failure.
2438#[unsafe(no_mangle)]
2439pub unsafe extern "C" fn gmcrypto_sm2_decrypt_c1c2c3_legacy(
2440    key: *const gmcrypto_sm2_privkey_t,
2441    raw_ct: *const u8,
2442    raw_ct_len: usize,
2443    out_pt: *mut u8,
2444    out_capacity: usize,
2445    out_actual_len: *mut usize,
2446) -> c_int {
2447    ffi_guard(|| {
2448        if key.is_null() {
2449            return GMCRYPTO_ERR;
2450        }
2451        let c = match unsafe { try_slice(raw_ct, raw_ct_len) } {
2452            Some(s) => s,
2453            None => return GMCRYPTO_ERR,
2454        };
2455        let parsed = match decode_c1c2c3_legacy(c) {
2456            Some(ct) => ct,
2457            None => return GMCRYPTO_ERR,
2458        };
2459        let der_bytes = ciphertext_der_encode(&parsed);
2460        // SAFETY: key non-null per check above.
2461        let k = unsafe { &*key };
2462        match sm2_decrypt(&k.inner, &der_bytes) {
2463            Ok(pt) => unsafe { write_output(&pt, out_pt, out_capacity, out_actual_len) },
2464            Err(_) => GMCRYPTO_ERR,
2465        }
2466    })
2467}
2468
2469// ============================================================
2470// SM2 — sign / encrypt with caller-supplied RNG (v0.5 W3).
2471//
2472// `_with_rng` variants of [`gmcrypto_sm2_sign`] and
2473// [`gmcrypto_sm2_encrypt`] taking a `gmcrypto_rng_callback` function
2474// pointer + opaque context. The existing `_sign` / `_encrypt` keep
2475// using `getrandom::SysRng` internally — additive surface per Q5.7.
2476//
2477// All bytes drawn from the callback flow through the same constant-
2478// time `sm2::sign_raw_with_id` / `sm2::encrypt` core. The fixed-K
2479// masked-select retry contract on sign is preserved: a callback
2480// returning the same bytes twice still gets masked-select retry on
2481// both candidates, exactly the same as a real RNG.
2482// ============================================================
2483
2484/// `_with_rng` variant of [`gmcrypto_sm2_sign`]. Identical contract
2485/// except RNG bytes come from the caller's `rng_callback` rather
2486/// than `getrandom::SysRng`.
2487///
2488/// Returns [`GMCRYPTO_OK`] on success; [`GMCRYPTO_ERR`] on any
2489/// failure including:
2490/// - null `key` pointer
2491/// - null `rng_callback` pointer
2492/// - callback returned non-zero on any draw
2493/// - signing produced no valid signature within the retry budget
2494///
2495/// Per the failure-mode invariant, the caller cannot distinguish
2496/// callback-error from signing-failure via return code or timing.
2497#[unsafe(no_mangle)]
2498pub unsafe extern "C" fn gmcrypto_sm2_sign_with_rng(
2499    key: *const gmcrypto_sm2_privkey_t,
2500    signer_id: *const u8,
2501    signer_id_len: usize,
2502    msg: *const u8,
2503    msg_len: usize,
2504    rng_callback: gmcrypto_rng_callback,
2505    rng_context: *mut c_void,
2506    out_der_sig: *mut u8,
2507    out_capacity: usize,
2508    out_actual_len: *mut usize,
2509) -> c_int {
2510    ffi_guard(|| {
2511        if key.is_null() {
2512            return GMCRYPTO_ERR;
2513        }
2514        let callback = match rng_callback {
2515            Some(cb) => cb,
2516            None => return GMCRYPTO_ERR,
2517        };
2518        let id: &[u8] = if signer_id_len == 0 {
2519            DEFAULT_SIGNER_ID
2520        } else {
2521            match unsafe { try_slice(signer_id, signer_id_len) } {
2522                Some(s) => s,
2523                None => return GMCRYPTO_ERR,
2524            }
2525        };
2526        let m = match unsafe { try_slice(msg, msg_len) } {
2527            Some(s) => s,
2528            None => return GMCRYPTO_ERR,
2529        };
2530        // SAFETY: key non-null per check above.
2531        let k = unsafe { &*key };
2532        let mut rng = CallbackRng {
2533            callback,
2534            context: rng_context,
2535        };
2536        let sig = match sign_with_id(&k.inner, id, m, &mut rng) {
2537            Ok(s) => s,
2538            Err(_) => return GMCRYPTO_ERR,
2539        };
2540        unsafe { write_output(&sig, out_der_sig, out_capacity, out_actual_len) }
2541    })
2542}
2543
2544/// `_with_rng` variant of [`gmcrypto_sm2_encrypt`]. Identical
2545/// contract except RNG bytes come from the caller's `rng_callback`
2546/// rather than `getrandom::SysRng`.
2547///
2548/// Output is GM/T 0009-2012 DER (same as `gmcrypto_sm2_encrypt`).
2549/// For raw byte-concat output (`C1 || C3 || C2`), use
2550/// `gmcrypto_sm2_encrypt_c1c3c2` — v0.5 doesn't ship a
2551/// `_c1c3c2_with_rng` combined variant; if needed, callers can
2552/// re-encode the DER output via gmcrypto-core's
2553/// `asn1::ciphertext::decode` + `raw_ciphertext::encode_c1c3c2`.
2554///
2555/// Same `GMCRYPTO_ERR`-on-any-failure posture as
2556/// `gmcrypto_sm2_sign_with_rng`.
2557#[unsafe(no_mangle)]
2558pub unsafe extern "C" fn gmcrypto_sm2_encrypt_with_rng(
2559    key: *const gmcrypto_sm2_pubkey_t,
2560    pt: *const u8,
2561    pt_len: usize,
2562    rng_callback: gmcrypto_rng_callback,
2563    rng_context: *mut c_void,
2564    out_der_ct: *mut u8,
2565    out_capacity: usize,
2566    out_actual_len: *mut usize,
2567) -> c_int {
2568    ffi_guard(|| {
2569        if key.is_null() {
2570            return GMCRYPTO_ERR;
2571        }
2572        let callback = match rng_callback {
2573            Some(cb) => cb,
2574            None => return GMCRYPTO_ERR,
2575        };
2576        let p = match unsafe { try_slice(pt, pt_len) } {
2577            Some(s) => s,
2578            None => return GMCRYPTO_ERR,
2579        };
2580        // SAFETY: key non-null per check above.
2581        let k = unsafe { &*key };
2582        let mut rng = CallbackRng {
2583            callback,
2584            context: rng_context,
2585        };
2586        let ct = match sm2_encrypt(&k.inner, p, &mut rng) {
2587            Ok(c) => c,
2588            Err(_) => return GMCRYPTO_ERR,
2589        };
2590        unsafe { write_output(&ct, out_der_ct, out_capacity, out_actual_len) }
2591    })
2592}
2593
2594// ============================================================
2595// SM2 key exchange (GM/T 0003.3) — v1.2.
2596//
2597// C-shaped projection of the v1.1 `sm2::key_exchange` role state-
2598// machines. The Rust consume-on-transition typestate cannot cross the
2599// FFI, so the four Rust types collapse to TWO opaque handles (scope
2600// Q2.2, docs/v1.2-scope.md):
2601//
2602// - the INITIATOR handle is born "waiting": `_new` samples the
2603//   ephemeral internally and writes `R_A` immediately, so the
2604//   pre-ephemeral state is unrepresentable in C;
2605// - `_confirm` / `_finish` CONSUME + FREE their handle (the v0.10
2606//   `_finalize*` precedent); `_free` is for abandonment only;
2607// - a second `_respond` on a waiting responder returns GMCRYPTO_ERR
2608//   with the in-flight state preserved; a FAILED `_respond` spends
2609//   the handle (the Rust responder was consumed by the attempt).
2610//
2611// Misuse ordering, invalid peer points, tag mismatches, RNG failure,
2612// and bad parameters all collapse to the single GMCRYPTO_ERR (the
2613// failure-mode invariant). The agreed key is written to caller
2614// memory — the CALLER owns wiping `key_out` (the
2615// `gmcrypto_sm2_privkey_to_sec1_be` contract); handle-internal
2616// secrets zeroize on drop/consume in the core.
2617//
2618// RNG: `_new` / `_respond` use `getrandom::SysRng`; the `_with_rng`
2619// variants take the v0.5 `gmcrypto_rng_callback` + context (which is
2620// how the c_smoke suite reproduces the GM/T 0003.5 recommended-curve
2621// KAT byte-for-byte through this ABI).
2622// ============================================================
2623
2624/// KX identity-string convention (mirrors the sign FFI): `len == 0`
2625/// selects [`DEFAULT_SIGNER_ID`] (`"1234567812345678"` — also the ID
2626/// the GM/T 0003.5 worked example uses for both parties). Over-long
2627/// ids collapse to the single failure mode in the core constructors.
2628///
2629/// # Safety
2630///
2631/// `ptr` must be valid for `len` reads when `len > 0` (the
2632/// `try_slice` contract).
2633unsafe fn kx_id<'a>(ptr: *const u8, len: usize) -> Option<&'a [u8]> {
2634    if len == 0 {
2635        Some(DEFAULT_SIGNER_ID)
2636    } else {
2637        // SAFETY: forwarded caller contract — `ptr` valid for `len` reads.
2638        unsafe { try_slice(ptr, len) }
2639    }
2640}
2641
2642/// Copy an exactly-`N`-byte slice (just produced by `try_slice(ptr, N)`)
2643/// into an owned array for the core's fixed-size wire types.
2644fn kx_to_array<const N: usize>(s: &[u8]) -> [u8; N] {
2645    let mut a = [0u8; N];
2646    a.copy_from_slice(s);
2647    a
2648}
2649
2650/// Shared tail of the two initiator constructors: run the core
2651/// state-machine (`new` → `produce_ephemeral`) and box the waiting
2652/// state. `out_r_a` is the caller's already-validated 65-byte buffer.
2653fn kx_initiator_build<R: rand_core::TryCryptoRng>(
2654    d: &Sm2PrivateKey,
2655    p_peer: &Sm2PublicKey,
2656    id_a: &[u8],
2657    id_b: &[u8],
2658    klen: usize,
2659    out_r_a: &mut [u8],
2660    rng: &mut R,
2661) -> Option<*mut gmcrypto_sm2_kx_initiator_t> {
2662    let initiator = InnerSm2KxInitiator::new(d, p_peer, id_a, id_b, klen).ok()?;
2663    let (r_a, waiting) = initiator.produce_ephemeral(rng).ok()?;
2664    out_r_a.copy_from_slice(&r_a.to_bytes());
2665    Some(Box::into_raw(Box::new(gmcrypto_sm2_kx_initiator_t {
2666        inner: waiting,
2667        klen,
2668    })))
2669}
2670
2671/// Shared body of the two responder `_respond` entry points: state
2672/// transition + the core `respond`. Returns the `(R_B, S_B)` wire
2673/// bytes to write out, or `None` for the single failure mode.
2674fn kx_respond<R: rand_core::TryCryptoRng>(
2675    handle: &mut gmcrypto_sm2_kx_responder_t,
2676    r_a: &[u8; GMCRYPTO_SM2_SEC1_UNCOMPRESSED_SIZE],
2677    rng: &mut R,
2678) -> Option<(
2679    [u8; GMCRYPTO_SM2_SEC1_UNCOMPRESSED_SIZE],
2680    [u8; GMCRYPTO_SM2_KX_CONFIRM_SIZE],
2681)> {
2682    // A `Waiting` handle must not be disturbed by a misuse call —
2683    // check before take-and-replace so the in-flight handshake
2684    // survives a stray second `_respond`.
2685    if !matches!(handle.state, InnerKxResponderState::Fresh(_)) {
2686        return None;
2687    }
2688    // The Rust `respond` consumes the responder; on failure the handle
2689    // stays `Spent` (nothing remains to retry with — caller frees).
2690    let prev = core::mem::replace(&mut handle.state, InnerKxResponderState::Spent);
2691    let InnerKxResponderState::Fresh(responder) = prev else {
2692        return None;
2693    };
2694    let (r_b, s_b, waiting) = (*responder)
2695        .respond(&Sm2KxEphemeralPoint::from_bytes(r_a), rng)
2696        .ok()?;
2697    handle.state = InnerKxResponderState::Waiting(Box::new(waiting));
2698    Some((r_b.to_bytes(), s_b.to_bytes()))
2699}
2700
2701/// Construct a key-exchange INITIATOR (party A) and write its
2702/// ephemeral point `R_A` (SEC1 uncompressed `04 ‖ X ‖ Y`, exactly
2703/// [`GMCRYPTO_SM2_SEC1_UNCOMPRESSED_SIZE`] = 65 bytes) to `out_r_a`.
2704/// The handle is created already awaiting the responder's reply: send
2705/// `out_r_a` to the responder, then call
2706/// [`gmcrypto_sm2_kx_initiator_confirm`] with its `(R_B, S_B)`.
2707///
2708/// `local_privkey` is A's static key; `peer_pubkey` is B's static
2709/// public key. `id_a` / `id_b` are the parties' identity strings
2710/// (`len == 0` selects the GM/T default ID `"1234567812345678"`).
2711/// `klen` is the agreed-key length in bytes (non-zero, under the KDF
2712/// ceiling); the SAME `klen` sizes `key_out` at confirm time.
2713/// Ephemeral randomness comes from the OS (`getrandom::SysRng`).
2714///
2715/// Returns the handle, or NULL on any failure (null pointer, bad
2716/// `klen`/`id`, RNG failure — indistinguishable by design). Pair with
2717/// exactly one `_confirm` (which frees) **or** one `_free`.
2718///
2719/// # Safety
2720///
2721/// `local_privkey` / `peer_pubkey` must be valid handles from this
2722/// library; `out_r_a` must be valid for 65 writes; `id_a` / `id_b`
2723/// must be valid for their lengths when non-zero.
2724#[unsafe(no_mangle)]
2725pub unsafe extern "C" fn gmcrypto_sm2_kx_initiator_new(
2726    local_privkey: *const gmcrypto_sm2_privkey_t,
2727    peer_pubkey: *const gmcrypto_sm2_pubkey_t,
2728    id_a: *const u8,
2729    id_a_len: usize,
2730    id_b: *const u8,
2731    id_b_len: usize,
2732    klen: usize,
2733    out_r_a: *mut u8,
2734) -> *mut gmcrypto_sm2_kx_initiator_t {
2735    let result = std::panic::catch_unwind(|| {
2736        if local_privkey.is_null() || peer_pubkey.is_null() {
2737            return None;
2738        }
2739        // SAFETY: id pointers valid per the caller contract.
2740        let ida = unsafe { kx_id(id_a, id_a_len) }?;
2741        // SAFETY: as above.
2742        let idb = unsafe { kx_id(id_b, id_b_len) }?;
2743        // SAFETY: caller guarantees `out_r_a` valid for 65 writes.
2744        let out = unsafe { try_slice_mut(out_r_a, GMCRYPTO_SM2_SEC1_UNCOMPRESSED_SIZE) }?;
2745        // SAFETY: non-null per the check above; valid handles per contract.
2746        let d = unsafe { &*local_privkey };
2747        // SAFETY: as above.
2748        let p = unsafe { &*peer_pubkey };
2749        kx_initiator_build(
2750            &d.inner,
2751            &p.inner,
2752            ida,
2753            idb,
2754            klen,
2755            out,
2756            &mut getrandom::SysRng,
2757        )
2758    });
2759    match result {
2760        Ok(Some(p)) => p,
2761        _ => ptr::null_mut(),
2762    }
2763}
2764
2765/// `_with_rng` variant of [`gmcrypto_sm2_kx_initiator_new`]: identical
2766/// contract except the ephemeral randomness comes from the caller's
2767/// `rng_callback` (the v0.5 `gmcrypto_rng_callback` shape) rather than
2768/// `getrandom::SysRng`. A null or failing callback returns NULL —
2769/// indistinguishable from every other failure by design.
2770///
2771/// # Safety
2772///
2773/// As [`gmcrypto_sm2_kx_initiator_new`]; additionally `rng_callback`
2774/// must be NULL or a valid function pointer honouring the callback
2775/// contract (fill `buf_len` bytes, return 0 on success).
2776#[unsafe(no_mangle)]
2777pub unsafe extern "C" fn gmcrypto_sm2_kx_initiator_new_with_rng(
2778    local_privkey: *const gmcrypto_sm2_privkey_t,
2779    peer_pubkey: *const gmcrypto_sm2_pubkey_t,
2780    id_a: *const u8,
2781    id_a_len: usize,
2782    id_b: *const u8,
2783    id_b_len: usize,
2784    klen: usize,
2785    rng_callback: gmcrypto_rng_callback,
2786    rng_context: *mut c_void,
2787    out_r_a: *mut u8,
2788) -> *mut gmcrypto_sm2_kx_initiator_t {
2789    let result = std::panic::catch_unwind(|| {
2790        if local_privkey.is_null() || peer_pubkey.is_null() {
2791            return None;
2792        }
2793        let callback = rng_callback?;
2794        // SAFETY: id pointers valid per the caller contract.
2795        let ida = unsafe { kx_id(id_a, id_a_len) }?;
2796        // SAFETY: as above.
2797        let idb = unsafe { kx_id(id_b, id_b_len) }?;
2798        // SAFETY: caller guarantees `out_r_a` valid for 65 writes.
2799        let out = unsafe { try_slice_mut(out_r_a, GMCRYPTO_SM2_SEC1_UNCOMPRESSED_SIZE) }?;
2800        // SAFETY: non-null per the check above; valid handles per contract.
2801        let d = unsafe { &*local_privkey };
2802        // SAFETY: as above.
2803        let p = unsafe { &*peer_pubkey };
2804        let mut rng = CallbackRng {
2805            callback,
2806            context: rng_context,
2807        };
2808        kx_initiator_build(&d.inner, &p.inner, ida, idb, klen, out, &mut rng)
2809    });
2810    match result {
2811        Ok(Some(p)) => p,
2812        _ => ptr::null_mut(),
2813    }
2814}
2815
2816/// Receive the responder's reply and finish the initiator side:
2817/// verify `S_B` (constant-time), and on success write the agreed key
2818/// (exactly `klen` bytes, the `klen` given at `_new`) to `key_out`
2819/// and the initiator's confirmation tag `S_A`
2820/// ([`GMCRYPTO_SM2_KX_CONFIRM_SIZE`] = 32 bytes) to `out_s_a` — send
2821/// `S_A` to the responder. `r_b` is the responder's ephemeral point
2822/// (65 bytes); `s_b` its confirmation tag (32 bytes).
2823///
2824/// CONSUMES + FREES the handle — even when the arguments are invalid
2825/// or the confirmation fails (the `_finalize*` precedent); do NOT
2826/// call `_free` afterwards. Returns [`GMCRYPTO_OK`] only when `S_B`
2827/// verified and the key was written; every failure (invalid `R_B`,
2828/// tag mismatch, null pointer) is the single [`GMCRYPTO_ERR`].
2829/// **The caller owns wiping `key_out`.**
2830///
2831/// # Safety
2832///
2833/// `initiator` must be a live handle from a `_new` (not yet consumed
2834/// or freed); `r_b` valid for 65 reads, `s_b` for 32 reads, `key_out`
2835/// for `klen` writes, `out_s_a` for 32 writes.
2836#[unsafe(no_mangle)]
2837pub unsafe extern "C" fn gmcrypto_sm2_kx_initiator_confirm(
2838    initiator: *mut gmcrypto_sm2_kx_initiator_t,
2839    r_b: *const u8,
2840    s_b: *const u8,
2841    key_out: *mut u8,
2842    out_s_a: *mut u8,
2843) -> c_int {
2844    ffi_guard(|| {
2845        if initiator.is_null() {
2846            return GMCRYPTO_ERR;
2847        }
2848        // SAFETY: came from Box::into_raw; take ownership + free
2849        // (consumed even on argument errors, per the _finalize
2850        // precedent — the drop wipes the ephemeral state).
2851        let boxed = unsafe { Box::from_raw(initiator) };
2852        let gmcrypto_sm2_kx_initiator_t { inner, klen } = *boxed;
2853        let rb = match unsafe { try_slice(r_b, GMCRYPTO_SM2_SEC1_UNCOMPRESSED_SIZE) } {
2854            Some(s) => s,
2855            None => return GMCRYPTO_ERR,
2856        };
2857        let sb = match unsafe { try_slice(s_b, GMCRYPTO_SM2_KX_CONFIRM_SIZE) } {
2858            Some(s) => s,
2859            None => return GMCRYPTO_ERR,
2860        };
2861        let key_dst = match unsafe { try_slice_mut(key_out, klen) } {
2862            Some(s) => s,
2863            None => return GMCRYPTO_ERR,
2864        };
2865        let sa_dst = match unsafe { try_slice_mut(out_s_a, GMCRYPTO_SM2_KX_CONFIRM_SIZE) } {
2866            Some(s) => s,
2867            None => return GMCRYPTO_ERR,
2868        };
2869        let r_b_point = Sm2KxEphemeralPoint::from_bytes(&kx_to_array(rb));
2870        let s_b_tag = Sm2KxConfirm::from_bytes(&kx_to_array(sb));
2871        match inner.confirm(&r_b_point, &s_b_tag) {
2872            Ok((key, s_a)) => {
2873                key_dst.copy_from_slice(key.as_bytes());
2874                sa_dst.copy_from_slice(&s_a.to_bytes());
2875                // `key` (Sm2SharedKey) drops here → the core zeroizes
2876                // its copy; the caller owns the `key_out` bytes.
2877                GMCRYPTO_OK
2878            }
2879            Err(_) => GMCRYPTO_ERR,
2880        }
2881    })
2882}
2883
2884/// Free an UNCONSUMED initiator handle (abandonment path — e.g. the
2885/// responder never replied). Safe on NULL. Do NOT call after
2886/// `_confirm` (which already consumed + freed the handle).
2887///
2888/// # Safety
2889///
2890/// `initiator` must be NULL or a live handle from a `_new`.
2891#[unsafe(no_mangle)]
2892pub unsafe extern "C" fn gmcrypto_sm2_kx_initiator_free(
2893    initiator: *mut gmcrypto_sm2_kx_initiator_t,
2894) {
2895    if !initiator.is_null() {
2896        // SAFETY: came from Box::into_raw; reclaim + drop (the core's
2897        // drop-wipes run on the inner ephemeral state).
2898        drop(unsafe { Box::from_raw(initiator) });
2899    }
2900}
2901
2902/// Construct a key-exchange RESPONDER (party B). `local_privkey` is
2903/// B's static key; `peer_pubkey` is A's static public key; ids and
2904/// `klen` follow the [`gmcrypto_sm2_kx_initiator_new`] conventions
2905/// (and must match the initiator's, or the confirmation tags will
2906/// not verify). The handle then waits for the initiator's `R_A` —
2907/// call [`gmcrypto_sm2_kx_responder_respond`].
2908///
2909/// Returns the handle, or NULL on any failure. Pair with exactly one
2910/// [`gmcrypto_sm2_kx_responder_finish`] (which frees) **or** one
2911/// [`gmcrypto_sm2_kx_responder_free`].
2912///
2913/// # Safety
2914///
2915/// `local_privkey` / `peer_pubkey` must be valid handles from this
2916/// library; `id_a` / `id_b` must be valid for their lengths when
2917/// non-zero.
2918#[unsafe(no_mangle)]
2919pub unsafe extern "C" fn gmcrypto_sm2_kx_responder_new(
2920    local_privkey: *const gmcrypto_sm2_privkey_t,
2921    peer_pubkey: *const gmcrypto_sm2_pubkey_t,
2922    id_a: *const u8,
2923    id_a_len: usize,
2924    id_b: *const u8,
2925    id_b_len: usize,
2926    klen: usize,
2927) -> *mut gmcrypto_sm2_kx_responder_t {
2928    let result = std::panic::catch_unwind(|| {
2929        if local_privkey.is_null() || peer_pubkey.is_null() {
2930            return None;
2931        }
2932        // SAFETY: id pointers valid per the caller contract.
2933        let ida = unsafe { kx_id(id_a, id_a_len) }?;
2934        // SAFETY: as above.
2935        let idb = unsafe { kx_id(id_b, id_b_len) }?;
2936        // SAFETY: non-null per the check above; valid handles per contract.
2937        let d = unsafe { &*local_privkey };
2938        // SAFETY: as above.
2939        let p = unsafe { &*peer_pubkey };
2940        let responder = InnerSm2KxResponder::new(&d.inner, &p.inner, ida, idb, klen).ok()?;
2941        Some(Box::into_raw(Box::new(gmcrypto_sm2_kx_responder_t {
2942            state: InnerKxResponderState::Fresh(Box::new(responder)),
2943            klen,
2944        })))
2945    });
2946    match result {
2947        Ok(Some(p)) => p,
2948        _ => ptr::null_mut(),
2949    }
2950}
2951
2952/// Receive the initiator's `R_A` (65 bytes) and produce the
2953/// responder's reply: writes `R_B` (65 bytes) to `out_r_b` and the
2954/// responder's confirmation tag `S_B` (32 bytes) to `out_s_b` — send
2955/// both to the initiator, then call
2956/// [`gmcrypto_sm2_kx_responder_finish`] with its `S_A`. Ephemeral
2957/// randomness comes from the OS (`getrandom::SysRng`).
2958///
2959/// The handle stays alive (it now holds the agreed key, wiped on
2960/// drop, pending the initiator's confirmation). Calling `_respond`
2961/// twice returns [`GMCRYPTO_ERR`] without disturbing the in-flight
2962/// state. A FAILED respond (invalid `R_A`, RNG failure) spends the
2963/// handle — every further call fails and the caller frees it.
2964///
2965/// # Safety
2966///
2967/// `responder` must be a live handle from `_new`; `r_a` valid for 65
2968/// reads, `out_r_b` for 65 writes, `out_s_b` for 32 writes.
2969#[unsafe(no_mangle)]
2970pub unsafe extern "C" fn gmcrypto_sm2_kx_responder_respond(
2971    responder: *mut gmcrypto_sm2_kx_responder_t,
2972    r_a: *const u8,
2973    out_r_b: *mut u8,
2974    out_s_b: *mut u8,
2975) -> c_int {
2976    ffi_guard(|| {
2977        if responder.is_null() {
2978            return GMCRYPTO_ERR;
2979        }
2980        let ra = match unsafe { try_slice(r_a, GMCRYPTO_SM2_SEC1_UNCOMPRESSED_SIZE) } {
2981            Some(s) => s,
2982            None => return GMCRYPTO_ERR,
2983        };
2984        let rb_dst = match unsafe { try_slice_mut(out_r_b, GMCRYPTO_SM2_SEC1_UNCOMPRESSED_SIZE) } {
2985            Some(s) => s,
2986            None => return GMCRYPTO_ERR,
2987        };
2988        let sb_dst = match unsafe { try_slice_mut(out_s_b, GMCRYPTO_SM2_KX_CONFIRM_SIZE) } {
2989            Some(s) => s,
2990            None => return GMCRYPTO_ERR,
2991        };
2992        // SAFETY: non-null per the check above; live handle per contract.
2993        let handle = unsafe { &mut *responder };
2994        match kx_respond(handle, &kx_to_array(ra), &mut getrandom::SysRng) {
2995            Some((rb, sb)) => {
2996                rb_dst.copy_from_slice(&rb);
2997                sb_dst.copy_from_slice(&sb);
2998                GMCRYPTO_OK
2999            }
3000            None => GMCRYPTO_ERR,
3001        }
3002    })
3003}
3004
3005/// `_with_rng` variant of [`gmcrypto_sm2_kx_responder_respond`]:
3006/// identical contract except the ephemeral randomness comes from the
3007/// caller's `rng_callback` rather than `getrandom::SysRng`.
3008///
3009/// # Safety
3010///
3011/// As [`gmcrypto_sm2_kx_responder_respond`]; additionally
3012/// `rng_callback` must be NULL or a valid function pointer honouring
3013/// the callback contract.
3014#[unsafe(no_mangle)]
3015pub unsafe extern "C" fn gmcrypto_sm2_kx_responder_respond_with_rng(
3016    responder: *mut gmcrypto_sm2_kx_responder_t,
3017    r_a: *const u8,
3018    rng_callback: gmcrypto_rng_callback,
3019    rng_context: *mut c_void,
3020    out_r_b: *mut u8,
3021    out_s_b: *mut u8,
3022) -> c_int {
3023    ffi_guard(|| {
3024        if responder.is_null() {
3025            return GMCRYPTO_ERR;
3026        }
3027        let callback = match rng_callback {
3028            Some(cb) => cb,
3029            None => return GMCRYPTO_ERR,
3030        };
3031        let ra = match unsafe { try_slice(r_a, GMCRYPTO_SM2_SEC1_UNCOMPRESSED_SIZE) } {
3032            Some(s) => s,
3033            None => return GMCRYPTO_ERR,
3034        };
3035        let rb_dst = match unsafe { try_slice_mut(out_r_b, GMCRYPTO_SM2_SEC1_UNCOMPRESSED_SIZE) } {
3036            Some(s) => s,
3037            None => return GMCRYPTO_ERR,
3038        };
3039        let sb_dst = match unsafe { try_slice_mut(out_s_b, GMCRYPTO_SM2_KX_CONFIRM_SIZE) } {
3040            Some(s) => s,
3041            None => return GMCRYPTO_ERR,
3042        };
3043        // SAFETY: non-null per the check above; live handle per contract.
3044        let handle = unsafe { &mut *responder };
3045        let mut rng = CallbackRng {
3046            callback,
3047            context: rng_context,
3048        };
3049        match kx_respond(handle, &kx_to_array(ra), &mut rng) {
3050            Some((rb, sb)) => {
3051                rb_dst.copy_from_slice(&rb);
3052                sb_dst.copy_from_slice(&sb);
3053                GMCRYPTO_OK
3054            }
3055            None => GMCRYPTO_ERR,
3056        }
3057    })
3058}
3059
3060/// Verify the initiator's confirmation tag `S_A` (32 bytes,
3061/// constant-time) and on success write the agreed key (exactly
3062/// `klen` bytes, the `klen` given at `_new`) to `key_out`.
3063///
3064/// CONSUMES + FREES the handle — even when the arguments are invalid
3065/// or the tag mismatches (the held key is wiped on drop in every
3066/// case); do NOT call `_free` afterwards. Returns [`GMCRYPTO_OK`]
3067/// only when `S_A` verified and the key was written; calling
3068/// `_finish` before a successful `_respond` is the same single
3069/// [`GMCRYPTO_ERR`]. **The caller owns wiping `key_out`.**
3070///
3071/// # Safety
3072///
3073/// `responder` must be a live handle from `_new` (not yet consumed
3074/// or freed); `s_a` valid for 32 reads, `key_out` for `klen` writes.
3075#[unsafe(no_mangle)]
3076pub unsafe extern "C" fn gmcrypto_sm2_kx_responder_finish(
3077    responder: *mut gmcrypto_sm2_kx_responder_t,
3078    s_a: *const u8,
3079    key_out: *mut u8,
3080) -> c_int {
3081    ffi_guard(|| {
3082        if responder.is_null() {
3083            return GMCRYPTO_ERR;
3084        }
3085        // SAFETY: came from Box::into_raw; take ownership + free
3086        // (consumed even on argument errors, per the _finalize
3087        // precedent — the drop wipes the held key).
3088        let boxed = unsafe { Box::from_raw(responder) };
3089        let gmcrypto_sm2_kx_responder_t { state, klen } = *boxed;
3090        let sa = match unsafe { try_slice(s_a, GMCRYPTO_SM2_KX_CONFIRM_SIZE) } {
3091            Some(s) => s,
3092            None => return GMCRYPTO_ERR,
3093        };
3094        let key_dst = match unsafe { try_slice_mut(key_out, klen) } {
3095            Some(s) => s,
3096            None => return GMCRYPTO_ERR,
3097        };
3098        let InnerKxResponderState::Waiting(waiting) = state else {
3099            return GMCRYPTO_ERR;
3100        };
3101        match (*waiting).finish(&Sm2KxConfirm::from_bytes(&kx_to_array(sa))) {
3102            Ok(key) => {
3103                key_dst.copy_from_slice(key.as_bytes());
3104                // `key` drops here → the core zeroizes its copy.
3105                GMCRYPTO_OK
3106            }
3107            Err(_) => GMCRYPTO_ERR,
3108        }
3109    })
3110}
3111
3112/// Free an UNCONSUMED responder handle (abandonment path — e.g. the
3113/// initiator never confirmed, or a `_respond` failure spent the
3114/// handle). Safe on NULL. Do NOT call after `_finish` (which already
3115/// consumed + freed the handle). Any held key material is wiped.
3116///
3117/// # Safety
3118///
3119/// `responder` must be NULL or a live handle from `_new`.
3120#[unsafe(no_mangle)]
3121pub unsafe extern "C" fn gmcrypto_sm2_kx_responder_free(
3122    responder: *mut gmcrypto_sm2_kx_responder_t,
3123) {
3124    if !responder.is_null() {
3125        // SAFETY: came from Box::into_raw; reclaim + drop (the core's
3126        // drop-wipes run on any held key / waiting state).
3127        drop(unsafe { Box::from_raw(responder) });
3128    }
3129}