Reputation: 3720
I am just about to write a new encryption system for a website I'm currently working on, and wanted to see if i could get someone to sense-check it before I get started if possible!
Update: Should have been clearer with my original question. I need to encrypt some user data that I also need to be able to read back at a later data. And, I also need to store the users password or a hash of the password for verifying the user upon login.
The plan is:
Master Key: Create a DPAPI key-setter application to take a text-based master key, encrypt via DPAPI, then save the encrypted output to a text file on the server. This is a one-off task that I'll perform every time I move the site to a new server. The master key will be used to perform AES encryption.
When a new user registers:
2.1. Save password data / hash of password data.
2.2. Load master key file, decrypt the key using DPAPI. Use the decrypted master key, and a new random IV for each piece of user data to create an AES-encrypted string. Save each encrypted string by prefixing the encrypted string with the corresponding random IV, and insert into a varchar column in the database.
Upon user login:
3.1. Match password hash to validate user.
3.2. For each encrypted user data field, split content into two parts: the IV and the encrypted data. Taking the master key from DPAPI, and the IV, decrypt the data and display on-screen.
How does that sound? Are there any obvious flaws to the above?
I'm new to this having previously used Enterprise Library Security for this sort of the stuff in the past (which is no longer available in .NET core!), so any help would be massively appreciated!
Upvotes: 0
Views: 2654
Reputation: 8467
Ignoring the question of passwords, and just considering user data, your scheme is fine, but can be improved.
Basically, you have a master key -- a symmetric key or maybe 16 or 32 bytes, that is protected at rest (on disk) and you decrypt this in memory on the server.
User data is encrypted using the master key, with a random IV for each piece of data. Be sure to use cryptographically strong random data.
You are storing the IV and ciphertext together, which is fine. Although, the IV and ciphertext are binary so you'll either have to use a blob database type, or encode the binary using Base64 to store it as varchar.
What you should consider adding to this is some form of tamper detection. For example, you could use two master keys -- one for encrypting/decrypting, and one to use for an HMAC on the data. You would use the encrypting key to encrypt the data, and then, apply an HMAC on the IV + ciphertext (using the second master key for HMAC) and store the HMAC alongside the IV and ciphertext (you could even just append it). Before decrypting, you validate the HMAC. This tells you if anything in the IV + ciphertext has been altered.
You did not mention padding. If you use AES CBC mode, you'll need to pad the data if it is not a multiple of 16 bytes. With padding, you just have to be careful that you don't accidentally provide a "padding oracle", where an attacker can send arbitrary ciphertext to the server and the response from the server tells the attacker either "padding error" or "invalid decryption" -- i.e. there is a difference in the server response.
If you use AES GCM mode, then the equivalent of HMAC is built-in -- you only need the single master key, and GCM itself will detect tampering of the ciphertext. This also lets you include "associated data" which is not part of the encryption, but is included in the "authentication", i.e. as if it were included in the HMAC. For example, the user name, "Joe", could be included as associated data in the GCM encryption of Joe's data. Then, when GCM mode decrypts the data, not only would it detect tampering of the IV + ciphertext, but the user name must also still be "Joe" -- note that "Joe" is not encrypted here.
Individual IV, random each time, for each piece of data, is a very good idea. You might also think about how long the master key is used before it is replaced. It should have a limited lifespan, and when it is "rotated" or "rolled" or "rekeyed" (all meaning the same thing -- you are replacing it) you'll need some way to re-encrypt everything. You want to change the master key periodically (it could be 3 months, it could be a year) to 1) limit the amount of data that is exposed should the master key be compromised (e.g. only 3 months worth) and 2) limit the amount of data a single key is used to encrypt (because technically, there is a limit to how much data you can encrypt with a single key and still be mathematically "secure").
Upvotes: 1
Reputation: 33108
You should never store a password if you can avoid it.
I'm unclear what the password is in step 1. If a new user isn't showing up until step 2, whose password is this?
Anyways, for your users a much better plan is to use/save derived material. For example, upon new user registration do something like
byte[] exportBytes;
byte[] exportSalt;
int exportPasswordSettingsVersion = YourSystemConfiguration.NewPasswordSettingsVersion;
using (Rfc2898DeriveBytes registerer = new Rfc2898DeriveBytes(
newUserPassword,
YourSystemConfiguration.GetSaltSize(exportPasswordSettingsVersion),
YourSystemConfiguration.GetIterationCount(exportPasswordSettingsVersion)))
{
exportSalt = registerer.Salt;
exportBytes = registerer.GetBytes(
YourSystemConfiguration.GetDerivedKeySize(exportPasswordSettingsVersion));
}
Then you export the salt bytes (which were randomly generated for you), derived password bytes, the sett as part of the user's profile. When the user comes to log in, you load these values back and check that they match:
using (Rfc2898DeriveBytes verifier = new Rfc2898DeriveBytes(
inputPassword,
loadedProfile.Salt,
YourSystemConfiguration.GetIterationCount(loadedProfile.PasswordSettingsVersion)))
{
byte[] verifyBytes = registerer.GetBytes(loadedProfile.PasswordVerify.Length);
if (!ConstantTimeEquals(verifyBytes, loadedProfile.PasswordVerify))
{
return false;
}
if (loadedProfile.PasswordSettingsVersion < YourSystemConfiguration.GetIterationCount(exportPasswordSettingsVersion))
{
// Re-derive their password and save it with your newer (stronger, presumably) cryptographic settings.
}
return true;
}
This scheme:
Upvotes: 2