4

I want to use .NET's Rfc2898DeriveBytes functionality in order to store my passwords in a database. In order to use it, I still need to make several decisions which will influence the level of security provided. These decisions are:

  • Salt length
  • Add pepper before hashing or not
  • Pepper length
  • Iteration count
  • Algorithm choice (MD5 vs. SHA1 vs. SHA256 vs. SHA384 vs. SHA512)
  • Hash length
  • How to combine the parts for the DB
  • The size of the DB field(s) needed

What are sensible values for these (in the current year 2019)?

schroeder
  • 123,438
  • 55
  • 284
  • 319

1 Answers1

2

Salt length

As mentioned at some point in this answer

16 bytes are enough so that you will never see a salt collision in your life, which is overkill but simple enough.

Pepper

According to a comment under this answer, you should indeed use it:

You make a pepper look as something only useful with a HSM, which it is not. Its purpose is to have different salts on different places, thus forcing the attacker to compromise all of them. Typically, the salt is stored with the username in the db but the pepper is stored in the login server. Thus making some leaks resistant to offline guessing: A broken RAID disk from the database server is leaked, but the pepper was stored in the web server; or the db was obtained through a SQL injection but the configuration file is not.

However, according to this question, the server side key should not be a "pepper" (= added to the cleartext password and then hashed):

There is a better way to add a server side key, than using it as a pepper. With a pepper an attacker must gain additional privileges on the server to get the key. The same advantage we get by calculating the hash first, and afterwards encrypting the hash with the server side key (two way encryption). This gives us the option to exchange the key whenever this is necessary.

Instead, the result of the hashing process should be encypted with a secret key which is kept out of the database (for example, it can be kept in the code). By using (two-way) encryption for this, it can be re-encrypted in case the key has been leaked. Example code for doing this can be found in this answer(using 2 distinct keys instead of one in this case).

Iteration count

This needs to be tested on the actual hardware. More iterations make it take longer for an attacker, but also for your users. An often stated goal is to make it take around a second to log in.

Algorithm choice, Hash length

According to this answer:

Choosing a derived key length that is less than the output length of the hash function makes little sense, [...] I'd suggest SHA-512 as the PRF, with a 512-bit derived key

So SHA-512 as algorithm, and in C# we would use .GetBytes(64) on our instance of Rfc2898DeriveBytes in order to get a 512-bit derived key.

How to combine the parts for the DB

There are several ways to do this. I would advise the following pattern:

{hasherVersion}${encryptionVersion}${payload}

hasherVersion is a number for the "version" of the hasher. Every time you change your way of hashing - be it increasing iterations or switching the algorithm entirely - you increment this number. This way, you can see if it was stored with an older version and can update it accordingly (e.g. when a user logs in).

encryptionVersion is pretty much the same, but for the encryption. Additionally, this allows you to update encryption in the database in parts.

payload is the actual encoded password. In order to create the payload, the steps are:

  • Create the random salt
  • Using the salt, create a hash from the password
  • Concatenate salt and hash into a single byte[80]
  • Encrypt that byte[], using the server-side secret
  • Convert the resulting (encrypted) byte[] to Base64

The size of the DB field(s) needed

120 characters should be enough for a long, long time:

  • 108 characters for the Base64-encoded salt+hash
  • 2 characters for the 2 delimiters
  • 5 characters each for hasherVersion and encryptionVersion

The first 2 are hard numbers. For the versions, well, probably 3 each is already enough (if you need to change those more than 999 times, you should probably try something else), but 5 each gives us a nice round end result of 120 characters.

  • Nice answer, I would argue the "combine the parts for the DB" section looks like security by obscurity and forces clients to do the splitting, but thats is not big deal security wise (the important points are the previous ones) – bradbury9 Nov 05 '19 at 14:31
  • @bradbury9 Well, the idea was not security by obscurity, but more a way to use the fact that we know exactly how long each part is to store the whole thing in one DB field. As an alternative, I have also seen patterns like e.g. `$$`. Honestly, I'm not sure yet which of those 2 is really better; seems to be readability vs. saving a tiny amount of storage - both of which seem like they don't really matter. – Raphael Schmitz Nov 05 '19 at 14:50
  • because of interoperability I would suggest having each field in its column – bradbury9 Nov 05 '19 at 15:08
  • It is not only it does not follow normal rules, but if in the future you have to check the # of iterations (to make sure it is safe) separating that info into its own column would prove useful. – bradbury9 Nov 05 '19 at 15:21
  • Many authentication systems that use key-stretching will use the dollar-sign delimited approach, with the algorithm being the first section, arguments to the algorithm being the second section, and the salt-and-hash concatenated together being the third section. For example, see the results when using bcrypt and Argon2i for the [PHP `password_hash()`](https://www.php.net/manual/en/function.password-hash.php) function. This lets the auth system verify passwords with different algorithms as best practices change, to allow upgrading algorithms over time as users log in. – Ghedipunk Nov 05 '19 at 20:03
  • `PBKDF2(PRF, Password, Salt, c, dkLen)`, `PRF` the hash function to use, `c` is the iteration count and `dLen` is the desired output length. Your answer need this! – kelalaka Nov 12 '19 at 20:01
  • @kelalaka IMHO, That belongs in the documentation of that API (if you ignore the official documentation and can't get it to work, it's really your own fault). Another point: I actually used [this one](https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.rfc2898derivebytes.-ctor?view=netframework-4.8#System_Security_Cryptography_Rfc2898DeriveBytes__ctor_System_String_System_Byte___System_Int32_System_Security_Cryptography_HashAlgorithmName_), where it's `(password, salt, iterations, algorithm)`. – Raphael Schmitz Nov 13 '19 at 08:06