Closed AndreKR closed 1 year ago
You've more or less reinvented https://github.com/RustCrypto/nacl-compat/tree/master/crypto_secretstream
There are some pretty big drawbacks to that design though which make STREAM much more flexible: when used with fixed-sized segments, STREAM permits random access with zero framing overhead. This is not possible with a crypto_secretstream
-like protocol, which is more like a sequence of framed packets which can only be processed in-order.
As another point of feedback: the reason aead::stream
API doesn't provide a generate_nonce
function is because the nonce prefixes are so small they risk potential collisions and nonce reuse, which is catastrophic with any online authentication scheme.
I guess we've discussed patterns for initializing STREAM quite a bit on Zulip but don't have a tracking issue for it. Both the Tink paper and https://eprint.iacr.org/2020/1019.pdf discuss methods of deriving a unique key per stream, which would help address this problem and allow for the use of a random nonce.
I opened https://github.com/RustCrypto/traits/issues/1306 to track improving STREAM initialization.
Otherwise, I would suggest using crypto_secretstream
if you want a ready-made packetized protocol, especially since it has multiple implementations in many different languages already.
You've more or less reinvented https://github.com/RustCrypto/nacl-compat/tree/master/crypto_secretstream
Indeed this didn't come up in my initial research. Probably because I didn't look under nacl-compat
because I don't use NaCl and also because the About section really just describes AEAD and nothing about the nature of the streaming it provides.
There are some pretty big drawbacks to that design though which make STREAM much more flexible: when used with fixed-sized segments, STREAM permits random access with zero framing overhead. This is not possible with a crypto_secretstream-like protocol, which is more like a sequence of framed packets which can only be processed in-order.
If the underlying En-/DecryptorBE32
allow random access, my wrappers could easily implement Seek
but I don't think they do - encrypt_next()
increments a position counter. If there were encrypt/decrypt functions that take a position
parameter and carry documentation that tells the user how to calculate the chunks (+ 16 bytes), it could be a suitable API for random access.
TBH, implementing Read
and Write
wasn't even my main intention when writing the wrappers, my main intention was to encapsulate all the knowledge that is required to use aead::stream
:
encrypt_last()
and decrypt_last()
. (This is a particular relief because in practice it is always very difficult to know that your input is about to end.)encrypt()
and decrypt()
even though they exist.As another point of feedback: the reason aead::stream API doesn't provide a generate_nonce function is because the nonce prefixes are so small they risk potential collisions and nonce reuse, which is catastrophic with any online authentication scheme.
It was my understanding that if you use XChaCha20Poly1305
as the cipher you can safely use random nonces because they are long enough? Is that negated by aead::stream
?
If the underlying En-/DecryptorBE32 allow random access, my wrappers could easily implement Seek but I don't think they do - encrypt_next() increments a position counter.
As the rustdoc notes, these types implement the 𝒟 decryptor and ℰ encryptor objects as described in the STREAM paper, which are explicitly designed to manage the counter for you.
Random access is possible via the StreamPrimitive
trait.
That you don't actually need to use encrypt_last() and decrypt_last()
You're adding an empty segment as a simplification. While that works, particularly for the packet-framed case, it's not a zero-cost abstraction: it adds an additional MAC tag, and if you're framing the packets, an additional length prefix.
These may be undesirable in e.g. the file decryption case.
It was my understanding that if you use XChaCha20Poly1305 as the cipher you can safely use random nonces because they are long enough?
It's fine with a cipher with an extended nonce, but not fine for any e.g. IETF AEAD, which is why as #1306 notes it isn't something that should be made into a general-purpose pattern.
That you don't actually need to use encrypt_last() and decrypt_last()
You're adding an empty segment as a simplification. While that works, particularly for the packet-framed case, it's not a zero-cost abstraction: it adds an additional MAC tag, and if you're framing the packets, an additional length prefix.
I (in my wrappers) don't actually do that, I detect the end of the stream by the fact that the encrypted stream has no more chunks. In fact that's my point: If you wanted to use decrypt_last()
(because it's there and the docs kind of imply you should use it), then you would have to somehow frame or pad your segments because otherwise how would you know that you have arrived at the last one?
In the file encryption case, you can either know you're at EOF via the filesystem API, or if you have a footer it can tell you when the encrypted ends
I'm going to close this issue.
Your code example is misusing STREAM. The "last block" flag in the nonce is very much a deliberate design decision directly from the STREAM paper: it's used to prevent truncation attacks, where an attacker can trick you into accepting a STREAM which is shorter than the original.
Yes, that makes the API a bit painful, but it's there for a reason.
No, it controls how the nonce is computed, so it needs to be known in advance prior to decryption.
Instead, you need to use EOF to detect it, or failing that some outer framing.
It might still be possible to build the sort of abstraction you want on top of STREAM (essentially a buffered Encryptor
/Decryptor
which operates over fixed-sized segments) but it MUST properly set the last block flag to prevent truncation attacks.
Here's an updated version that uses encrypt_last()
and decrypt_last()
for the last chunk before EOF:
I also feel the stream API is really awkward. I have some code using the aead
crate, but trying to use streams is confusing. Especially the nonce part, it seems to be circular. You need a nonce for the given stream type, but can't make the stream type without the nonce already. It would be really nice to see an example usage in the docs.
Please see #1306 for alternative stream initialization designs
I used
aead::stream
and it was quite painful.Read
orWrite
orIterator
. You have to break your data up into chunks and encrypt one chunk at a time.encrypt_last()
, although I'm relatively sure that this isn't actually true - encrypting the last chunk withencrypt_next()
works just fine.decrypt_last()
method, but it's unclear how/when you would use it. The only way to know that you're at the last chunk is if you had encrypted an empty chunk at the end. (Granted, encrypting an empty chunk at the end usually happens by accident anyway because that's the way EOF is signaled if you're reading your cleartext from a file.) You could then detect the end by checking if the remaining chunk size is 16, which indicates an empty chunk. This feels awkward but luckily thedecrypt_last()
method doesn't actually have to be called.encrypt()
anddecrypt()
methods and I'm pretty sure those should never be called.generate_nonce()
function to generate the required 19-byte nonce, I had to find the length by trial-and-error.To help with those issues I wrote an encryptor/decryptor pair that wraps
aead::stream::EncryptorBE32/DecryptorBE32
and implementsWrite
(for encryption) andRead
(for decryption).Here they are:
Code
```rust use std::cmp::min; use std::collections::VecDeque; use std::io::{ErrorKind, Read, Write}; use anyhow::Result; use chacha20poly1305::aead::stream::{DecryptorBE32, EncryptorBE32}; use chacha20poly1305::KeyInit; use chacha20poly1305::XChaCha20Poly1305; use hkdf::Hkdf; use rand::rngs::OsRng; use rand::RngCore; use sha2::Sha256; struct XChaCha20Poly1305StreamEncryptorA couple of things of note:
Result<Self>
. This is not something I see often, so maybe this is considered bad style and the I/O should be delayed until the first read/write.Result
s are currentlyanyhow::Result
s, also not something I see often.Is this something that could have a place as part of RustCrypto? Or does it make more sense to publish it as a crate of my own? Or is it just a bad idea in general? (I might be missing something here.)