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
}
}
```
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$Tag::Final
tag, so that you can later verify that the content wasn't truncated. $\endgroup$