27

Many recommendations for storing passwords recommend hash(salt + password) rather than hash(password + salt).

Doesn't putting the salt first make it much faster for the attacker to bruteforce the password, because they can precompute the state of the hashing function with the bytes of the salt, and then each time of their billions and trillions attempts they only need to finish calculating the hash using the bytes of the password.

In other words, each bruteforce iteration needs to calculate only the hash of the password intermediateHashState(password) instead of the whole hash(salt + password).

And if the salt was placed after the password, the attacker wouldn't have this shortcut.

Does this advantage exist and is it significant?

2
  • 14
    Resources that recommend either of those two options are wrong. Commented Jun 8, 2019 at 1:49
  • @FutureSecurity: hash(salt + password + salt) doesn't seem outright wrong...
    – user541686
    Commented Jun 10, 2019 at 20:15

4 Answers 4

14

Hash functions recommended for password use do not have this advantage - I'm not sure that any non-trivial hashing functions do, in fact, but wouldn't want to make a blanket statement there.

Instead, hashing functions mix parts from the whole input in each stage. For example, given the input ABCDEFGHIJKLMNOPQRSTUVWXYZ, the first step could be something like pairing the first character with the last, and iterating until no input is left. This would give AZBYCXDWEVFUGTHSIRJQKPLOMN. Assume the "salt" was ABCDEF, and the password was the rest, then change the password to PASSWORD - the output of this example first step would be ADBRCODWESFSPA, which wouldn't help you with the original hash in the slightest.

This is just an example - real hash functions work on binary values, and perform more complex mixing, for a couple of differences - but you can see that it doesn't matter where the salt is for the output to change at a very early point.

There is even a principle (the Avalanche Effect) which suggests that a single bit being changed in the input to a hash function should change about 50% of the output bits, and most hash functions, even ones which are now considered unsafe such as MD5, follow this (while my example above does not!).

Essentially, you can't pre-compute part of a hash in the way you suggest, unless there are other flaws in the system. If you, for some reason, hashed the salt, took the first half of the output, and bolted it onto the second half of the hash of the password for storage, that would introduce the theoretical weakness so you'd only need to compute the hashes of passwords.

7
  • 24
    I don't quite agree with the explanation. Hash functions do usually work in blocks, so they don't mix parts from the whole input in each stage. If you have two plaintexts that share a common prefix of blocks, the hash state of the common blocks can be re-used. However, the block size of even SHA-256 is 64 bytes, so this is not an issue for this application.
    – wrtlprnft
    Commented Jun 7, 2019 at 21:27
  • 13
    The explanation in paragraph two is nothing like real world hash functions. Plus it doesn't follow from a hash function exhibiting the avalanche effect that there is no way to use partial evaluation to optimize brute force. Commented Jun 8, 2019 at 1:48
  • Aha. So a 64-byte salt could actually make it easier to bruteforce.
    – user123540
    Commented Jun 8, 2019 at 5:43
  • 3
    @Voo, that’s just one of the reasons why you shouldn’t be using any SHA variant for hashing passwords. You want a specialized password hashing algorithm which doesn’t have that problem.
    – cjm
    Commented Jun 9, 2019 at 0:12
  • 4
    @cjm : PBKDF2 is not a perfect password hashing function, but the fact it is based on SHAx is not seen as one of its weaknesses. Commented Jun 10, 2019 at 9:02
35

Actually, you are tackling it the opposite way.

It is true that doing hash(salt + password) would allow you to precompute the salt (but see note below) and only hash each password candidate for all those trials. The cost be the same you would bear if you were not using a salt at all.

However, the goal of the salt is not to make bruteforcing a single hash harder, but to ensure that the hashes for different users are different even if they chose the same password and so the cracking effort can't be applied for multiple users.

Let's assume you dumped a PayPal database hashes where they were using MD5 hashes. You want to check passwords 'paypal', 'PayPal', 'PayPal123'...

  1. If they used MD5(password), you can trivially hash any of them and find out if anyone is using such weak password.

  2. If they used MD5(salt + password), you could precompute the partial MD5(salt) for everyone, but still need to hash each password candidate for each user.

  3. If they used MD5(password + salt), you could precompute the partial MD5(password) for each candidate password, and then apply their salt for each user.

#1 is clearly the worst here. You could argue between #2 and #3 based on the different lengths of passwords and salts, as well as the number of users, but I would consider #2 to be preferable. Based on length alone, the enforced minimum length for your passwords is probably higher than the salt size. But I suspect there could be other weakness with the #3 construct, too.

Is it a significant advantage?

Not really.

First of all, many hash functions work in blocks, and the precomputation for values smaller than the block size simply store a copy of the "precomputed bytes". In 99% of the cases the length of both salt and password will be shorter than the block size, so actually there would be no real precomputation being performed. You would need to be using really long strings there for that to be of use.

Additionally, any modern password hash function will at the very minimum use many iterations, if not using more advanced means to make bruteforcing expensive, and your optimization is only applicable the initial iteration.

In any case, rather than simply concatenating salt and password, the most secure way to combine them would be to do an HMAC over them, which mixes them in a better way.

