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