Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fec2a24
sm4: add bouncycastle-sm4, a constant-time SM4 block cipher ported fr…
dghgit Sep 5, 2026
3c32759
sm4: override encrypt_4blocks / decrypt_4blocks with the eight-lane c…
dghgit Sep 5, 2026
92f28b0
sm4: use a four-lane u16 circuit
dghgit Sep 5, 2026
9e59b20
padding: PaddedMode moves in from bouncycastle-aes and takes BLOCK_LE…
dghgit Sep 8, 2026
71c7967
sm4: every CBC and ECB alias takes <Dir, Pad> over the shared PaddedM…
dghgit Sep 8, 2026
b7d1ae1
sm4: the ElectronicCodeBook trait is the only public route to the per…
dghgit Sep 7, 2026
b504527
sm4: consolidate the bc-java attribution
dghgit Sep 19, 2026
3b3f299
mem_usage_benches: fence bench_sm4_mem_usage's shell recipes as ```text
dghgit Sep 19, 2026
b4c2614
sm4: add the SM4_CFB, SM4_CFB8 and SM4_CTR aliases and their sm4-cfb …
dghgit Sep 20, 2026
9f20f8d
sm4: make the new CLI stream-mode test harnesses tolerate a broken pi…
dghgit Sep 20, 2026
61c6461
sm4: give sm4_cbc_cli_tests.rs the same broken-pipe tolerance the str…
dghgit Sep 20, 2026
a62d081
CLAUDE.md: correct the Toolchain and CI sections, both of which descr…
dghgit Sep 20, 2026
0536341
sm4: drop references to ElectronicCodeBook's batch-method defaults, w…
dghgit Sep 25, 2026
a72ff15
sm4: adapt to feature/simple-ciphers -- the API changes this branch a…
dghgit Sep 28, 2026
8333094
Merge branch 'feature/simple-ciphers' into feature/sm4
dghgit Sep 29, 2026
33d8038
sm4: adapt to feature/simple-ciphers 0fb4e79 -- the stream-mode alias…
dghgit Sep 29, 2026
e083044
Merge branch 'feature/simple-ciphers' into feature/sm4
dghgit Sep 30, 2026
84a29b8
Merge branch 'feature/simple-ciphers' into feature/sm4
dghgit Oct 1, 2026
8ce8fc9
sm4: adopt the hazmat layout (#156 step 5)
dghgit Oct 1, 2026
7365806
Merge branch 'feature/simple-ciphers' into feature/sm4
dghgit Oct 1, 2026
6a0b92e
sm4: project SM4_CBC through core's sealed Direction::Select
dghgit Oct 1, 2026
4c336bf
Merge branch 'feature/simple-ciphers' into feature/sm4
dghgit Oct 2, 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
29 changes: 28 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,31 @@ external vector suites — is specified in QUALITY_AND_STYLE.md and CONTRIBUTING

## CI

The only workflow is `.github/workflows/publish_doc_benches_to_ghpages.yaml`: on every PR it builds rustdoc and runs `quality_stats.sh`; on `main` it additionally runs `cargo bench --all` and publishes docs, code stats, and benchmark results to GitHub Pages (`https://bcgit.github.io/bc-rust/`). There is no separate CI test/lint job — local `cargo test --workspace` is the gate, and nothing but a developer running it stands between a broken test and `main`.
Five workflows in `.github/workflows/`, every one of them triggered by `pull_request` — so they run
when the branch you push has a PR open, and a bare branch push with no PR runs nothing.

| Workflow | File | What it runs |
|---|---|---|
| Rust Build | `rust-build.yml` | `cargo build --workspace --all-targets --all-features` |
| Rust Tests | `rust-test.yml` | `cargo test --all` |
| Rust Docs | `rust-docs.yml` | `cargo doc --all` |
| Rust Style | `rust-style.yml` | `cargo fmt --all --check`, under nightly rustfmt |
| Build and Publish Docs | `publish_doc_benches_to_ghpages.yaml` | `cargo doc`, and `quality_stats.sh ./crypto` |

Run those first four locally before pushing — together they are the gate, and `Rust Tests` is the
one that actually fails PRs. (`cargo test --all` is the same thing as `cargo test --workspace`;
`--all` is the old spelling. Either way the point in [Common commands](#common-commands) stands:
without it you run zero tests.)

Things worth knowing about the fifth:

- It only **publishes** to GitHub Pages (`https://bcgit.github.io/bc-rust/`) on a push to `main`;
its `collect_ghpages` and `publish_to_gh_pages` jobs are gated on
`github.ref == 'refs/heads/main'`. On a PR it just builds the doc and code-stats artifacts.
- It does **not** run benchmarks, despite the file name: the `run_benches` job is commented out
("the benches run crazy slow on the github agent"), so no benchmark results reach the site.
- It uses `concurrency: group: "pages"` with `cancel-in-progress: true`, which is global rather than
per branch. Push several branches at once and all but the last report **cancelled** — that is the
concurrency group, not a failure.

`Rust Style` is skipped on forks (`if: github.repository == 'bcgit/bc-rust'`).
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
2 changes: 2 additions & 0 deletions alpha_0.1.3_release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* New algorithms added to crypto/ :
* SM3 -- the SM3 hash (GB/T 32905-2016 / ISO/IEC 10118-3:2018), ported from bc-java.
* AES -- AES-128/192/256, along with its modes AES_ECB, AES_CBC, AES_CCM, AES_CFB, AES_CFB8, AES_CTR, and AES_GCM.
* SM4 -- the SM4 block cipher (GB/T 32907-2016), ported from bc-java, along with its SM4_CBC, SM4_CFB,
SM4_CFB8 and SM4_CTR modes.
* ASCON -- Ascon-AEAD128, Ascon-Hash256, Ascon-XOF128 and Ascon-CXOF128 (NIST SP 800-232).
* Further memory usage improvements on ML-DSA / ML-KEM. New figures for the largest size are:
* ML-DSA-87/Sign 118 kb, ML-DSA-87/Verify 212 kb
Expand Down
122 changes: 122 additions & 0 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ 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;

use crate::mac_cmd::HMACVariant;
use crate::mldsa_cmd::MLDSAAction;
Expand Down Expand Up @@ -1344,6 +1348,112 @@ enum Subcommands {
x: bool,
},

/// SM4 in CBC mode (GB/T 32907-2016 block cipher; NIST SP 800-38A Sec 6.2 mode), streaming
/// stdin to stdout.
///
/// See `aes128-cbc` for the IV convention, block-alignment requirement and warnings; the key
/// and block are both 16 bytes, as for AES-128.
SM4_CBC {
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>,

#[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.
///
/// See `aes128-cfb` for the IV convention, input-length rule and warnings; the key and block
/// are both 16 bytes, as for AES-128.
SM4_CFB {
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>,

#[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.
///
/// See `aes128-cfb8` for the IV convention, input-length rule and warnings; the key and block
/// are both 16 bytes, as for AES-128.
SM4_CFB8 {
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>,

#[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.
///
/// See `aes128-ctr` for the 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 {
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>,

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

/// The ML-KEM-512 key encapsulation algorithm.
MLKEM512 {
action: mlkem_cmd::MLKEMAction,
Expand Down Expand Up @@ -1711,6 +1821,18 @@ fn run() {
Some(Subcommands::AES256_CBC { direction, key, key_file, x }) => {
aes_cbc_cmd::aes256_cbc_cmd(direction, key, key_file, *x);
}
Some(Subcommands::SM4_CBC { direction, key, key_file, x }) => {
sm4_cbc_cmd::sm4_cbc_cmd(direction, key, key_file, *x);
}
Some(Subcommands::SM4_CFB { direction, key, key_file, x }) => {
sm4_cfb_cmd::sm4_cfb_cmd(direction, key, key_file, *x);
}
Some(Subcommands::SM4_CFB8 { direction, key, key_file, x }) => {
sm4_cfb8_cmd::sm4_cfb8_cmd(direction, key, key_file, *x);
}
Some(Subcommands::SM4_CTR { direction, key, key_file, x }) => {
sm4_ctr_cmd::sm4_ctr_cmd(direction, key, key_file, *x);
}
Some(Subcommands::AES128_CFB { direction, key, key_file, x }) => {
aes_cfb_cmd::aes128_cfb_cmd(direction, key, key_file, *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, stdin framing and
//! block-alignment enforcement are all in [`crate::helpers::block_mode_helpers`], shared with the `aes*-cbc`,
//! `aes*-cfb` and `aes*-ecb` commands. See that module for the command-line contract.
//!
//! `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::{
BLOCK_LEN, CipherDirection, decrypt_stream, encrypt_stream, load_key,
};
use bouncycastle::cipher::modes::Cbc;
use bouncycastle::cipher::{Decrypting, Encrypting};
use bouncycastle::sm4::hazmat::SM4;

/// Names the mode in error messages.
const MODE: &str = "CBC";

pub(crate) fn sm4_cbc_cmd(
action: &CipherDirection,
key: &Option<String>,
key_file: &Option<String>,
output_hex: bool,
) {
let key = load_key::<16>(key, key_file, "SM4");
match action {
CipherDirection::Encrypt => {
encrypt_stream::<Cbc<SM4, Encrypting, 16, BLOCK_LEN>, 16, BLOCK_LEN>(
&key, output_hex, MODE,
)
}
CipherDirection::Decrypt => {
decrypt_stream::<Cbc<SM4, Decrypting, 16, BLOCK_LEN>, 16, BLOCK_LEN>(
&key, output_hex, MODE,
)
}
}
}
38 changes: 38 additions & 0 deletions cli/src/sm4_cfb8_cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//! 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 loader), 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::{BLOCK_LEN, CipherDirection, load_key};
use crate::helpers::stream_mode_helpers::run_stream_mode;
use bouncycastle::cipher::modes::Cfb8;
use bouncycastle::cipher::{Decrypting, Encrypting};
use bouncycastle::sm4::hazmat::SM4;

pub(crate) fn sm4_cfb8_cmd(
action: &CipherDirection,
key: &Option<String>,
key_file: &Option<String>,
output_hex: bool,
) {
let key = load_key::<16>(key, key_file, "SM4");
run_stream_mode::<
Cfb8<SM4, Encrypting, 16, BLOCK_LEN>,
Cfb8<SM4, Decrypting, 16, BLOCK_LEN>,
16,
BLOCK_LEN,
>(action, &key, output_hex)
}
42 changes: 42 additions & 0 deletions cli/src/sm4_cfb_cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//! 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 loader), 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 unlike `sm4-cbc` 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::{BLOCK_LEN, CipherDirection, load_key};
use crate::helpers::stream_mode_helpers::run_stream_mode;
use bouncycastle::cipher::modes::Cfb;
use bouncycastle::cipher::{Decrypting, Encrypting};
use bouncycastle::sm4::hazmat::SM4;

pub(crate) fn sm4_cfb_cmd(
action: &CipherDirection,
key: &Option<String>,
key_file: &Option<String>,
output_hex: bool,
) {
let key = load_key::<16>(key, key_file, "SM4");
run_stream_mode::<
Cfb<SM4, Encrypting, 16, BLOCK_LEN>,
Cfb<SM4, Decrypting, 16, BLOCK_LEN>,
16,
BLOCK_LEN,
>(action, &key, output_hex)
}
47 changes: 47 additions & 0 deletions cli/src/sm4_ctr_cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//! SM4-CTR encryption and decryption, streaming stdin to stdout.
//!
//! Only the cipher wiring lives here: the nonce convention, key loading and stdin framing are in
//! [`crate::helpers::stream_mode_helpers`] (and [`crate::helpers::block_mode_helpers`] for the key loader), shared with the
//! `aes*-ctr` commands. See those modules for the command-line contract.
//!
//! `sm4-ctr` is the same command as `aes128-ctr` over the SM4 permutation (GB/T 32907-2016; 16-byte
//! key, 16-byte block), so every remark there applies unchanged, including the split of the counter
//! block: the nonce is **12 bytes** and the counter the remaining 4, giving 2^32 blocks -- 64 GiB
//! -- in a single message. draft-ribose-cfrg-sm4-10 Sec 8.7 takes the counter sequence as an input
//! and requires only that it "does not repeat within the block size", so this is one admissible
//! choice; it is NIST SP 800-38A Appendix B.2's.
//!
//! `encrypt` writes that 12-byte nonce as the first bytes of its output and `decrypt` reads it back,
//! exactly as the other modes do with their IVs; note that it is 12 bytes here, not 16.
//!
//! # Warning
//!
//! CTR provides confidentiality only and is the most malleable mode here: flipping any ciphertext
//! bit flips exactly the corresponding plaintext bit and affects nothing else (SP 800-38A
//! Appendix D, Table D.2), so an attacker can edit the plaintext at will with no garbling to give
//! it away. A repeated nonce under one key is fatal rather than merely unwise -- the same keystream
//! twice leaks the XOR of the two messages -- which is why the nonce is drawn from the OS-backed
//! DRBG and there is no way to supply one. Do not decrypt data you have not authenticated
//! separately.

use crate::helpers::block_mode_helpers::{BLOCK_LEN, CipherDirection, load_key};
use crate::helpers::stream_mode_helpers::run_stream_mode;
use bouncycastle::cipher::modes::Ctr;
use bouncycastle::cipher::{Decrypting, Encrypting};
use bouncycastle::sm4::CTR_NONCE_LEN;
use bouncycastle::sm4::hazmat::SM4;

pub(crate) fn sm4_ctr_cmd(
action: &CipherDirection,
key: &Option<String>,
key_file: &Option<String>,
output_hex: bool,
) {
let key = load_key::<16>(key, key_file, "SM4");
run_stream_mode::<
Ctr<SM4, Encrypting, 16, BLOCK_LEN, CTR_NONCE_LEN>,
Ctr<SM4, Decrypting, 16, BLOCK_LEN, CTR_NONCE_LEN>,
16,
CTR_NONCE_LEN,
>(action, &key, output_hex)
}
Loading
Loading