Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5765b73
sm4: add bouncycastle-sm4, a constant-time SM4 block cipher ported fr…
dghgit Sep 5, 2026
815997c
sm4: override encrypt_4blocks / decrypt_4blocks with the eight-lane c…
dghgit Sep 5, 2026
66feafb
sm4: use a four-lane u16 circuit
dghgit Sep 5, 2026
3e372dc
sm4: every CBC and ECB alias takes <Dir, Pad> over the shared PaddedM…
dghgit Sep 8, 2026
b707d36
sm4: the ElectronicCodeBook trait is the only public route to the per…
dghgit Sep 7, 2026
89cf56d
sm4: consolidate the bc-java attribution
dghgit Sep 19, 2026
e27f8f4
mem_usage_benches: fence bench_sm4_mem_usage's shell recipes as ```text
dghgit Sep 19, 2026
e4c3e0e
sm4: add the SM4_CFB, SM4_CFB8 and SM4_CTR aliases and their sm4-cfb …
dghgit Sep 20, 2026
4d5e55e
sm4: make the new CLI stream-mode test harnesses tolerate a broken pi…
dghgit Sep 20, 2026
c0b19d0
sm4: give sm4_cbc_cli_tests.rs the same broken-pipe tolerance the str…
dghgit Sep 20, 2026
2b846ea
sm4: drop references to ElectronicCodeBook's batch-method defaults, w…
dghgit Sep 25, 2026
ae86de8
sm4: adapt to feature/simple-ciphers -- the API changes this branch a…
dghgit Sep 28, 2026
856dc55
sm4: adapt to feature/simple-ciphers 0fb4e79 -- the stream-mode alias…
dghgit Sep 29, 2026
0372699
sm4: adopt the hazmat layout (#156 step 5)
dghgit Oct 1, 2026
0b154ab
sm4: project SM4_CBC through core's sealed Direction::Select
dghgit Oct 1, 2026
0ec386a
sm4: what the linear replay dropped -- the merge-carried adaptations …
dghgit Oct 25, 2026
88de918
sm4: onto the key objects -- SM4Key<M> hands out the mode workers, th…
dghgit Oct 25, 2026
8df9e35
sm4: the key carries only the roles the aliases use -- the AEAD, ECB …
dghgit Oct 25, 2026
b907b3c
sm4: the engine and the keys compare in constant time
dghgit Oct 25, 2026
e3ccd5a
Merge branch 'feature/key-rework-memory' into feature/sm4
dghgit Oct 26, 2026
e6bed7b
Merge branch 'feature/key-rework-memory' into feature/sm4
dghgit Oct 27, 2026
d4df925
sm4: review fixes -- the key's leftover MAC-key path goes, the stack …
dghgit Oct 28, 2026
a964b86
cli: the sm4 commands take the aes128 contract -- PKCS7, --iv/--iv-fi…
dghgit Oct 28, 2026
2868bba
sm4: the Wycheproof SM4-GCM and SM4-CCM suites, through the mode trai…
dghgit Oct 28, 2026
1ec8b8e
sm4: the stack table sums the frame chain, as the AES table does
dghgit Oct 30, 2026
9928d7e
sm4: a key checks the key type its mode asks for before the permutati…
dghgit Oct 30, 2026
5fc25cb
sm4: SM4_GCM through the key, and an sm4-gcm command
dghgit Oct 31, 2026
71093bf
Merge branch 'feature/key-rework-memory' into feature/sm4
dghgit Oct 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ bouncycastle-rng = { path = "./crypto/rng" }
bouncycastle-sha2 = { path = "./crypto/sha2" }
bouncycastle-sha3 = { path = "./crypto/sha3" }
bouncycastle-sm3 = { path = "./crypto/sm3" }
bouncycastle-sm4 = { path = "./crypto/sm4" }
bouncycastle-utils = { path = "./crypto/utils" }


Expand Down Expand Up @@ -64,3 +65,4 @@ bouncycastle-rng.workspace = true
bouncycastle-sha2.workspace = true
bouncycastle-sha3.workspace = true
bouncycastle-sm3.workspace = true
bouncycastle-sm4.workspace = true
242 changes: 242 additions & 0 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ mod rng_cmd;
mod sha2_cmd;
mod sha3_cmd;
mod sm3_cmd;
mod sm4_cbc_cmd;
mod sm4_cfb8_cmd;
mod sm4_cfb_cmd;
mod sm4_ctr_cmd;
mod sm4_gcm_cmd;

use crate::mac_cmd::HMACVariant;
use crate::mldsa_cmd::MLDSAAction;
Expand Down Expand Up @@ -1696,6 +1701,228 @@ enum Subcommands {
x: bool,
},

/// SM4 in CBC mode (GB/T 32907-2016 block cipher; NIST SP 800-38A Sec 6.2 mode) with PKCS7
/// padding, streaming stdin to stdout.
///
/// On `encrypt`, a fresh unpredictable IV is generated and written as the FIRST 16 BYTES of
/// the output; on `decrypt` it is read back from the first 16 bytes of the input, so the two
/// compose directly in a pipeline. With `--iv` (hex) or `--iv-file` (raw bytes), the IV is
/// supplied instead: `encrypt` uses it and writes ONLY the ciphertext, and `decrypt` reads
/// ONLY ciphertext from stdin.
///
/// Input may be any length: it is padded with PKCS7 to a whole number of 16-byte blocks.
///
/// See `aes128-cbc` for the full IV convention, the padding rule and the warnings; the key
/// and block are both 16 bytes, as for AES-128.
SM4_CBC {
#[arg(short, long)]
direction: CipherDirection,

/// The 16-byte SM4 key in hex.
/// The `key_file` option is preferred to avoid leaving key material in command history.
#[arg(long)]
key: Option<String>,

/// A file containing the 16-byte SM4 key, in binary or hex.
/// If both key and key_file options are provided, the file will be used.
#[arg(short, long)]
key_file: Option<String>,

/// The 16-byte IV in hex. Supplied instead of generated: `encrypt` then writes no IV and
/// `decrypt` reads none. See the IV paragraph above.
#[arg(long)]
iv: Option<String>,

/// A file containing the 16-byte IV, as raw bytes exactly as they are (no hex decoding).
/// If both iv and iv_file options are provided, the file will be used.
#[arg(long)]
iv_file: Option<String>,

#[arg(short)]
/// Output in hex format.
x: bool,
},

/// SM4 in CFB128 mode (GB/T 32907-2016 block cipher; NIST SP 800-38A Sec 6.3 mode), streaming
/// stdin to stdout.
///
/// The segment size is the full block: draft-ribose-cfrg-sm4-10 calls this SM4-CFB-128. Its
/// 8-bit variant is a different, non-interoperable mode; use `sm4-cfb8` for that.
///
/// On `encrypt`, a fresh unpredictable IV is generated and written as the FIRST 16 BYTES of
/// the output; on `decrypt` it is read back from the first 16 bytes of the input. With `--iv`
/// (hex) or `--iv-file` (raw bytes), the IV is supplied instead and only ciphertext is written
/// or read. Input may be ANY length: nothing is padded.
///
/// See `aes128-cfb` for the full IV convention, input-length rule and warnings; the key and
/// block are both 16 bytes, as for AES-128.
SM4_CFB {
#[arg(short, long)]
direction: CipherDirection,

/// The 16-byte SM4 key in hex.
/// The `key_file` option is preferred to avoid leaving key material in command history.
#[arg(long)]
key: Option<String>,

/// A file containing the 16-byte SM4 key, in binary or hex.
/// If both key and key_file options are provided, the file will be used.
#[arg(short, long)]
key_file: Option<String>,

/// The 16-byte IV in hex. Supplied instead of generated: `encrypt` then writes no IV and
/// `decrypt` reads none. See the IV paragraph above.
#[arg(long)]
iv: Option<String>,

/// A file containing the 16-byte IV, as raw bytes exactly as they are (no hex decoding).
/// If both iv and iv_file options are provided, the file will be used.
#[arg(long)]
iv_file: Option<String>,

#[arg(short)]
/// Output in hex format.
x: bool,
},

/// SM4 in CFB8 mode (GB/T 32907-2016 block cipher; NIST SP 800-38A Sec 6.3 mode, s = 8),
/// streaming stdin to stdout.
///
/// The segment size is one byte. This is a DIFFERENT, NON-INTEROPERABLE mode from the CFB128 of
/// `sm4-cfb`: the two ciphertexts agree only on their first byte. It also costs one SM4 call
/// per byte, sixteen times the work of `sm4-cfb`, so prefer that unless a byte-granular
/// self-synchronising stream is required or the format demands CFB8.
///
/// On `encrypt`, a fresh unpredictable IV is generated and written as the FIRST 16 BYTES of
/// the output; on `decrypt` it is read back from the first 16 bytes of the input. With `--iv`
/// (hex) or `--iv-file` (raw bytes), the IV is supplied instead and only ciphertext is written
/// or read. Input may be ANY length: nothing is padded.
///
/// See `aes128-cfb8` for the full IV convention, input-length rule and warnings; the key and
/// block are both 16 bytes, as for AES-128.
SM4_CFB8 {
#[arg(short, long)]
direction: CipherDirection,

/// The 16-byte SM4 key in hex.
/// The `key_file` option is preferred to avoid leaving key material in command history.
#[arg(long)]
key: Option<String>,

/// A file containing the 16-byte SM4 key, in binary or hex.
/// If both key and key_file options are provided, the file will be used.
#[arg(short, long)]
key_file: Option<String>,

/// The 16-byte IV in hex. Supplied instead of generated: `encrypt` then writes no IV and
/// `decrypt` reads none. See the IV paragraph above.
#[arg(long)]
iv: Option<String>,

/// A file containing the 16-byte IV, as raw bytes exactly as they are (no hex decoding).
/// If both iv and iv_file options are provided, the file will be used.
#[arg(long)]
iv_file: Option<String>,

#[arg(short)]
/// Output in hex format.
x: bool,
},

/// SM4 in CTR mode (GB/T 32907-2016 block cipher; NIST SP 800-38A Sec 6.5 mode), streaming
/// stdin to stdout.
///
/// The counter block is a 12-byte nonce followed by a 4-byte counter starting at zero, so one
/// message can be up to 2^32 blocks (64 GiB); past that the command errors rather than
/// repeating keystream. On `encrypt` the nonce is written as the FIRST 12 BYTES of the output
/// and on `decrypt` it is read back from there -- 12, not the 16 the other modes write. With
/// `--iv` (hex) or `--iv-file` (raw bytes), the 12-byte nonce is supplied instead and only
/// ciphertext is written or read; it MUST then be unique per message under one key.
///
/// See `aes128-ctr` for the full nonce convention, input-length rule and warnings, including
/// why a repeated nonce is fatal and why CTR is the most malleable mode here; the key and
/// block are both 16 bytes, as for AES-128.
SM4_CTR {
#[arg(short, long)]
direction: CipherDirection,

/// The 16-byte SM4 key in hex.
/// The `key_file` option is preferred to avoid leaving key material in command history.
#[arg(long)]
key: Option<String>,

/// A file containing the 16-byte SM4 key, in binary or hex.
/// If both key and key_file options are provided, the file will be used.
#[arg(short, long)]
key_file: Option<String>,

/// The 12-byte nonce in hex. Supplied instead of generated: `encrypt` then writes none
/// and `decrypt` reads none. See the nonce paragraph above.
#[arg(long)]
iv: Option<String>,

/// A file containing the 12-byte nonce, as raw bytes exactly as they are (no hex
/// decoding). If both iv and iv_file options are provided, the file will be used.
#[arg(long)]
iv_file: Option<String>,

#[arg(short)]
/// Output in hex format.
x: bool,
},

/// SM4 in GCM (GB/T 32907-2016 block cipher; NIST SP 800-38D mode; RFC 8998 Sec 3.5.1's
/// AEAD_SM4_GCM), streaming stdin to stdout.
///
/// AUTHENTICATED, unlike the other SM4 modes here: tampering with the ciphertext, the AAD or
/// the IV is detected rather than merely producing wrong plaintext. The framing is
/// `aes128-gcm`'s: on `encrypt` a fresh 12-byte IV is written first, the ciphertext follows
/// and the 16-byte tag comes last; `decrypt` reads the same layout back and checks the tag
/// once input is exhausted. With `--iv` / `--iv-file` the IV is supplied and not written or
/// read, and MUST then be unique per message under one key. `--aad` (hex) or `--aad-file`
/// (raw bytes) supply the additional authenticated data; if neither is given, AAD is empty.
///
/// See `aes128-gcm` for the full contract and the warnings, including that on `decrypt` a tag
/// failure may be reported only after plaintext has already been written to stdout, so a
/// script MUST check the exit code; the key and block are both 16 bytes, as for AES-128.
SM4_GCM {
#[arg(short, long)]
direction: CipherDirection,

/// The 16-byte SM4 key in hex.
/// The `key_file` option is preferred to avoid leaving key material in command history.
#[arg(long)]
key: Option<String>,

/// A file containing the 16-byte SM4 key, in binary or hex.
/// If both key and key_file options are provided, the file will be used.
#[arg(short, long)]
key_file: Option<String>,

/// The 12-byte IV in hex. Supplied instead of generated: `encrypt` then writes none and
/// `decrypt` reads none. See the IV paragraph above.
#[arg(long)]
iv: Option<String>,

/// A file containing the 12-byte IV, as raw bytes exactly as they are (no hex decoding).
/// If both iv and iv_file options are provided, the file will be used.
#[arg(long)]
iv_file: Option<String>,

/// The additional authenticated data, in hex. Covered by the tag but not encrypted.
#[arg(long)]
aad: Option<String>,

/// A file containing the additional authenticated data, as raw bytes (never hex-decoded).
/// If both aad and aad_file options are provided, the file will be used.
#[arg(long)]
aad_file: Option<String>,

#[arg(short)]
/// Output in hex format.
x: bool,
},

/// Perform TupleHash128 (NIST SP 800-185 Sec 5) over a tuple of strings. The tuple is given
/// by repeated --element flags, each in hex; with none, stdin is hashed as a single element.
/// The boundaries between elements are part of the hash.
Expand Down Expand Up @@ -2033,6 +2260,21 @@ fn run() {
Some(Subcommands::SM3 { x }) => {
sm3_cmd::sm3_cmd(*x);
}
Some(Subcommands::SM4_CBC { direction, key, key_file, iv, iv_file, x }) => {
sm4_cbc_cmd::sm4_cbc_cmd(direction, key, key_file, iv, iv_file, *x);
}
Some(Subcommands::SM4_CFB { direction, key, key_file, iv, iv_file, x }) => {
sm4_cfb_cmd::sm4_cfb_cmd(direction, key, key_file, iv, iv_file, *x);
}
Some(Subcommands::SM4_CFB8 { direction, key, key_file, iv, iv_file, x }) => {
sm4_cfb8_cmd::sm4_cfb8_cmd(direction, key, key_file, iv, iv_file, *x);
}
Some(Subcommands::SM4_CTR { direction, key, key_file, iv, iv_file, x }) => {
sm4_ctr_cmd::sm4_ctr_cmd(direction, key, key_file, iv, iv_file, *x);
}
Some(Subcommands::SM4_GCM { direction, key, key_file, iv, iv_file, aad, aad_file, x }) => {
sm4_gcm_cmd::sm4_gcm_cmd(direction, key, key_file, iv, iv_file, aad, aad_file, *x);
}
Some(Subcommands::TUPLEHASH128 { length, elements, customization, x }) => {
sha3_cmd::tuplehash_cmd(128, *length, elements, customization, *x);
}
Expand Down
42 changes: 42 additions & 0 deletions cli/src/sm4_cbc_cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//! SM4-CBC encryption and decryption, streaming stdin to stdout.
//!
//! Only the cipher wiring lives here: the IV convention, key loading and stdin framing are all
//! in [`crate::helpers::block_mode_helpers`], shared with the `aes*-cbc` and `aes*-ecb`
//! commands. See that module for the command-line contract. Input is padded with PKCS7, so it
//! may be any length.
//!
//! `sm4-cbc` is the same command as `aes128-cbc` over the SM4 permutation (GB/T 32907-2016;
//! 16-byte key, 16-byte block), so every remark there applies unchanged. CBC (NIST SP 800-38A
//! Sec 6.2) provides confidentiality only: neither the ciphertext nor the IV is authenticated,
//! and a flipped ciphertext bit flips the same bit of the *next* block's plaintext (Appendix D).
//! Do not decrypt data you have not authenticated separately.

use crate::helpers::block_mode_helpers::{
CipherDirection, decrypt_stream, encrypt_stream, load_iv, load_key_object, start_encryptor,
};
use bouncycastle::cipher::modes::CBC;
use bouncycastle::cipher::padding::PKCS7;
use bouncycastle::core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey};
use bouncycastle::sm4::{BLOCK_LEN, KEY_LEN, SM4Key};

/// Dispatches to the shared streaming loops under the key's padded CBC mode.
pub(crate) fn sm4_cbc_cmd(
action: &CipherDirection,
key: &Option<String>,
key_file: &Option<String>,
iv: &Option<String>,
iv_file: &Option<String>,
output_hex: bool,
) {
let key = load_key_object::<SM4Key<CBC<PKCS7>>, KEY_LEN>(key, key_file, "SM4");
let iv = load_iv::<BLOCK_LEN>(iv, iv_file);
match action {
CipherDirection::Encrypt => {
let (enc, generated_iv) = start_encryptor(iv, |rng| key.create_encryptor(rng));
encrypt_stream::<_, BLOCK_LEN>(enc, generated_iv.as_ref(), output_hex, None)
}
CipherDirection::Decrypt => {
decrypt_stream::<_, BLOCK_LEN>(iv, |iv| key.create_decryptor(iv), output_hex, None)
}
}
}
36 changes: 36 additions & 0 deletions cli/src/sm4_cfb8_cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//! SM4-CFB8 encryption and decryption, streaming stdin to stdout.
//!
//! Only the cipher wiring lives here: the IV convention, key loading and stdin framing are in
//! [`crate::helpers::stream_mode_helpers`] (and [`crate::helpers::block_mode_helpers`] for the
//! key and IV loaders), shared with the `aes*-cfb8` commands. See those modules for the
//! command-line contract.
//!
//! `sm4-cfb8` is the same command as `aes128-cfb8` over the SM4 permutation (GB/T 32907-2016;
//! 16-byte key, 16-byte block), so every remark there applies unchanged. The segment size is one
//! byte: this is draft-ribose-cfrg-sm4-10's **SM4-CFB-8** (Sec 8.5.1), a different and
//! non-interoperable mode from the SM4-CFB-128 of `sm4-cfb`, and it costs a full SM4 call per byte,
//! sixteen times the work. Prefer `sm4-cfb` unless a byte-granular self-synchronising stream is
//! required or the format demands CFB8.
//!
//! # Warning
//!
//! CFB8 provides confidentiality only. It does not detect tampering, and neither the ciphertext nor
//! the IV is authenticated. Do not decrypt data you have not authenticated separately.

use crate::helpers::block_mode_helpers::{CipherDirection, load_iv, load_key_object};
use crate::helpers::stream_mode_helpers::run_stream_mode;
use bouncycastle::cipher::modes::CFB8;
use bouncycastle::sm4::{BLOCK_LEN, KEY_LEN, SM4Key};

/// Dispatches to the shared streaming loops under the key's `CFB8` mode.
pub(crate) fn sm4_cfb8_cmd(
action: &CipherDirection,
key: &Option<String>,
key_file: &Option<String>,
iv: &Option<String>,
iv_file: &Option<String>,
output_hex: bool,
) {
let key = load_key_object::<SM4Key<CFB8>, KEY_LEN>(key, key_file, "SM4");
run_stream_mode::<_, KEY_LEN, BLOCK_LEN>(action, &key, load_iv(iv, iv_file), output_hex)
}
40 changes: 40 additions & 0 deletions cli/src/sm4_cfb_cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//! SM4-CFB128 encryption and decryption, streaming stdin to stdout.
//!
//! Only the cipher wiring lives here: the IV convention, key loading and stdin framing are in
//! [`crate::helpers::stream_mode_helpers`] (and [`crate::helpers::block_mode_helpers`] for the
//! key and IV loaders), shared with the `aes*-cfb` commands. See those modules for the
//! command-line contract.
//!
//! `sm4-cfb` is the same command as `aes128-cfb` over the SM4 permutation (GB/T 32907-2016;
//! 16-byte key, 16-byte block), so every remark there applies unchanged. The segment size is the
//! full block, i.e. draft-ribose-cfrg-sm4-10's **SM4-CFB-128** (Sec 8.5.1); the draft's 8-bit
//! variant is a different, non-interoperable mode and has its own command, `sm4-cfb8`.
//!
//! CFB is a stream cipher, so this command accepts input of any length and pads nothing; the
//! ciphertext is exactly as long as the plaintext.
//!
//! # Warning
//!
//! CFB provides confidentiality only. It does not detect tampering, and neither the ciphertext nor
//! the IV is authenticated. Flipping a ciphertext bit flips the *same* bit of the plaintext in the
//! *same* block (NIST SP 800-38A Appendix D, Table D.2), at the cost of randomising the next one,
//! so an attacker edits the block they aimed at. Do not decrypt data you have not authenticated
//! separately.

use crate::helpers::block_mode_helpers::{CipherDirection, load_iv, load_key_object};
use crate::helpers::stream_mode_helpers::run_stream_mode;
use bouncycastle::cipher::modes::CFB;
use bouncycastle::sm4::{BLOCK_LEN, KEY_LEN, SM4Key};

/// Dispatches to the shared streaming loops under the key's `CFB` mode.
pub(crate) fn sm4_cfb_cmd(
action: &CipherDirection,
key: &Option<String>,
key_file: &Option<String>,
iv: &Option<String>,
iv_file: &Option<String>,
output_hex: bool,
) {
let key = load_key_object::<SM4Key<CFB>, KEY_LEN>(key, key_file, "SM4");
run_stream_mode::<_, KEY_LEN, BLOCK_LEN>(action, &key, load_iv(iv, iv_file), output_hex)
}
Loading
Loading