0

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:

  1. 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.

  2. 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.

  3. 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!

Andy-Delosdos
  • 3,560
  • 1
  • 12
  • 25
  • 1
    It's generally better to hash than encrypt passwords. – Luke Joshua Park Jul 17 '16 at 00:02
  • **Do not encrypt passwords**, when the attacker gets the DB he will also get the encryption key. Iterate over an HMAC with a random salt for about a 100ms duration and save the salt with the hash. Use functions such as password_hash, PBKDF2, Bcrypt and similar functions. The point is to make the attacker spend a lot of time finding passwords by brute force. See the answer by @bartonjs for more information. – zaph Jul 17 '16 at 18:45
  • I understand using hashing is a good idea, but in this case - the hacker would not get the encryption key from the DB, because the key itself would be encrypted using DPAPI. Having said that, I am intending to use password hashing anyway, and use DPAPI + aes for the non-password user fields – Andy-Delosdos Jul 17 '16 at 20:52

2 Answers2

2

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:

  • Uses RFC2898's PBKDF2 algorithm to derive a key from a password, storing this derived value is better than storing the password, because if your credentials database is compromised it doesn't give away the password.
  • Uses a new random salt for each new user. (And can also be re-generated on password change or an upgraded login)
  • Saves (and loads) information about what settings were used when the password was set so you can change your defaults in the future.
  • Doesn't use DPAPI (DPAPI isn't bad, but it's Windows-only, and if you're using .NET Core then you might want a cross-platform solution)
bartonjs
  • 30,352
  • 2
  • 71
  • 111
  • Apologies, my original question wasn't very clear. I've updated it. Thanks for the code samples, very useful. – Andy-Delosdos Jul 17 '16 at 10:12
  • Also, with regards to the hashing, do you see any harm in saving the salt / hash / number of iterations in a blob, to be stored in a single DB column? – Andy-Delosdos Jul 17 '16 at 12:49
  • And similarly, do you seen any harm in storing the IV and along with the encrypted AES string for the non-password user data fields? – Andy-Delosdos Jul 17 '16 at 15:22
  • Typically these things are prepended to the hash and encrypted respectively, they are not secrets. – zaph Jul 17 '16 at 18:41
1

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").

Jim Flood
  • 8,144
  • 3
  • 36
  • 48
  • @zaph The OP described protecting the key with DPAPI. That would be up to Windows then -- it could be tied to the Windows userid (i.e. security principal) or to the machine. That is beyond the scope of this question, how exactly DPAPI protects the key. Google "Windows Data Protection API" for details. – Jim Flood Jul 17 '16 at 18:50
  • Understood, comment deleted. – zaph Jul 17 '16 at 18:52
  • 1
    @JimFlood Thank you for the comprehensive answer. I was planning to use RijndaelManaged().GenerateIV() to create the random IV. As there's no AES HCM mode in .NET (at the time of writing), I'll probably use AES CBC and go with your HMAC and two-key suggestion. I found an example where someone was doing something quite similar: [link to Github](https://gist.github.com/jbtule/4336842#file-aesthenhmac-cs). I'll probably use this as a base, swap to RijndaelManaged and make a few modifications to add configuration versioning so it's easy to change the salt size/iterations at a later date – Andy-Delosdos Jul 17 '16 at 21:39