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
41 changes: 26 additions & 15 deletions alpha_0.1.3_release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ permutation (NIST FIPS 197), re-exported from the umbrella crate.
New crate `bouncycastle-modes` (`bouncycastle::modes`): block cipher modes of operation
(NIST SP 800-38A), currently **CBC** (Sec 6.2). Re-exported from the umbrella crate.

* `Cbc<P, Dir, KEY_LEN, BLOCK_LEN>` over any `BlockPermutation`, so the crate depends on no
* `Cbc<P, Dir, KEY_LEN, BLOCK_LEN>` over any `ElectronicCodeBook`, so the crate depends on no
concrete cipher. The direction is a type parameter: `BlockCipherEncryptor` is implemented only
for `Cbc<_, Encrypting, _, _>` and `BlockCipherDecryptor` only for `Cbc<_, Decrypting, _, _>`,
making a wrong-direction call a compile error rather than a runtime check.
Expand All @@ -40,7 +40,7 @@ New crate `bouncycastle-modes` (`bouncycastle::modes`): block cipher modes of op
supplying your own. Known-answer tests drive `do_encrypt_init_rng` with a fixed-output test RNG.
* **Parallel decryption.** Sec 6.2 notes CBC decryption's inverse cipher calls can run in
parallel, so `do_decrypt_blocks[_out]` walks the ciphertext in pairs through
`BlockPermutation::decrypt_blocks2`, with a one-block remainder for odd `N`. Measured against an
`ElectronicCodeBook::decrypt_blocks2`, with a one-block remainder for odd `N`. Measured against an
otherwise identical permutation that does not override the pair methods, this is **1.83x** the
decryption throughput (67.9 vs 37.1 MiB/s, AES-128, 16 KiB, N=8). CBC encryption is serial by
construction and does not use it.
Expand Down Expand Up @@ -71,8 +71,9 @@ New crate `bouncycastle-modes` (`bouncycastle::modes`): block cipher modes of op
not be secret (SP 800-38A Sec 5.3), so this is sound.
* Input must be a whole number of 16-byte blocks. Unaligned input is rejected with a message
pointing at the missing padding layer rather than being silently padded.
* Reads do not respect block boundaries, so a block split across two reads is carried over;
verified by round-tripping 64 KiB through `dd bs=3`.
* Reads need not respect block boundaries: bytes accumulate in a 1 KiB buffer that goes through the flat
`do_*_out::<1024>` when full, and the whole-block remainder at end of input goes one block at a time; verified by
round-tripping 64 KiB through `dd bs=3`.
* Verified against SP 800-38A F.2: prepending the spec's IV to the spec's ciphertext and running
`decrypt` reproduces the spec's plaintext for all three key lengths. The `encrypt` direction was
cross-checked against an independent CBC implementation under the IV the CLI generated.
Expand All @@ -81,18 +82,17 @@ New crate `bouncycastle-modes` (`bouncycastle::modes`): block cipher modes of op
the F.2 vectors, round trips across the chunk boundary, a fresh IV per invocation, hex/binary
agreement, `--key-file` in both hex and binary, and every error path with its message.

`core`: new `BlockPermutation<KEY_LEN, BLOCK_LEN>` trait (`crypto/core/src/traits.rs`), the raw
`core`: new `ElectronicCodeBook<KEY_LEN, BLOCK_LEN>` trait (`crypto/core/src/traits.rs`), the raw
keyed permutation -- `CIPH_K` / `CIPH^-1_K` of SP 800-38A Sec 5.1 -- that a mode is built on.
`new`, `encrypt_block`, `decrypt_block`, plus provided `encrypt_blocks2` / `decrypt_blocks2` that
default to two single-block calls and which bit-sliced implementations override. The block methods
are infallible; only `new` can fail, and only on the key. `bouncycastle-aes-lowmemory` implements
it for all three key lengths (and `BlockCipher`, which is metadata only and is
`BlockPermutation`'s supertrait; the data-encryption traits are still deliberately not
implemented there).
it for all three key lengths (the data-encryption traits are still deliberately not implemented
there).

Testing:

* `core-test-framework` gains `TestFrameworkBlockPermutation`, which pins the trait contract:
* `core-test-framework` gains `TestFrameworkElectronicCodeBook`, which pins the trait contract:
both directions are inverses either way round, the permutation is injective, and the pair
methods are indistinguishable from two single-block calls **including their order** -- the check
that makes an override safe.
Expand Down Expand Up @@ -130,19 +130,30 @@ Testing:
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.
`KEMEncapsulator` / `KEMDecapsulator`) so the direction is encoded in the implementing type. Both, and
`ElectronicCodeBook`, are bounded on `Algorithm`, whose `MAX_SECURITY_STRENGTH` is the strength the `_init`
constructors enforce (a mode reports its permutation's name and 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.
* One-shot static APIs are provided (default) methods implemented once in the traits -- `encrypt`, `encrypt_rng` on
`BlockCipherEncryptor` and `decrypt` on `BlockCipherDecryptor` -- so every block-aligned mode gets the
house-standard one-shot API at no cost to implementors. They take a flat `&mut [u8; LEN]` and work **in place**
(plaintext in, ciphertext out in the same bytes; `encrypt` returns the generated init data). `LEN` must be a whole
number of blocks, and this is enforced at **compile time** by an inline `const` assertion at the instantiating call
site, so there is no runtime length check and no error variant for it. Data whose length is only known at run
time goes block by block or through the padding layer. (Earlier forms took `[[u8; BLOCK_LEN]; N]`, then separate
input and output arrays; both were replaced before release.)
* The streaming API is flat and in place as well: `do_{en,de}crypt<LEN>(&mut [u8; LEN])`, with the same compile-time
alignment check, are provided methods. The single block-shaped method left is the implementor hook
`do_{en,de}crypt_blocks<N>(&mut [[u8; BLOCK_LEN]; N])`, which is what guarantees an implementation never sees a
partial block; an implementor writes only `do_{en,de}crypt_init[_rng]` and that hook. The data methods keep a
`Result` only for modes with a per-initialization data limit (counter-based modes); CBC never fails them.

Testing:

Expand Down
127 changes: 46 additions & 81 deletions cli/src/aes_cbc_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ use bouncycastle::core::key_material::{
KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations,
};
use bouncycastle::core::traits::{
BlockCipherDecryptor, BlockCipherEncryptor, BlockPermutation, SecurityStrength,
BlockCipherDecryptor, BlockCipherEncryptor, ElectronicCodeBook, SecurityStrength,
};
use bouncycastle::hex;
use bouncycastle::modes::{Cbc, Decrypting, Encrypting};
Expand All @@ -49,12 +49,12 @@ use std::{fs, io};
/// The AES block length in bytes.
const BLOCK_LEN: usize = 16;

/// Blocks processed per call: 64 blocks = 1 KiB, matching the other streaming commands.
/// Bytes processed per call: 1 KiB = 64 blocks, matching the other streaming commands.
///
/// A whole chunk goes through `do_*_blocks[_out]::<CHUNK_BLOCKS>` in one call, which for decryption
/// means 32 pairs down the `decrypt_blocks2` path. The at-most-63-block tail at end of input is
/// flushed one block at a time; it is bounded, so its cost does not scale with the input.
const CHUNK_BLOCKS: usize = 64;
/// A full chunk goes through `do_*::<CHUNK_LEN>` in one call, in place, which for decryption means
/// 32 pairs down the `decrypt_blocks2` path. The at-most-63-block tail at end of input goes one
/// block at a time; it is bounded, so its cost does not scale with the input.
const CHUNK_LEN: usize = 64 * BLOCK_LEN;

#[derive(ValueEnum, Clone, Debug)]
pub(crate) enum AESCBCAction {
Expand Down Expand Up @@ -172,7 +172,7 @@ fn load_key<const KEY_LEN: usize>(
/// Encrypts stdin to stdout, writing the generated IV first.
fn encrypt_stream<P, const KEY_LEN: usize>(key: &KeyMaterial<KEY_LEN>, output_hex: bool)
where
P: BlockPermutation<KEY_LEN, BLOCK_LEN>,
P: ElectronicCodeBook<KEY_LEN, BLOCK_LEN>,
{
let (mut enc, iv) = Cbc::<P, Encrypting, KEY_LEN, BLOCK_LEN>::do_encrypt_init(key)
.unwrap_or_else(|e| {
Expand All @@ -183,21 +183,18 @@ where
// The IV goes out ahead of the ciphertext, so `decrypt` can pick it up.
write_bytes_or_hex(&iv, output_hex);

let mut out = [[0u8; BLOCK_LEN]; CHUNK_BLOCKS];

stream_blocks(|blocks| match <&[[u8; BLOCK_LEN]; CHUNK_BLOCKS]>::try_from(blocks) {
Ok(full_chunk) => {
// Cannot fail: the mode's block methods are infallible for a constructed value.
enc.do_encrypt_blocks_out(full_chunk, &mut out).unwrap();
write_blocks(&out, output_hex);
}
Err(_) => {
// The bounded tail at end of input.
for block in blocks.iter() {
let [c] = enc.do_encrypt_blocks(&[*block]).unwrap();
write_bytes_or_hex(&c, output_hex);
// The cipher works in place: `data` holds plaintext on the way in and ciphertext on the way out.
stream_aligned(|data| {
if let Ok(chunk) = <&mut [u8; CHUNK_LEN]>::try_from(&mut *data) {
// Cannot fail: CBC has no per-IV data limit.
enc.do_encrypt(chunk).unwrap();
} else {
// The bounded tail at end of input: whole blocks, fewer than a chunk.
for block in data.as_chunks_mut::<BLOCK_LEN>().0 {
enc.do_encrypt(block).unwrap();
}
}
write_bytes_or_hex(data, output_hex);
});

finish(output_hex);
Expand All @@ -206,7 +203,7 @@ where
/// Decrypts stdin to stdout, taking the IV from the first block of input.
fn decrypt_stream<P, const KEY_LEN: usize>(key: &KeyMaterial<KEY_LEN>, output_hex: bool)
where
P: BlockPermutation<KEY_LEN, BLOCK_LEN>,
P: ElectronicCodeBook<KEY_LEN, BLOCK_LEN>,
{
// The leading block is the IV, not ciphertext.
let mut iv = [0u8; BLOCK_LEN];
Expand All @@ -224,90 +221,58 @@ where
exit(-1);
});

let mut out = [[0u8; BLOCK_LEN]; CHUNK_BLOCKS];

stream_blocks(|blocks| match <&[[u8; BLOCK_LEN]; CHUNK_BLOCKS]>::try_from(blocks) {
Ok(full_chunk) => {
stream_aligned(|data| {
if let Ok(chunk) = <&mut [u8; CHUNK_LEN]>::try_from(&mut *data) {
// A full chunk is 32 pairs, so this is the `decrypt_blocks2` path.
dec.do_decrypt_blocks_out(full_chunk, &mut out).unwrap();
write_blocks(&out, output_hex);
}
Err(_) => {
for block in blocks.iter() {
let [p] = dec.do_decrypt_blocks(&[*block]).unwrap();
write_bytes_or_hex(&p, output_hex);
dec.do_decrypt(chunk).unwrap();
} else {
for block in data.as_chunks_mut::<BLOCK_LEN>().0 {
dec.do_decrypt(block).unwrap();
}
}
write_bytes_or_hex(data, output_hex);
});

finish(output_hex);
}

/// Reads stdin a block at a time, calling `process` with a full `CHUNK_BLOCKS` slice whenever one
/// is available and once more at end of input with whatever whole blocks remain.
/// Reads stdin and hands it to `process` in block-aligned pieces, mutably so it can be transformed
/// in place: a full `CHUNK_LEN` bytes each time one has accumulated, then once more at end of input
/// with whatever whole blocks remain (fewer than a chunk). Reads need not respect block or chunk boundaries -- bytes simply accumulate in the
/// buffer until it is full -- so a block split across two reads needs no special handling.
///
/// `process` therefore sees a slice of exactly `CHUNK_BLOCKS` for every call but the last, which is
/// how the callers can hand a fixed-size array to `do_*_blocks_out::<CHUNK_BLOCKS>` and fall back
/// to single blocks only for the bounded tail.
///
/// Reads do not respect block boundaries, so a block can arrive split across two reads; the
/// partial block is carried over rather than assumed complete. Input whose total length is not a
/// multiple of `BLOCK_LEN` is an error, because CBC is not defined on a partial block and there is
/// no padding layer to appeal to.
fn stream_blocks(mut process: impl FnMut(&[[u8; BLOCK_LEN]])) {
let mut staged = [[0u8; BLOCK_LEN]; CHUNK_BLOCKS];
let mut read_buf = [0u8; BLOCK_LEN * CHUNK_BLOCKS];
let mut partial = [0u8; BLOCK_LEN];
let mut partial_len = 0usize;
let mut blocks = 0usize;
/// Input whose total length is not a multiple of `BLOCK_LEN` is an error, because CBC is not
/// defined on a partial block and there is no padding layer to appeal to.
fn stream_aligned(mut process: impl FnMut(&mut [u8])) {
let mut buf = [0u8; CHUNK_LEN];
let mut filled = 0usize;

loop {
let n = io::stdin().read(&mut read_buf).unwrap_or_else(|e| {
let n = io::stdin().read(&mut buf[filled..]).unwrap_or_else(|e| {
eprintln!("Error: failed to read from stdin: {e}");
exit(-1);
});
if n == 0 {
break;
}

let mut src = &read_buf[..n];
while !src.is_empty() {
let take = core::cmp::min(BLOCK_LEN - partial_len, src.len());
partial[partial_len..partial_len + take].copy_from_slice(&src[..take]);
partial_len += take;
src = &src[take..];

if partial_len == BLOCK_LEN {
staged[blocks] = partial;
blocks += 1;
partial_len = 0;

if blocks == CHUNK_BLOCKS {
process(&staged);
blocks = 0;
}
}
filled += n;
if filled == CHUNK_LEN {
process(&mut buf);
filled = 0;
}
}

if partial_len != 0 {
if !filled.is_multiple_of(BLOCK_LEN) {
eprintln!(
"Error: input is not a whole number of {BLOCK_LEN}-byte blocks ({partial_len} \
trailing byte(s)). CBC is defined only on whole blocks (SP 800-38A Sec 5.2), and \
this build has no padding layer, so the input must be padded by the caller."
"Error: input is not a whole number of {BLOCK_LEN}-byte blocks ({} trailing byte(s)). \
CBC is defined only on whole blocks (SP 800-38A Sec 5.2), and this build has no \
padding layer, so the input must be padded by the caller.",
filled % BLOCK_LEN
);
exit(-1);
}

if blocks != 0 {
process(&staged[..blocks]);
}
}

/// Writes a run of whole blocks.
fn write_blocks(blocks: &[[u8; BLOCK_LEN]], output_hex: bool) {
for block in blocks.iter() {
write_bytes_or_hex(block, output_hex);
if filled != 0 {
process(&mut buf[..filled]);
}
}

Expand Down
2 changes: 2 additions & 0 deletions crypto/aes-lowmemory/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ edition.workspace = true
[dependencies]
bouncycastle-core.workspace = true
bouncycastle-utils.workspace = true
# Only for the AES-CBC type aliases in `cbc.rs`; the engine itself does not use it.
bouncycastle-modes.workspace = true

[dev-dependencies]
bouncycastle-core-test-framework.workspace = true
Expand Down
32 changes: 5 additions & 27 deletions crypto/aes-lowmemory/src/aes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::sbox::{inv_sbox, sbox};
use crate::schedule::{Aes128Params, Aes192Params, Aes256Params, AesParams, expand, round_key};
use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError};
use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType};
use bouncycastle_core::traits::{Algorithm, BlockCipher, BlockPermutation, SecurityStrength};
use bouncycastle_core::traits::{Algorithm, ElectronicCodeBook, SecurityStrength};
use bouncycastle_utils::secret::Secret;

/// The AES block length in bytes: 16 (FIPS 197 Sec 3.4, `Nb` = 4 words).
Expand Down Expand Up @@ -221,36 +221,14 @@ impl Algorithm for Aes256 {
const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit;
}

// `BlockCipher` here is metadata only -- it declares `MAX_SECURITY_STRENGTH` and nothing else, and
// it is the supertrait `BlockPermutation` requires. It is *not* one of the data-encryption traits
// (`SymmetricCipher`, `BlockCipherEncryptor`, `BlockCipherDecryptor`, `AEADCipher`), which this
// crate still deliberately does not implement: those are mode-of-operation concerns. See the crate
// docs.
//
// Both `Algorithm` and `BlockCipher` declare `MAX_SECURITY_STRENGTH`, so a bare
// `Aes128::MAX_SECURITY_STRENGTH` is ambiguous; qualify it as `<Aes128 as Algorithm>::...` or
// `<Aes128 as BlockCipher>::...` at the use site.

impl BlockCipher for Aes128 {
const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit;
}

impl BlockCipher for Aes192 {
const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_192bit;
}

impl BlockCipher for Aes256 {
const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit;
}

// The three `BlockPermutation` impls are one-line delegations to the inherent methods above. They
// The three `ElectronicCodeBook` impls are one-line delegations to the inherent methods above. They
// are written out longhand rather than generated, for the `cargo mutants` reason given above.
//
// Each overrides `encrypt_blocks2` / `decrypt_blocks2`, because a pair of blocks is exactly what
// the bit-sliced state holds: the pair form costs barely more than one block, where the default
// (two single-block calls) would do four blocks' worth of work.

impl BlockPermutation<16, BLOCK_LEN> for Aes128 {
impl ElectronicCodeBook<16, BLOCK_LEN> for Aes128 {
fn new(key: &KeyMaterial<16>) -> Result<Self, SymmetricCipherError> {
Aes128::new(key)
}
Expand All @@ -268,7 +246,7 @@ impl BlockPermutation<16, BLOCK_LEN> for Aes128 {
}
}

impl BlockPermutation<24, BLOCK_LEN> for Aes192 {
impl ElectronicCodeBook<24, BLOCK_LEN> for Aes192 {
fn new(key: &KeyMaterial<24>) -> Result<Self, SymmetricCipherError> {
Aes192::new(key)
}
Expand All @@ -286,7 +264,7 @@ impl BlockPermutation<24, BLOCK_LEN> for Aes192 {
}
}

impl BlockPermutation<32, BLOCK_LEN> for Aes256 {
impl ElectronicCodeBook<32, BLOCK_LEN> for Aes256 {
fn new(key: &KeyMaterial<32>) -> Result<Self, SymmetricCipherError> {
Aes256::new(key)
}
Expand Down
Loading
Loading