LoupVaillant / Monocypher

An easy to use, easy to deploy crypto library
https://monocypher.org
Other
614 stars 80 forks source link

XChaCha block counter size #279

Closed hakavlad closed 3 weeks ago

hakavlad commented 1 month ago

XSalsa20 has the same shape as Salsa20, except for the much longer nonce: it produces a 512-bit output block given a 256-bit key, a 192-bit nonce, and a 64-bit block counter.

-- http://cr.yp.to/snuffle/xsalsa-20110204.pdf

Use the subkey and remaining 8 byte nonce with ChaCha20 as normal (prefixed by 4 NUL bytes, since [RFC8439] specifies a 12-byte nonce).

-- XChaCha20-IETF uses 32-bit ctr https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha-03#section-2.3

crypto_aead_init_djb() uses a 64-bit nonce and a 64-bit counter. crypto_aead_init_ietf() is fully compatible with the RFC.

-- https://monocypher.org/manual/aead#STANDARDS

The documentation does not specify the size of the XChaCha20 block counter, nor does it explicitly state the maximum message size for XChaCha20. Could you please clarify this in the documentation?

ctr
    The number of 64-byte blocks we skip from the beginning of the stream. This can be used to encrypt (or decrypt) part of a long message or to implement some AEAD constructions such as the one described in RFC 8439. Should be zero by default. When using this, be careful not to accidentally reuse parts of the random stream as that would destroy confidentiality. The return value can help here.

Max ctr value also is not described.

Different libraries use different counter sizes for XChaCha20: Libsodium uses a 64-bit counter, while PyCryptodome uses a 32-bit counter.

LoupVaillant commented 1 month ago

Indeed, the sizes and ranges are not stated in the description, because they're encoded in the ctr argument (sometimes it's an uint32_t, sometimes it's an uint64_t. I'm not sure how to be more explicit, or if I should be to be honest.

As for message sizes, with the 64-bit counter it's "as big as you'll ever want". That is, 2^64 times 512 bytes. With the 32-bit counter (RFC 8439) it is quite a bit smaller, as well as realistically feasible, I should probably have a more explicit warning about that.

@fscoto, may I summon you for your counsel?

fscoto commented 1 month ago

If I may, @LoupVaillant, you may be misunderstanding. @hakavlad is pointing to the crypto_aead_lock page, not the ChaCha20 page.

I suspect that I understand whence the confusion arises. On the crypto_aead_lock page, we have: the main interface to XChaCha20-Poly1305, the incremental interface to XChaCha20-Poly1305 and the ChaCha20-Poly1305 interface.

This highlights something that has been at the core of Monocypher and isn't going to go away. Monocypher is opinionated about what you should be doing and, as far as I recall its history, more or less begrudgingly added compatibility layers to interoperate with whatever mainly the IETF and IRTF specified; one particularly unfortunate situation is Argon2 requiring BLAKE2 but Ed25519 requiring SHA-512. The documentation here appears to be an unfortunate consequence of that design decision.

Having said that, I believe the Standards section in the manual speaks for itself. Only crypto_aead_init_ietf complies with RFC 8439, thereby meaning a counter size of 32 bits and a nonce size of 96 bits. If you've read RFC 8439, this is clear to you. If you haven't, it is far from obvious. Monocypher finds itself squished somewhere between kitchen-sink libcrypto (OpenSSL) and "my way or go away" NaCl.

The main question this raises is: Should downstreams be required to at least skim the RFCs for the relevant cryptographic primitives and constructions? That's a decision I can't make for you. But I can propose my own opinion on it. Personally, I think the answer must be yes. Monocypher helps you make reasonable decisions. But you must be aware of what you are actually implementing. However, I do acknowledge that this runs entirely counter to current trends in library and framework design that I have been observing. Monocypher has a very C-style, "trust the programmer" design as a result of its goals of being small and portable. That implies pushing a lot of domain-specific knowledge up the stack and onto the programmer.

Namespace issues, column length and interface consistency have also restricted the naming, further complicating this: For example, crypto_aead_init_chacha20poly1305_ietf might have been more obvious and, given a one-level indent, would also have consumed over half the length of a 80-character line. This is the route libsodium takes, at the expense of line length.

Ultimately, I think it may be sensible to add a note about the message size, but not specifically about counter size. The counter size is discernible from the size of the nonce parameter and the mechanism by which ChaCha20 works. If you are trying to interoperate with another specification, the crypto_aead_* family is the wrong place to start anyway since you get no control over the counter value to begin with.

hakavlad commented 1 month ago

STANDARDS

These functions implement RFC 8439. crypto_aead_lock() and crypto_aead_init_x(), use XChaCha20

I can't even agree with this formulation: XChaCha20 is not described in this RFC.

LoupVaillant commented 1 month ago

RFCs aren’t the only reference. Granted, XChacha20 is not part of the RFC, contrary to what "These functions implement RFC 8439" suggest. But if we’re splitting hairs, these functions do in fact implement RFC 8439 and then some. It’s not perfect, but I can’t be explicit about everything without bloating the documentation.

Dissecting this STANDARDS section a bit:

crypto_aead_init_ietf() is fully compatible with the RFC.

This should take care of counter size and message length.

crypto_aead_init_djb() uses a 64-bit nonce and a 64-bit counter.

No message length here, I assumed it was clear enough from counter size alone. (Also, mistakenly assuming the message length has a practical limit does not introduce any vulnerability.)

crypto_aead_lock() and crypto_aead_init_x(), use XChaCha20 instead of ChaCha20. […] Note that XChaCha20 derives from ChaCha20 the same way XSalsa20 derives from Salsa20 and benefits from the same security reduction

To know exactly what that means you would need to dig up the definition of XSalsa20, in one of DJB’s papers. Once you do however, the definition of XChaCha20 is obvious and unambiguous. (I personally got it right on the first try, and when I tested Monocypher against libsodium it unsurprisingly gave identical results.)

Or you could just trust the API "just works", which is kinda does: crafting messages long enough to overflow the 64-bit counter is so impractical it could be considered computationally infeasible.


Now I could be a bit more explicit about message length limits. Instead of just saying:

  • __text_size__ Length of both plain_text and cipher_text, in bytes.

I should probably say something like:

  • __text_size__ Length of both plain_text and cipher_text, in bytes. Virtually unlimited, except when crypto_aead_init_ietf() is used, in which case it must never exceed 2^38 - 64.

That way it should be clear that for XChaCha20 the message length is unlimited. As for the counter size (relevant only when you use the lower level API), it is made explicit in the prototype of the function itself (by either being uint32_t or uint64_t, and in the case of XChaCha20 you can see a 64 bit counter).

Would that minor change to the text_size documentation on the AEAD page be enough?