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

Misuse issue in (X)ChaCha20 and (X)ChaCha20-Poly1305 APIs #308

Closed
brycx opened this issue Nov 29, 2022 · 2 comments · Fixed by #309
Closed

Misuse issue in (X)ChaCha20 and (X)ChaCha20-Poly1305 APIs #308

brycx opened this issue Nov 29, 2022 · 2 comments · Fixed by #309
Labels
bug Something isn't working
Milestone

Comments

@brycx
Copy link
Member

brycx commented Nov 29, 2022

During a review of parts of the codebase, I've come across a rather unfortunate misuse possibility in ChaCha20, which also affects ChaCha20-Poly1305, but much more limited therein.

(X)ChaCha20

In orion::hazardous::stream::chacha20::encrypt() we allow the user to specify the initial_counter, that the ChaCha20 state should be initialized with (also known as the block counter). If this initial_counter is too large, meaning would overflow when adding 1, then we return an error.

match initial_counter.checked_add(ctr as u32) {

The problem is, that before this error is returned, we have already copied the plaintext to the output destination dst_out (in order to later XOR it with the keystream).

dst_out[..plaintext.len()].copy_from_slice(plaintext);

If the initial_counter.checked_add(1) returns Err() the plaintext bytes that were supposed to be XORed with the next keystream bytes, remain in plaintext in the output buffer.

    let k = SecretKey::generate();
    let n = Nonce::from_slice(&[0u8; 12]).unwrap();
    let ic: u32 = u32::MAX - 1;

    let text = [b'x'; 128 + 4];
    let mut dst_out = [0u8; 128 + 4];

    let err = encrypt(&k, &n, ic, &text, &mut dst_out).unwrap_err();

    assert_eq!(&[b'x'; 4], &dst_out[dst_out.len() - 4..]);

While we do return an error, and a user should not use the output of encrypt() if it returns an error, they might reasonably expect that dst_out not contain a copy of some plaintext bytes.

This issue also applies to XChaCha20, as it uses chacha20::encrypt().

(X)ChaCha20-Poly1305

The impact on (X)ChaCha20-Poly1305 is more limited, as the user cannot specify the initial_counter. However, the plaintext is encrypted with an initial block counter of 1, because the first is used for the Poly1305 key generation. Thus, plaintext bytes may be left in the output buffer if the length of the plaintext is one block less than the maximum (max is 2^32-1 * 64 bytes), which is very unlikely to happen in real-world use.

For the streaming implementation, one block less than the required amount for (X)ChaCha20-Poly1305, is required to hit this, as the initial counter there used internally starts with 2, for plaintext bytes.

Solution

The obvious solution here seems to be, to only copy anything to the output buffer that is ciphertext. Thus, we change the underlying XOR macro usage in ChaCha20 and can take the same approach when processing plaintext bytes in AEAD. In AEAD, we could maybe even get a bit of speedup as we could then encrypt and authenticate the ciphertext block individually, instead of making two passes over the plaintext and ciphertext bytes (first encrypt, then authenticate).

We could also discuss removing the option to set initial_counter for (X)ChaCha20, in a future major release, as there seems to be no real use for it.

cc @vlmutolo

@brycx brycx added the bug Something isn't working label Nov 29, 2022
@brycx brycx added this to the 0.17.3 milestone Nov 29, 2022
@vlmutolo
Copy link
Contributor

A third option would be to check all the error conditions before any output is written. But I think you're right that we shouldn't ever write plaintext to the output buffer, even if we immediately overwrite it.

One particularly bad situation is if the user sets up the output buffer to be memory-mapped to a file. In that case, there's a chance the plaintext is written to disk and is then far more observable.

@brycx
Copy link
Member Author

brycx commented Nov 29, 2022

Just a note that the removal of initial_counter in the public API, wouldn't fix the issue AEADs.

One particularly bad situation is if the user sets up the output buffer to be memory-mapped to a file. In that case, there's a chance the plaintext is written to disk and is then far more observable.

Very good point! We'll use the proposed approach instead then.

brycx added a commit that referenced this issue Dec 2, 2022
brycx added a commit that referenced this issue Dec 3, 2022
brycx added a commit that referenced this issue Dec 3, 2022
brycx added a commit that referenced this issue Dec 7, 2022
…before all needed keystream blocks are guaranteed to be produced (#309)

* aead: Do not copy plaintext into dst_out before we know next keystream can be reliably produced (see #308)

* nit

* chacha20: Do not copy plaintext into dst_out before we know next keystream can be reliably produced (see #308)

* chacha20: Add test for #308

* nit

* nit

* chacha20poly1305: Add P/A/C_MAX length checks for maximum values specified in RFC 8439

* xchacha20poly1305: Add length errors to docs refencing the new checks in chacha20poly1305::

* chacha20poly1305: Check A_MAX on open()

* doc: Clarification on (X)ChaCha20-based constructions and their behaviour if dst_out is longer then input

* clippy: Allow extreme comparisons for AD in AEAD
@brycx brycx closed this as completed in #309 Dec 7, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
2 participants