Repository navigation
ChaCha RNG block/word position #438
Description
Activity
The byte arrays are for anyone who wants to use the output of an RNG or a KDF to set these parameters. I don't personally use byte arrays to set anything in my personal code, but I'm sure that someone who is doing that would appreciate these. RNGs can use
next_u32()but KDFs only output byte arrays. But the word arrays are more useful since you can specifically setstate[12],state[13],state[14]andstate[15]I think
[u8; 4]is fine if it matches (legacy) ChaCha20 test vectorsThat's an argument for
set_block_possupporting byte-arrays but notset_word_pos.Good point. While a byte array could be convenient for that, it is still pretty arbitrary the way it works now
- added a commit that references this issue
on Aug 19, 2025 I still see a few issues in the latest
set_word_pos: (https://github.com/RustCrypto/stream-ciphers/blob/master/chacha20/src/rng.rs#L395)- Docs mentions a "36-bit number" (should be 68)
- The index is 6-bit due to
BUFFER_SIZE - The block counter should be incremented (
get_word_posdecrements it, which I'm guessing is correct)
Is this issue still active? If you need a reason for us to use
<Into<X>>, one reason is that users can supply an "intermediate value" or resulting value in the process of converting au64into twou32s. This saves a hair of processing time, but more importantly it allows the expansion of 2 inputs into up to 16 different numerical inputs. One use case for this CSPRNG is usingoutput_type(key/salt) for one input,purpose(MAC/KDF/ECDH/ECDSA/RSA) for another input, and an additional input forepoch(u32oru64). Having this "input size splitting" handled by the RNG library allows other libraries to not need to manually test and confirm input conversions, allowing them to trust the RNG's implementation thatrng.set_block_pos(epoch); rng.set_stream([output_type, purpose]);
does what they think that it's supposed to do without needing to write tests. I know I only used 3 inputs, but it could easily be expanded to have more parameters if desired. 16x
1Binputs is probably waaaay more than enough for most people, but it gives them options: options to split au64into twou32s, or 8u8s. I think it gives tons of control that I haven't seen any other library provide in any language that I've used.I will say though, the
[u32; 2]input also allows for setting the upper 32 bits of the block pos more easily and more readily given the docs mentioning setting the upper 32 bits. It basically offers an explicit control for the upper 32 bits and can act as a way to extend thestream_idby 32 more bits by treating theblock_posas an extension of thestream_id, or even more if the desired output size was small, like for a key or salt.I think the current code is acceptable, so closing.
I'm not sure why
set_streambothers to callself.core.generate_and_set(self.core.index());instead of justself.core.reset().That's a good point. A similar operation is present in the current
rand_chacha:if self.rng.index() != 64 { let wp = self.get_word_pos(); self.set_word_pos(wp); }
The
test_chacha_clone_streamstest will fail if we change it because the test assumes that the currentword_poscarries over, but that raises a new issue.Cloneis not implemented forChaChaXRng. It also raises the question of how this should be implemented. Should it carry over the previousword_posor should it reset? I feel like that's not my decision to make.The most important thing is that this is documented clearly. It would be perfectly reasonable to reset the
word_posto 0 inset_streamsince if someone really wants to they may callset_word_posafterwards to restore a prior value. If we want to make this change fromrand_chachahere I think it's reasonable (but would be less reasonable later since it is a silent change in behaviour affecting results).This would make the
test_chacha_clone_streamsuseless — it could be deleted. Alternatively it could be adapted to show thatset_word_posenables the same behaviour for anyone especially wanting it.If I can get
reset()to work inset_stream()I would be more than happy to replace it. But it's failing some tests. I modified the tests again but they are still failing here:let mut word_pos = rng.get_word_pos(); assert_eq!(word_pos, 1); // passes rng.set_stream(1234567); assert_eq!(rng.get_block_pos(), 0); // passes assert_eq!(rng.get_word_pos(), 64); // passes, should not pass let _ = rng.next_u32(); word_pos = rng.get_word_pos(); assert_eq!(word_pos, 1); // fails, 65 != 1 let test = rng.next_u32(); rng.set_word_pos(word_pos); let expected = 3110319182; assert_eq!(test, expected); // passes assert_eq!(rng.next_u32(), expected); // fails
It looks like a classic
64 + 1possibly resulting fromgenerate()being called, incrementingblock_posby 4. But if we think it will be easier to just document the behavior it originally had, I'm fine with that. Or we can makeset_stream()overwrite theblock_posandindexto ensure that nothing naughty happens under the hood, but some users might perceive that as naughty behavior.@nstilt1 this is likely caused by the block code: internally it never sets the index to 0 but to
N(the buffer size);fn indexjust returns this.This is a leaky API; for correctness
indexshould return something in the range0..N(i.e. < N), but I'm not sure if that would have perf. issues. Maybe I'll addraw_indexto the buffer for internal use and makeindexreturn0for anything >= N.I found the issue with that test I had (
stream_id_endianness).generate_and_setin the originalset_streamwas incrementing theblock_posby 4, so any test that assumes that it continues atword_pos = 1and notword_pos = 1 + 64would fail. This behavior is not expected based on the source code, so it probably wasn't expected looking at the docs.Should
set_stream()resetblock_posto 0, which also resets theindexto 0, or should it just reset the index while keeping whateverblock_posit is already at? Keeping it as it was is probably infeasible, but it might break some existing dependents that don't setblock_posafter settingstream_id. I also want to changeget_block_pos()to attempt to return the actual currentblock_posinstead of theblock_pos + 4caused bygenerate.I suggest that
set_streamsets the word position (block_posandindex) to 0. Just bear in mind that without rand_core#44index()returnsNafter a reset but this can be considered 0.Should I wait for rand_core#44 before changing
chacha20with this update? I've got a working updated branch ofchacha20with the changes from that PR. One thing I don't like is that now these are not the same:// ineffective rng.set_block_pos([1, 2]); rng.set_stream([3, 4]); // erases block_pos // working rng.set_stream([3, 4]); rng.set_block_pos([1, 2]);
It's not a big deal but I'll need to mention it in
set_block_pos()andset_word_pos()if we keep it functioning this way.
Currently this consists of:
First, we need 64-bit counters; thats #334 and #399.
Second, there are some weird Endian-conversions going on here. I would go into them except a more radical reform might be better.
Third, I don't think there's a good reason we need to support byte-arrays. Let users call
to_le_bytesif they need that. (We don't technically needget_word_posbelow either, but it's very little code and serves as an example.)Proposal:
I won't make a PR because I don't want to interfere with #399 or whatever @nstilt1 is up to.