Skip to content
Draft
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
3 changes: 1 addition & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ members = [

[profile.dev]
opt-level = 2

[patch.crates-io.rand_core]
git = "https://github.com/rust-random/rand_core.git"
rev = "45fb1f9609a874e146a118bb9c697e96293d6cad"
5 changes: 4 additions & 1 deletion chacha20/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ pub use legacy::{ChaCha20Legacy, ChaCha20LegacyCore, LegacyNonce};
#[cfg(feature = "rng")]
pub use rand_core;
#[cfg(feature = "rng")]
pub use rng::{ChaCha8Rng, ChaCha12Rng, ChaCha20Rng, Seed, SerializedRngState};
pub use rng::{
ChaCha8Rng, ChaCha12Rng, ChaCha20Rng, FastErasureChaCha8Rng, FastErasureChaCha12Rng,
FastErasureChaCha20Rng, FastErasureCore, Seed, SerializedRngState,
};
#[cfg(feature = "xchacha")]
pub use xchacha::{XChaCha8, XChaCha12, XChaCha20, XNonce, hchacha};

Expand Down
130 changes: 129 additions & 1 deletion chacha20/src/rng.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ impl<R: Rounds, V: Variant> SeedableRng for ChaChaCore<R, V> {
}

impl<R: Rounds, V: Variant> Generator for ChaChaCore<R, V> {
type Word = u32;
type Output = [u32; BUFFER_SIZE];

/// Generates 4 blocks in parallel with avx2 & neon, but merely fills
/// 4 blocks with sse2 & soft
fn generate(&mut self, buffer: &mut [u32; BUFFER_SIZE]) {
fn generate(&mut self, buffer: &mut [u32; BUFFER_SIZE]) -> usize {
cfg_if! {
if #[cfg(chacha20_backend = "soft")] {
backends::soft::Backend(self).gen_ks_blocks(buffer);
Expand Down Expand Up @@ -89,6 +90,8 @@ impl<R: Rounds, V: Variant> Generator for ChaChaCore<R, V> {
backends::soft::Backend(self).gen_ks_blocks(buffer);
}
}

0
}

// `Drop` impl of `BlockRng` calls this method and passes reference to
Expand Down Expand Up @@ -342,3 +345,128 @@ macro_rules! impl_chacha_rng {
impl_chacha_rng!(ChaCha8Rng, R8);
impl_chacha_rng!(ChaCha12Rng, R12);
impl_chacha_rng!(ChaCha20Rng, R20);

/// ChaCha core with fast erasure
#[derive(Debug)]
pub struct FastErasureCore<R: Rounds, V: Variant>(ChaChaCore<R, V>);

impl<R: Rounds, V: Variant> SeedableRng for FastErasureCore<R, V> {
type Seed = Seed;

#[inline]
fn from_seed(seed: Self::Seed) -> Self {
FastErasureCore(ChaChaCore::from_seed(seed))
}
}

impl<R: Rounds, V: Variant> FastErasureCore<R, V> {
/// Get the current block position.
#[inline(always)]
#[must_use]
pub fn get_block_pos(&self) -> V::Counter {
self.0.get_block_pos()
}
}

impl<R: Rounds, V: Variant> Generator for FastErasureCore<R, V> {
type Word = u32;
type Output = [u32; BUFFER_SIZE];

// Generate a block, overwriting the seed
//
// The counter is incremented like usual (i.e. it is not reset).
fn generate(&mut self, buffer: &mut [u32; BUFFER_SIZE]) -> usize {
let _ = self.0.generate(buffer);
self.0.state[4..12].copy_from_slice(&buffer[0..8]);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this overwrites the original value, doesn't it also leave the newly generated key in the output buffer?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aah, is that what this is intended to address? rust-random/rand_core#81

@dhardy dhardy Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this overwrites the original value, doesn't it also leave the newly generated key in the output buffer?

Yes. We could erase it immediately but I didn't see the point: the output buffer should be right next to the key in memory and will be overwritten next time output is generated (the next key update).

Yes, it's related to that PR; see also rust-random/rand#1828 for context.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I would worry about bugs or other issues leaking the key somehow.

The whole point of these schemes is to erase the keys from memory for the purposes of forward secrecy so it seems safer to me to ensure such keys aren't persisted in places like output buffers that are designed specifically to be accessible to the caller.

8
}

fn erase(slice: &mut [Self::Word]) {
// Zero consumed values. We don't need zeroize to observe the result
// since the buffer is not deallocated here.
for word in slice {
*word = 0;
}
}

// drop() method of inner type is called
}

macro_rules! impl_chacha_rng {
($Rng:ident, $rounds:ident) => {
/// A cryptographically secure random number generator with fast key erasure using the ChaCha stream cipher.
///
/// See the [crate docs][crate] for more information about the underlying stream cipher.
///
/// This RNG implementation uses fast key erasure to provide backtracking resistance. Each
/// time a new buffer of results are generated, the first 32 bytes are used to overwrite the
/// key. Each time any value is consumed from the buffer, it is overwritten with zero.
///
/// # Example
///
/// ```rust
#[doc = concat!("use chacha20::", stringify!($Rng), ";")]
/// use rand_core::{SeedableRng, Rng};
///
/// let seed = [42u8; 32];
#[doc = concat!("let mut rng = ", stringify!($Rng), "::from_seed(seed);")]
///
/// let random_u32 = rng.next_u32();
/// let random_u64 = rng.next_u64();
///
/// let mut random_bytes = [0u8; 3];
/// rng.fill_bytes(&mut random_bytes);
/// ```
///
/// See the [`rand`](https://docs.rs/rand/) crate for more advanced RNG functionality.
pub struct $Rng {
core: BlockRng<FastErasureCore<$rounds, Legacy>>,
}

impl SeedableRng for $Rng {
type Seed = Seed;

#[inline]
fn from_seed(seed: Self::Seed) -> Self {
let core = FastErasureCore(ChaChaCore::new_internal(&seed, &[0u8; 8]));
Self {
core: BlockRng::new(core),
}
}
}

impl TryRng for $Rng {
type Error = Infallible;

#[inline]
fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
Ok(self.core.next_word())
}
#[inline]
fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
Ok(self.core.next_u64_from_u32())
}
#[inline]
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> {
self.core.fill_bytes(dest);
Ok(())
}
}

impl TryCryptoRng for $Rng {}

#[cfg(feature = "zeroize")]
impl ZeroizeOnDrop for $Rng {}

// Custom Debug implementation that does not expose the internal state
impl fmt::Debug for $Rng {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, concat!(stringify!($Rng), " {{ ... }}"))
}
}
};
}

impl_chacha_rng!(FastErasureChaCha8Rng, R8);
impl_chacha_rng!(FastErasureChaCha12Rng, R12);
impl_chacha_rng!(FastErasureChaCha20Rng, R20);
Loading