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}