# XChacha20poly1305 vulnerability to known plaintext attacks

During a security audit, the guy said our cryptosystem is vulnerable to known-plaintext attacks. He said that's because «the implementation uses a crypto_stream_xor() approach» (from libsodium). That's way above my skill, and I did not find anything relevant so far so I'm trying to understand if that's really a problem.

Here is what we do: we derive a key using a global (password, salt) pair. This key −always the same− is used in order to cipher all the documents using xchacha20poly1305. We initialize the stream using xchacha20poly1305::Stream::init_push: it provides us with a header (the nonce ?) that we store with the file for decryption later on. We use rust's implementation of libsodium, sodiumoxide.

• Is its assessment correct? (= is xchacha20poly1305 or our usage vulnerable to KPA, and is it due to the fact it uses a xor ?) Didn't the guy mistakingly mixed things with the fact that we reuse password?
• How bad is it? Does it mean that someone who can cipher a known document in our system can decipher anything else it puts its hands on?

Edit: As requested, here is what we do, with more details, using pseudo rust code. I edited out all the irrelevant details but hopefully left enough relevant information and comments.

password, salt, chunk_size are stored in environment variables and never change.


function encrypt(file, password, salt, chunk_size) {
// first, we derive a key
sodiumoxide::crypto::pwhash::derive_key(
&mut key,
&salt,
pwhash::OPSLIMIT_INTERACTIVE,
pwhash::MEMLIMIT_INTERACTIVE,
)

// Then we initialize the encryption stream
xchacha20poly1305::Stream::init_push(&key);

// we store the IV
store encryption_header_bytes in the output file

// then we encrypt the file
while (there are data in the file) {
// cipher the buffer using xchacha20poly1305's stream
let encoded = stream.push(&buffer[0..chunk_size], None, Tag::Message).unwrap();
store encoded in the output file
}
}

function decrypt(encrypted_file, password, salt, chunk_size) {
// first, we derive a key
sodiumoxide::crypto::pwhash::derive_key(
&mut key,
&salt,
pwhash::OPSLIMIT_INTERACTIVE,
pwhash::MEMLIMIT_INTERACTIVE,
)
// then we read the file in a buffer

// then we read the header, and create a stream out of the header and the key
.unwrap();

// then we decrypt the rest of the file using said stream
while(there are data to decrypt in the buffer) {
let (decrypted, _) = stream
.pull(
&buffer[0..(xchacha20poly1305::ABYTES + chunk_size)],
None,
)
.unwrap();
buffer

store decrypted in the output file
}
}
$$$$

• It might be helpful if you spelled out in a little more detail exactly what your protocol is, and what API you're using. Oct 30 '19 at 15:10
• That's right. I edited my question with a more accurate explanation of our encryption process. Oct 30 '19 at 16:21
• Two suggestions: 1. Link to the documentation for xchacha20poly1305::init_push. 2. Write down the protocol in enough detail that you can write a compatible implementation in another language—and use that compatible implementation to generate deterministic known-answer test vectors, and then write automatic tests against those test vectors in your main implementation. Oct 30 '19 at 16:23
• It's a wrapper around crypto_secretstream_xchacha20poly1305_init_push. The call to hchacha20 initializes chacha20 with the random IV it generates. Oct 30 '19 at 16:47
• Just one thing: the last block should have the Tag::Final tag, so that you can later verify that the content wasn't truncated. Oct 30 '19 at 22:07

Using crypto_secretstream that should be fine. It generates a random nonce for each message. Reusing a key in that context is fine.

Yes, that's bad. You have to use a unique IV/nonce for each document.

A stream cipher, such as ChaCha, which is combined with the plaintext using XOR reveals the keystream to anyone who knows the plaintext. That allows them to decrypt other documents. And even if they do not know the plaintext, there can be attacks.

Every document has to have some unique identifier that you can use as a nonce. It does not have to be secret or unpredictable.

• Not just every document, but every version of every document too. For example, if you're encrypting a database field with this and you hash the table name and column name and rowid into a 192-bit nonce, that's still not enough if you ever update the database field with a different value. Oct 30 '19 at 14:50
• Isn't that taken care of by xchacha20poly1305::Stream::init_push`? I thought it was in charge of generating the necessary random nonce (that we store in the header of the file for decryption). Oct 30 '19 at 15:03
• I also edited my question with a better vocabulary: the key is derived from the (password, salt) pair, not (password, IV). Oct 30 '19 at 15:08
• That depends on the semantics of the operation you're talking about, which is not itself part of libsodium and which I'm not familiar with. With crypto_secretbox_xchacha20poly1305, it is your responsibility to assign to each message a unique message number. Maybe the wrapper you're using automatically generates a 192-bit nonce uniformly at random and passes it along with the message, and that's a reasonable thing to do in some cases—but it's better if you can choose message numbers sequentially, like TLS does with the record number, so you can detect replay attacks and buggy implementations. Oct 30 '19 at 15:10
• Ah, you didn't mention crypto_secretstream. If you are using that, then it generates the nonce for you: github.com/jedisct1/libsodium-doc/blob/master/…
– otus
Oct 30 '19 at 18:32