1
$\begingroup$

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,
        &password,
        &salt,
        pwhash::OPSLIMIT_INTERACTIVE,
        pwhash::MEMLIMIT_INTERACTIVE,
    )

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

    // we store the IV
    let encryption_header_bytes = Bytes::from(encryption_header.as_ref());
    store encryption_header_bytes in the output file

    // then we encrypt the file
    buffer = read file content
    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();
        buffer.advance(chunk_size);
        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,
        &password,
        &salt,
        pwhash::OPSLIMIT_INTERACTIVE,
        pwhash::MEMLIMIT_INTERACTIVE,
    )
    // then we read the file in a buffer
    buffer = read encrypted_file content

    // then we read the header, and create a stream out of the header and the key
    let header = Header::from_slice(&buffer[0..xchacha20poly1305::HEADERBYTES])
                        .unwrap();
    stream_decoder = Some(xchacha20poly1305::Stream::init_pull(&header, &key).unwrap());
    buffer.advance(xchacha20poly1305::HEADERBYTES);

    // 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
            .advance(xchacha20poly1305::ABYTES + chunk_size);

        store `decrypted` in the output file
    }
}
```
$\endgroup$
  • $\begingroup$ It might be helpful if you spelled out in a little more detail exactly what your protocol is, and what API you're using. $\endgroup$ – Squeamish Ossifrage Oct 30 '19 at 15:10
  • $\begingroup$ That's right. I edited my question with a more accurate explanation of our encryption process. $\endgroup$ – Chacha123 Oct 30 '19 at 16:21
  • $\begingroup$ 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. $\endgroup$ – Squeamish Ossifrage Oct 30 '19 at 16:23
  • $\begingroup$ It's a wrapper around crypto_secretstream_xchacha20poly1305_init_push. The call to hchacha20 initializes chacha20 with the random IV it generates. $\endgroup$ – Chacha123 Oct 30 '19 at 16:47
  • 1
    $\begingroup$ Just one thing: the last block should have the Tag::Final tag, so that you can later verify that the content wasn't truncated. $\endgroup$ – Frank Denis Oct 30 '19 at 22:07
1
$\begingroup$

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

Original answer, before edit follows:

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.

|improve this answer|||||
$\endgroup$
  • 1
    $\begingroup$ 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. $\endgroup$ – Squeamish Ossifrage Oct 30 '19 at 14:50
  • $\begingroup$ 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). $\endgroup$ – Chacha123 Oct 30 '19 at 15:03
  • $\begingroup$ I also edited my question with a better vocabulary: the key is derived from the (password, salt) pair, not (password, IV). $\endgroup$ – Chacha123 Oct 30 '19 at 15:08
  • $\begingroup$ 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. $\endgroup$ – Squeamish Ossifrage Oct 30 '19 at 15:10
  • $\begingroup$ 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/… $\endgroup$ – otus Oct 30 '19 at 18:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.