I am currently writing a password manager application. As usual, the passwords are put in a file encrypted using a user-entered password. I'm using the NaCl library but actually it's not really important.
Here is what I do to encrypt the file content with a user-entered password P:
- First I generate a random salt S of 32 bytes (256 bits) and store it at the start of the encrypted file.
- Second I derive a secret encryption key K from the password P and salt S using Argon2i.
- I then compute a hash H of the file content using SHA-512.
- Then I encrypt and authenticate the hash H with the XSalsa20 and Poly1305 primitive of the NaCl library (crypto_secretbox functions) using key K and nonce 0 and I store the encrypted and authenticated hash in the encrypted file after the salt.
- Finally, I split the file content into blocks of 1024 bytes, except for the last block which may be less than 1024 bytes. The blocks are encrypted/authenticated and stored in the encrypted file in order, one by one, using the XSalsa20 and Poly1305 primitive. The first block is encrypted with key K and nonce 1, second block with nonce 2, third block with nonce 3, etc...
The file content length is not hidden since the last block is not padded to 1024 bytes but I don't believe it's a problem for a file holding logins and passwords.
Each time a file is encrypted or re-encrypted, for instance after having been modified, a new salt S is generated to prevent the same key K from being used twice.
To decrypt the file, the plaintext salt is used to derive the key again from the password entered by the user, then the hash H and file content are decrypted and authenticated and a hash of the decrypted file content is computed and checked to see if it match the initial hash H.
The reason I'm computing a hash H and encrypting and authenticating it is to prevent the individual encrypted blocks of the file from being added, removed or swapped. If an attacker try to do this, the hash H will not match at decryption. I'm also using block size of 1024 bytes because the NaCl website warns that primitives are only tested with message lengths up to 4096 bytes (https://nacl.cr.yp.to/valid.html). 1024 is completely arbitrary, I could have chosen 2048, 3159 or 4096 but I thought "Better take some margin" and I feel power of 2 are neater.
It's obvious that in a given file, a combination of K and a given nonce is never used twice, since the nonce is incremented each time the key K is used. The same key K is also never used twice since it depends on the salt which is random and changed each time and is therefore never the same. So unless I'm overlooking something, it seems to me that my scheme is not vulnerable to a reused key attack.
However, the NaCl website also warns that "NaCl does not make any promises regarding the resistance of crypto_stream to related-key attacks". Since crypto_stream is XSalsa20, I believe this warning also applies to XSalsa20 + Poly1305. It makes me fear that two secret keys K1 and K2 derived from the same password P using Argon2i and two different salts S1 and S2 are somewhat related, since they come from the same password.
On the other hand, I'm thinking that even if a mathematical relation exists between K1 and K2 it's not a simple direct one. Otherwise we could compute, say Argon2i(P, S1) from Argon2i(P, S2) and that would mean that Argon2 was flawed since the salt would be ineffective.
Does two keys K1 and K2 derived from the same password P but from two different salts using Argon2i are vulnerable to a related key attack ? By the way, do you see any other flaws in my file encryption scheme? My main goal is confidentiality but it's a plus if my application can detect files which were tampered with, that is authenticity.