Why not mix hashes?

When trying to hash passwords, the attacker can always use the same kind of hardware as the defender. What the attacker tries is to do better, by using specialized hardware which will allow him to hash N potential passwords for less total cost than if he were using the defender's hardware. The total cost includes the cost of buying the hardware, the cost of developing the relevant software on it, and then the cost of running the hardware, which basically amounts to the used electricity (for powering the hardware and for cooling it). For a serious attacker, the power cost dominates.

Whenever there is some operation that the attacker can do cheaper than the defender, the attacker wins. An important point is that password cracking is an embarrassingly parallel problem: by definition, the attacker has many potential passwords to try.

Suppose that you cascade three distinct hash functions Hash1, Hash2 and Hash3. This means that the defender must have all three implementations at hand, all running on his server. The attacker, on the other hand, can have a better scheduling: he can (say) hash one million potential passwords with Hash1 and save the results in some buffer; then switch hardware to something that applies Hash2, and run it over the million saved outputs from the previous step, there again saving the Hash2 outputs in some buffer; finally switching hardware again, with Hash3.

This kind of "hardware switching" is especially relevant when using FPGA: each "switching" is a reprogramming of the same actual hardware, and is a matter of a few seconds at most. By using such scheduling and buffering, the "switching" cost is negligible.

This can also be used as pipelining: if the attacker built three specialized machines, one for Hash1, one for Hash2 and one for Hash3, then he can run Hash1 on the first potential password, then send the output to the machine that computes Hash2. While the second machine computes Hash2, the first machine can compute Hash1 on another potential password. And so on. In practice, the attacker can maintain all his specialized machines at full occupancy at any time, thereby laughing at your attempts at "increased strength".

Moreover, if there are three distinct hash functions to implement and only one of them can be optimized with specialized hardware, then the attacker still gets a win by optimizing that one. To say things crudely, if you cascade bcrypt, scrypt and SHA-256, then the attacker will use a PC for the first two, and a GPU for the SHA-256, and will thus avoid about 1/3rd of the cost.


To sum up, the intuition that "mixing a set of different hash algorithms should provide additional strength" is wrong. It does the opposite. Such mixing increases development and usage costs for the defender, while it does not slow the attacker (who has a lot of parallelism to benefit with), and increases the attacker's options for optimization.

(All of this is said without talking looking into practical things, such as the management for the individual salts for all cascaded functions, and the dangers of homemade cryptography.)


I was asked to make my comment an answer, so here goes.

Basically, I said that the likes of bcrypt, scrypt and pbkdf2 are not hashes themselves, but are KDF's (key derivation functions). KDF's are built upon HMAC algorithms, which in turn are built upon one-way hashing algorithms like SHA-256 to generate the message digest values.

There is already significant mixing and stirring and shaking involved in getting a value out of a KDF.

Even if mixing the output of multiple KDF's doesn't weaken overall security, it seems likely not to materially improve it.


To copy my answer to a similar question on progs.SE:

the problem with

hash1(hash2(hash3(...hashn(pass+salt)+salt)+salt)...)+salt)

is that this is only as strong as the weakest hash function in the chain. for example if hashn (the innermost hash) gives a collision the entire hash chain will give a collision (irrespective of what other hashes are in the chain)

a stronger chain would be

hash1(hash2(hash3(...hashn(pass+salt)+pass+salt)+pass+salt)...)+pass+salt)

here we avoid the early collision problem and we essentially generate a salt that depends on the password for the final hash

and if one step in the chain collides it doesn't matter because in the next step the password is used again and should give a different result for different passwords

Tags:

Algorithm

Hash