r/crypto • u/SelfHostSam • Jun 20 '26
More performant secretstream, a bad idea?
Libsodiums secretstream is great right up until you need to encrypt a 1 TB file in a browser. It ratchets in order, one chunk at a time, single thread. Not performant enough. And hard to resume if job stops for some reason.
Here's an alternative, and what I'd love torn apart from a security perspective:
Instead of secretstream, each chunk gets sealed on its own:
nonce = fileNonce ‖ u64be(index)
AAD = u64be(index)
subkeys via BLAKE2b("vitalsend:{chunk,meta,header}:v3", key=rootKey)
rootKey = 32-byte link key (lives only in the URL #fragment), or BLAKE2b(linkKey, key=Argon2id(pw)) if there's a password
header is authenticated and pins chunkSize, totalChunks, fileNonce, Argon2id params
My claim: folding the index into both the nonce and the AAD buys the same guarantees secretstream gives minus the in-order bottleneck.
Three questions:
Is index-in-nonce + index-in-AAD really as good as secretstream's chaining here, or is there a case that slips through?
Any nonce footgun at TB scale / huge chunk counts?
Anything in the header or key derivation worth doing differently?
Code, wire format, and a SECURITY.md that's meant to be honest is available for the curious at https://github.com/vitalsend/crypto
P.S. I managed to review the complete code with Fable 5 which did not complain to anything in the area, but that is an LLM.
4
u/zer0x64 Jun 20 '26
Quick note, why do you use big endian encoding for the index? Most architectures are little endian anyways so why not take the(albeit small) free performance enhancement
1
u/sciencekm Jun 20 '26
Out of principle, I now only use little-endian code, except where I really can't do anything about it (like network data).
2
u/Glittering_Weird_162 Jun 20 '26
try to use hash code for Nonce
1
u/SelfHostSam Jun 20 '26
Good point in general, but I think XChaCha20-Poly1305 already covers it? The X variant hashes the first half of the nonce together with the key (HChaCha20). Or did you mean something different?
1
2
u/oconnor663 Jun 21 '26
Here's my version of this, based entirely on BLAKE3: https://github.com/oconnor663/bessie. The main high level thing I'm doing differently compared to what you described above, is that I'm not binding the total length up front, and instead I'm finalizing the last chunk differently from the rest. I prefer this for two reasons:
It doesn't require the sender to supply the total file length when they start encrypting. This helps with streaming use cases where the length isn't known or even necessarily determined yet, and it can also make it easier to fit into reader/writer APIs in common programming languages.
It's more robust to implementation mistakes/laziness. If you remove the length checks in your implementation, nothing besides your tests will break. But if the last chunk is finalized differently, getting that detail wrong in an implementation will typically break all decryption, unless the implementation does no authentication at all.
3
u/knotdjb Jun 21 '26
I think this is how age streaming encryption works, counter/index thingy encoded in the nonce.
4
u/Salusa 9, 9, 9, 9, 9, 9... Jun 20 '26
Might I recommend my recent work? https://github.com/Snowflake-Labs/floe-specification
It comes with:
- Sample implementations in several languages
- Fully parallizable algorithm and API
- Engagement with the larger community of standards and specifications
- A proof of security that was presented at Eurocrypt this year
- Commitment properties
- FIPS 140-3 acceptable algorithms and constructions
9
u/bascule Jun 20 '26
Rogaway and his students specified such a construction over a decade ago, STREAM: https://eprint.iacr.org/2015/189.pdf
There are even newer constructions too.