Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,13 @@ mutants.out*/

.idea/
.vscode/

# Claude Code: ignore personal/local state, but share team tooling
# (skills, slash commands, subagents, and project settings.json).
.claude/*
!.claude/settings.json
!.claude/skills/
!.claude/commands/
!.claude/agents/
.claude/settings.local.json
.claude 2/
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ version = "0.1.3"

# *** Internal Dependencies ***
bouncycastle = { path = "./" }
bouncycastle-aes-lowmemory = { path = "./crypto/aes-lowmemory" }
bouncycastle-base64 = { path = "./crypto/base64" }
bouncycastle-core = { path = "crypto/core" }
bouncycastle-core-test-framework = { path = "./crypto/core-test-framework" }
Expand Down Expand Up @@ -41,6 +42,7 @@ version.workspace = true
edition.workspace = true

[dependencies]
bouncycastle-aes-lowmemory.workspace = true
bouncycastle-base64.workspace = true
bouncycastle-core.workspace = true
bouncycastle-factory.workspace = true
Expand Down
15 changes: 14 additions & 1 deletion QUALITY_AND_STYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,20 @@ which parts were done for a very specific reason and should not be changed on a

## Naming Conventions

All normal rust naming convensions from clippy apply. In addition, some library-specific naming conventions:
All normal rust naming conventions from clippy apply, with the following exceptions:

* Many bouncycastle crates use `#[allow(non_snake_case)]`, `#[allow(non_upper_case_globals)]`
`#[allow(non_camel_case_types)]` and so forth, either locally or crate-wide to indicate a preference for keeping the
exact capitalization from a spec (FIPS, RFC, etc) over following rust convention. For example, a struct implementing
the Advanced Encryption Standard (AES) in Galois Counter Mode (GCM) should be named `struct AES_GCM` to match the spec
even though rust convention might be `struct AesGcm`. The same goes for variable and constant names, for example, if a
spec uses a notation where `A` is a matrix and `a` is vector, then it is perfectly acceptable to do
`let A = Matrix;` and
`let a = Vector`, or the global constant `Rcon` should keep that capitalization and not `RCON` or `R_CON`. The
intention is to speed up human code review by a human familiar with the spec is more important than following rust
convention.

In addition, some library-specific naming conventions:

* In constants, "LEN" is the length of a value in bytes (typically used for sizing arrays), whereas "SIZE" is a value in
bits (typically used as a security parameter). For example SHA256 could have constants `HASH_SIZE = 256` and
Expand Down
28 changes: 4 additions & 24 deletions alpha_0.1.3_release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,11 @@

## Major features

* New crate `bouncycastle-aes-lowmemory` (`bouncycastle::aes_lowmemory`): AES-128/192/256 as a raw keyed block
permutation (NIST FIPS 197).

## Minor features / bug fixes

* bug fixes to the way SHA3/SHAKE handled absorbing and squeezing a partial final byte.
* Design discussions about whether core::traits::XOF (in the abstract) should allow interleaving absorb -> squeeze ->
absorb (ie "absorb-after-squeeze). Outcome: absorb-after-squeeze forbidden. Could be changed in the future.

Block cipher traits (PR #96):

* The single `BlockCipher` streaming trait is split into `BlockCipherEncryptor` and `BlockCipherDecryptor` (mirroring
`KEMEncapsulator` / `KEMDecapsulator`) so the direction is encoded in the implementing type. A minimal `BlockCipher`
supertrait carries the shared `MAX_SECURITY_STRENGTH`; the `SymmetricCipher` one-shot API is no longer a supertrait.
* The single-block `do_{en,de}crypt_block[_out]` methods are replaced by multi-block
`do_{en,de}crypt_blocks[_out]<const N>`, taking `&[[u8; BLOCK_LEN]; N]` so the block count is compile-time and
input/output lengths cannot disagree.
* `do_encrypt_init_rng(key, &mut dyn RNG)` is added alongside `do_encrypt_init`, matching the `encaps` / `encaps_rng`
pattern.
* The `do_{en,de}crypt_final[_out]` methods are removed: the traits are now strictly block-aligned, and padding of
arbitrary-length data belongs to a separate `PaddedEncryptor` / `PaddedDecryptor` layer built on top.
* One-shot static APIs are provided (default) methods implemented once in the traits -- `encrypt_blocks`,
`encrypt_blocks_rng`, `encrypt_blocks_out`, `encrypt_blocks_out_rng` on `BlockCipherEncryptor` and `decrypt_blocks`,
`decrypt_blocks_out` on `BlockCipherDecryptor` -- so every block-aligned mode gets the house-standard
take-data-return-result API at no cost to implementors.

Testing:

* The core-test-framework block cipher test now takes separate encryptor/decryptor type parameters, exercises N = 1 and
N = 2 (including mixed single/multi-block encrypt vs decrypt sequences), and checks the one-shots agree with the
streaming API and round-trip.
absorb (ie "absorb-after-squeeze). Outcome: absorb-after-squeeze forbidden. Could be changed in the future.
18 changes: 18 additions & 0 deletions crypto/aes-lowmemory/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "bouncycastle-aes-lowmemory"
version.workspace = true
edition.workspace = true

[dependencies]
bouncycastle-core.workspace = true
bouncycastle-utils.workspace = true

[dev-dependencies]
bouncycastle-hex.workspace = true
bouncycastle-rng.workspace = true
criterion.workspace = true
serde_json = "1.0" # for parsing test vector files

[[bench]]
name = "aes_benches"
harness = false
204 changes: 204 additions & 0 deletions crypto/aes-lowmemory/benches/aes_benches.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
//! Criterion benchmarks for the bit-sliced AES engine.

use bouncycastle_aes_lowmemory::{
AES_128, AES_192, AES_256, AES128Params, AES192Params, AES256Params, AESParams, BLOCK_LEN,
};
use bouncycastle_core::key_material::{KeyMaterial, KeyType};
use bouncycastle_core::traits::RNG;
use bouncycastle_rng as rng;
use criterion::{Criterion, Throughput, criterion_group, criterion_main};
use std::hint::black_box;

/// 16 KiB of data, i.e. 1024 AES blocks.
const NUM_BLOCKS: usize = 1024;
const DATA_LEN: usize = NUM_BLOCKS * BLOCK_LEN;

fn random_blocks() -> Vec<[u8; BLOCK_LEN]> {
let mut blocks = vec![[0u8; BLOCK_LEN]; NUM_BLOCKS];
let mut generator = rng::DefaultRNG::default();
for block in blocks.iter_mut() {
generator.next_bytes_out(block).unwrap();
}
blocks
}

fn key<const N: usize>() -> KeyMaterial<N> {
let mut bytes = [0u8; N];
rng::DefaultRNG::default().next_bytes_out(&mut bytes).unwrap();
KeyMaterial::<N>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap()
}

fn bench_key_expansion(c: &mut Criterion) {
let mut group = c.benchmark_group("aes_lowmemory::key expansion");

let key128 = key::<16>();
group.throughput(Throughput::Bytes(<AES128Params as AESParams>::KEY_LEN as u64));
group.bench_function("Aes128::new()", |b| {
b.iter(|| black_box(AES_128::new(black_box(&key128)).unwrap()))
});

let key192 = key::<24>();
group.throughput(Throughput::Bytes(<AES192Params as AESParams>::KEY_LEN as u64));
group.bench_function("Aes192::new()", |b| {
b.iter(|| black_box(AES_192::new(black_box(&key192)).unwrap()))
});

let key256 = key::<32>();
group.throughput(Throughput::Bytes(<AES256Params as AESParams>::KEY_LEN as u64));
group.bench_function("Aes256::new()", |b| {
b.iter(|| black_box(AES_256::new(black_box(&key256)).unwrap()))
});

group.finish();
}

fn bench_aes128(c: &mut Criterion) {
let aes = AES_128::new(&key::<16>()).unwrap();
let mut blocks = random_blocks();

let mut group = c.benchmark_group("aes_lowmemory::Aes128");
group.throughput(Throughput::Bytes(DATA_LEN as u64));

group.bench_function("16KiB -- .encrypt_block() x1024", |b| {
b.iter(|| {
// So that we're not making copies within the measured loop, we'll just
// encrypt the ciphertext over and over again.
for block in blocks.iter_mut() {
aes.encrypt_block(black_box(block));
}
black_box(&blocks);
})
});

group.bench_function("16KiB -- .encrypt_blocks2() x512", |b| {
b.iter(|| {
let mut buf = blocks.clone();
for pair in buf.chunks_exact_mut(2) {
// `try_into` cannot fail: `chunks_exact_mut(2)` yields slices of length 2.
let pair: &mut [[u8; BLOCK_LEN]; 2] = pair.try_into().unwrap();
aes.encrypt_2blocks(black_box(pair));
}
black_box(&buf);
})
});

group.bench_function("16KiB -- .decrypt_block() x1024", |b| {
b.iter(|| {
let mut buf = blocks.clone();
for block in buf.iter_mut() {
aes.decrypt_block(black_box(block));
}
black_box(&buf);
})
});

group.bench_function("16KiB -- .decrypt_blocks2() x512", |b| {
b.iter(|| {
let mut buf = blocks.clone();
for pair in buf.chunks_exact_mut(2) {
let pair: &mut [[u8; BLOCK_LEN]; 2] = pair.try_into().unwrap();
aes.decrypt_2blocks(black_box(pair));
}
black_box(&buf);
})
});

group.finish();
}

fn bench_aes192(c: &mut Criterion) {
let aes = AES_192::new(&key::<24>()).unwrap();
let mut blocks = random_blocks();

let mut group = c.benchmark_group("aes_lowmemory::Aes192");
group.throughput(Throughput::Bytes(DATA_LEN as u64));

group.bench_function("16KiB -- .encrypt_block() x1024", |b| {
b.iter(|| {
// So that we're not making copies within the measured loop, we'll just
// encrypt the ciphertext over and over again.
for block in blocks.iter_mut() {
aes.encrypt_block(black_box(block));
}
black_box(&blocks);
})
});

group.bench_function("16KiB -- .encrypt_blocks2() x512", |b| {
b.iter(|| {
let mut buf = blocks.clone();
for pair in buf.chunks_exact_mut(2) {
let pair: &mut [[u8; BLOCK_LEN]; 2] = pair.try_into().unwrap();
aes.encrypt_2blocks(black_box(pair));
}
black_box(&buf);
})
});

group.bench_function("16KiB -- .decrypt_block() x1024", |b| {
b.iter(|| {
let mut buf = blocks.clone();
for block in buf.iter_mut() {
aes.decrypt_block(black_box(block));
}
black_box(&buf);
})
});

group.bench_function("16KiB -- .decrypt_blocks2() x512", |b| {
b.iter(|| {
let mut buf = blocks.clone();
for pair in buf.chunks_exact_mut(2) {
let pair: &mut [[u8; BLOCK_LEN]; 2] = pair.try_into().unwrap();
aes.decrypt_2blocks(black_box(pair));
}
black_box(&buf);
})
});

group.finish();
}

fn bench_aes256(c: &mut Criterion) {
let aes = AES_256::new(&key::<32>()).unwrap();
let mut blocks = random_blocks();

let mut group = c.benchmark_group("aes_lowmemory::Aes256");
group.throughput(Throughput::Bytes(DATA_LEN as u64));

group.bench_function("16KiB -- .encrypt_block() x1024", |b| {
b.iter(|| {
for block in blocks.iter_mut() {
aes.encrypt_block(black_box(block));
}
black_box(&blocks);
})
});

group.bench_function("16KiB -- .encrypt_blocks2() x512", |b| {
b.iter(|| {
let mut buf = blocks.clone();
for pair in buf.chunks_exact_mut(2) {
let pair: &mut [[u8; BLOCK_LEN]; 2] = pair.try_into().unwrap();
aes.encrypt_2blocks(black_box(pair));
}
black_box(&buf);
})
});

group.bench_function("16KiB -- .decrypt_blocks2() x512", |b| {
b.iter(|| {
let mut buf = blocks.clone();
for pair in buf.chunks_exact_mut(2) {
let pair: &mut [[u8; BLOCK_LEN]; 2] = pair.try_into().unwrap();
aes.decrypt_2blocks(black_box(pair));
}
black_box(&buf);
})
});

group.finish();
}

criterion_group!(benches, bench_key_expansion, bench_aes128, bench_aes192, bench_aes256);
criterion_main!(benches);
Loading
Loading