10
  • 5
    Are you sure about the ability to precompute MD5(password) and later add the salt (or vice versa)? If the password is the size of an MD5 block I wouldn't doubt it, but MD5 uses a 512 bit block and most passwords (or salts) aren't exactly 64 characters. Commented Jun 8, 2019 at 0:10
  • @AndrolGenhald it would partially precompute, up to the working register that requires salt bytes Commented Jun 8, 2019 at 0:12
  • 1
    @Ángel MD5_update() will not precompute unless you have a full block, anything less and it will just keep it in a buffer Commented Jun 8, 2019 at 0:28
  • 1
    @Ángel Then don't you think it's a little bit misleading to say you can precompute the partial MD5(salt)? A salt or password by itself is almost never going to be large enough to compute the first block. Commented Jun 8, 2019 at 0:45
  • 2
    @AndrolGenhald you are completely right. My answer was in a different line, and I didn't take into account this. I have edited the answer adding a paragraph about this on the part discussing if it would be a significant advantage.
    – Ángel
    Commented Jun 8, 2019 at 0:59
25

Many recommendations for storing passwords recommend hash(salt + password) rather than hash(password + salt).

Those recommendations are evidently bad, because what they should be telling you is to use a password hashing function that's been specially designed for the purpose, like (in rough order of newer and betterish to older and worse-ish):

  • Argon2 (best)
  • scrypt
  • bcrypt (not bad but beginning to look dated)
  • PBKDF2 (far from ideal but much better than homebrew password hashing)

These functions either:

  1. Take the password and the salt as separate arguments, and thus take care of your question internally;
  2. Give you a higher-level API that takes care of salt generation and management internally:
    • An "enrollment" function that takes a password, generates a salt, and outputs a verification string that encapsulates both salt and hash (e.g., password_hash() in PHP);
    • A "verification" function that takes a password and verification string, and verifies that the password matches the latter (e.g., password_verify() in PHP).

If you're manually concatenating passwords and salts, you're doing it wrong.


That said, it's generally better for a password hashing function to absorb the salt first and the password after. Why? Because that order is generically better at resisting precomputation attacks. In the password-first order, the attacker can precompute the intermediate states that correspond to common passwords, and perhaps there is some clever way of building some sort of big table that exploits this to compute their salted hashes quicker than they could otherwise. Whereas the salt-first order makes this impossible, more so if salts are random.

Doesn't putting the salt first make it much faster for the attacker to bruteforce the password, because they can precompute the state of the hashing function with the bytes of the salt, and then each time of their billions and trillions attempts they only need to finish calculating the hash using the bytes of the password.

No, because the point is that the attacker is not supposed to learn the salts before they steal the password database. That bit of precomputation you mention only gives a tiny speedup relative to the cost of a well-designed password hashing function, and the precomputed state is only good for attacking one individual password entry (assuming no duplicate salts, which is a requirement anyway).

In contrast, with the password-first order, they can:

  • Precompute and store hash function states for large numbers of common passwords before they ever steal the password database;
  • Reuse the precomputed tables across password entries hashed with different salts, even across multiple password databases.
9
  • 3
    PBKDF2 should be retired. If a developer has the freedom to choose what algorithm to use then they shouldn't use it. Or any function that only stretches passwords by iterating an easily parallelized hash many times. Bcrypt is a step up - it's a simple algorithm iterated many times, but it's not one that GPUs can do easily - but many-core computers and FPGAs can attack it much more efficiently Commented Jun 8, 2019 at 2:01
  • 2
    @forest Even if you have SHA-1 acceleration, you should not use SHA-1. The objective here is to minimize the ratio of (attacker's hash rate)/(your hash rate). A memory-hard hash scores better than SHA-1 no matter what hardware acceleration you have. Recall that you're hashing one password at a time while the attacker isn't.
    – Navin
    Commented Jun 8, 2019 at 15:30
  • 2
    @jpmc26: I don't actually disagree with your take on PHP, but strangely enough its modern password hashing API is exemplary and making more people aware of it does the world a lot of good. Commented Jun 9, 2019 at 4:55
  • 2
    @Navin You're absolutely correct, and a proper memory-hard KDF beats hardware-accelerated SHA-1 any day. I was just pointing out that I disagree with the idea that one of PBKDF2's problems is its flexibility. While normally I do agree that flexibility is a bad thing in cryptography and developers shouldn't be given access to complex APIs, in this specific case, I think PBKDF2's flexibility is actually a positive for the aforementioned reasons. Also technically you could probably make PBKDF2 memory-hard since it works with any keyed PRF, not just HMAC. Consider a memory-hard keyed PRF.
    – forest
    Commented Jun 9, 2019 at 7:24
  • 3
    One key advantage that PBKDF2 still has is that it's the only one that's NIST approved, which is relevant in orgs where compliance is important. Hopefully they'll publish a new recommendation soon, though.
    – James_pic
    Commented Jun 10, 2019 at 10:34
2

While the above answers do raise some valid points they do not seem to be entirely correct and it should be noted that there do in fact exist weaknesses in certain hash functions that make a significant difference in the cracking speeds for "$pass.$salt" or "$salt.$pass".

See for example md5. According to atom (https://hashcat.net/forum/thread-8365.html), creator of the hashcat password cracking software:

The way how hashcat exploits some weakness in MD5 to get additional acceleration requires it to change only the first 4 bytes of the input data. Since this part is fixed (because of the salt) it cannot use the acceleration.

That does reflect significantly in the cracking speed.
On a Titan RTX the speed for md5($pass.$salt) is with 63819.9 MH/s almost double the speed as for md5($salt.$pass) with "only" 34696.2 MH/s.

Similar differences exist for other hash algorithms. The developers and community of password cracking software spend a significant amount of time to find weaknesses and shortcuts to improve speed. So from a cracking perspective it can be a good idea to look at current hashcat benchmarks like this one https://gist.github.com/Chick3nman/5d261c5798cf4f3867fe7035ef6dd49f and compare the speeds for the different Salt-Password-Variants.
If that link goes missing, you can also generate your own benchmark with the following command:

hashcat -b

You must log in to answer this question.