Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initialize ChaChaRng with arbitrary counter? #1369

Open
xpe opened this issue Dec 31, 2023 · 3 comments
Open

Initialize ChaChaRng with arbitrary counter? #1369

xpe opened this issue Dec 31, 2023 · 3 comments

Comments

@xpe
Copy link

xpe commented Dec 31, 2023

I don't see a way to initialize ChaChaRng with an arbitrary counter value.

I picked ChaCha because it is a great PRNG with a counter / "random access" / "fast forwarding". From Wikipedia:

[The ChaCha cipher is] built on a pseudorandom function based on add-rotate-XOR (ARX) operations — 32-bit addition, bitwise addition (XOR) and rotation operations. The core function maps a 256-bit key, a 64-bit nonce, and a 64-bit counter to a 512-bit block of the key stream [...]. This gives Salsa20 and ChaCha the unusual advantage that the user can efficiently seek to any position in the key stream in constant time.

Not seeing a way to initialize the counter with the current API seems like an oversight. Or perhaps I'm misunderstanding how to appropriately use the counter? If my request makes sense, I would be happy to help with a PR.

Here is one way to construct a ChaCha RNG:

impl ChaCha {
    #[inline(always)]
    pub fn new(key: &[u8; 32], nonce: &[u8]) -> Self {
        init_chacha(key, nonce)
    }
}

Below, you can see ctr_nonce corresponds to 4 32-bit words. In this implementation, the first word is the counter; the rest (3 words = 12 bytes) is the nonce. In the code below, the counter is fixed at 0.

dispatch_light128!(m, Mach, {
    fn init_chacha(key: &[u8; 32], nonce: &[u8]) -> ChaCha {
        let ctr_nonce = [
            0,
            if nonce.len() == 12 {
                read_u32le(&nonce[0..4])
            } else {
                0
            },
            read_u32le(&nonce[nonce.len() - 8..nonce.len() - 4]),
            read_u32le(&nonce[nonce.len() - 4..]),
        ];
        let key0: Mach::u32x4 = m.read_le(&key[..16]);
        let key1: Mach::u32x4 = m.read_le(&key[16..]);
        ChaCha {
            b: key0.into(),
            c: key1.into(),
            d: ctr_nonce.into(),
        }
    }
});

Also, for reference, from https://datatracker.ietf.org/doc/html/rfc8439 :

Note also that the original ChaCha had a 64-bit nonce and 64-bit
block count.  We have modified this here to be more consistent with
recommendations in [Section 3.2 of [RFC5116]](https://datatracker.ietf.org/doc/html/rfc5116#section-3.2).  This limits the use of
a single (key,nonce) combination to 2^32 blocks, or 256 GB, but that
is enough for most uses.  In cases where a single key is used by
multiple senders, it is important to make sure that they don't use
the same nonces.  This can be assured by partitioning the nonce space
so that the first 32 bits are unique per sender, while the other 64
bits come from a counter.

The ChaCha20 state is initialized as follows:

o  The first four words (0-3) are constants: 0x61707865, 0x3320646e,
   0x79622d32, 0x6b206574.

o  The next eight words (4-11) are taken from the 256-bit key by
   reading the bytes in little-endian order, in 4-byte chunks.

o  Word 12 is a block counter.  Since each block is 64-byte, a 32-bit
   word is enough for 256 gigabytes of data.

o  Words 13-15 are a nonce, which MUST not be repeated for the same
   key.  The 13th word is the first 32 bits of the input nonce taken
   as a little-endian integer, while the 15th word is the last 32
   bits.

    cccccccc  cccccccc  cccccccc  cccccccc
    kkkkkkkk  kkkkkkkk  kkkkkkkk  kkkkkkkk
    kkkkkkkk  kkkkkkkk  kkkkkkkk  kkkkkkkk
    bbbbbbbb  nnnnnnnn  nnnnnnnn  nnnnnnnn
@xpe
Copy link
Author

xpe commented Dec 31, 2023

Ah, it looks like 'block position' is a synonym for 'counter'. But this function from guts.rs is private:

#[inline(always)]
pub fn set_block_pos(&mut self, value: u64) {
    set_stream_param(self, STREAM_PARAM_BLOCK, value)
}

It gets used like this:

/// Set the offset from the start of the stream, in 32-bit words.
///
/// As with `get_word_pos`, we use a 68-bit number. Since the generator
/// simply cycles at the end of its period (1 ZiB), we ignore the upper
/// 60 bits.
#[inline]
pub fn set_word_pos(&mut self, word_offset: u128) {
    let block = (word_offset / u128::from(BLOCK_WORDS)) as u64;
    self.rng
        .core
        .state
        .set_block_pos(block);
    self.rng.generate_and_set((word_offset % u128::from(BLOCK_WORDS)) as usize);
}

The above function sets the counter value, indirectly. But it does additional work. Therefore, this isn't the simplest possible API.

Take-away, there is room for a simpler API:

  • set_counter_u32(...)
  • set_counter_u64(...)

This accounts for common variations in how words 12, 13, 14, 15 are allocated between counter and nonce.

@xpe
Copy link
Author

xpe commented Dec 31, 2023

I have a fork where I'm testing the addition of:

  • set_counter_u32(...)
  • get_counter_u32(...)
  • set_counter_u64(...)
  • get_counter_u64(...)

xpe@4bce6c9

@dhardy
Copy link
Member

dhardy commented Dec 31, 2023

Yes, set_word_pos is the nearest equivalent. Possibly it should just be a wrapper over set_counter(u64).

Feel free to make suggestions, though it helps also knowing your use-case.

We're planning on changing the backing implementation anyway: #934.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants