The problem is that I'm pretty sure that storing together, the salt, the digest of the passphrase - which is at the end the symmetric key used to encrypt the private key - and the private key itself, encrypted with the digest of the passphrase, is a naive solution.
Storing the digest of course negates both the PKBDF function and the encryption. Storing the salt and wrapped key is of course not a good idea.
But if I don't save the digest of passphrase together with the encrypted private key, how can I be sure, when decrypting the private key with an inserted passphrase, that the passphrase and the obtained private key are the correct ones?
The easiest way to do this is to use the key for authenticated encryption such as GCM or SIV. If the authentication succeeds then you know that the key was encrypted with the key derived from the passphrase.
So you'd store:
salt | authenticated_ciphertext
where the tag is the authentication tag calculated by the authenticated encryption.
You'd calculate the salt by using a secure random number generator, the ciphertext as:
key = PKBDF2(password, salt, iteration_count, key_size)
authenticated_ciphertext = SIV_AES(key, encoded_rsa_key, [])
Now decryption will fail as SIV_AES includes an IV that doubles as an authentication tag. [] is the additional authenticated data (AAD) which is the empty array, i.e. it is not present.
A disadvantage of this scheme is that you don't know on failure if you have the wrong passphrase or the wrong ciphertext. The simplest way to deal with this is to include a specific ID with the ciphertext and treat it as Additional Authenticated Data in your authenticated encryption. Furthermore, you might want to indicate the protocol version:
protocol | id | salt | authenticated_ciphertext
and the calculations:
key = PKBDF2(password, salt, iteration_count, key_size)
authenticated_ciphertext = SIV_AES(key, encoded_rsa_key, protocol | id)
so now you've got a protocol and id which are also protected by the authentication tag.
Notes:
- in above scheme the
iteration_count
and key_size
are considered configuration parameters (i.e. stored separately from the ciphertext), possibly derived from the protocol
in the later version;
- the above scheme has the disadvantage that the authentication tag can only be verified after decrypting; this is both a disadvantage with regards to security as well as the legit user having to wait a long time for large plaintext - I won't go into the details of these weaknesses here;
- note that if active attacks are possible that the stored data may be altered; in that case you should consider that any part of the data (including the protocol, id or just the authentication tag) may have been altered - relying on any specific error condition is in that case not possible anymore.
- don't forget to zero your sensitive data in memory after you've concluded your private key calculations or if you've encountered an error condition;
If you're starting to see the full scale of what you're attempting, you might want to use an existing scheme instead.