Epistemic status: confidant. I'm not a cryptographer, but no one raised strong objections and I thought about it a bunch since first thinking about it.

Everyone talking about password peppering is missing a better method to implement it. 

whether you think peppers are good or bad, knowing this is valuable. If you think they're good, this is a better method to use them. If you think they're bad, this is a stronger version to argue against. The main point of this post isn't to endorse peppers (though I do think they're good), but to compare the common methods to an alternative one.

Terms and background, in short

Skip if you're familiar 

Hash: Secure password storage doesn't store passwords, so if the database leaks, the passwords aren't revealed. Instead you use a hashing function, which takes any password, and turns it into a seemingly random (very large) number, this is your hash, and this is what you store instead of the password. Hashes are hard to reverse, and unlike encryption, hashes don't have keys to decrypt them. When the user logs in, the password is hashed again and compared to the stored hash.

Salt: A random string that is added to to the password before it's hashed, and is usually stored in plain text near the now salted hash (so each salt is connected to a user). When the user logs in the salt is again hashed with their password and compared to the stored hash. Salts prevent rainbow table attacks, and make the hashes of passwords shared by multiple users unique so each password has to be cracked individually. However, if an attacker targets a specific user and is willing to bruteforce their password, a salt will do nothing to stop them, and the user will have to rely on the strength of their own password.

Pepper: A pepper is a secret salt that is stored without connection to a user (for example at the top of the database[1]), or not stored at all. Peppers make it harder to crack individual passwords even in the case of a leak. Peppers should always be used together with salts.

Common Approaches

There's two main approaches to peppering, both should be in addition to salts.

  1. You generate a really long number (128+ bits) and store it outside the database, then whenever you need to hash a user's password you add it to the password before hashing.
    • Pros: if attackers don't know the pepper it's impossible to crack any of the passwords.
    • Cons: If the attackers find the pepper it becomes useless, and if they know the salt and plain text password of some user (could even be themselves), and which hashing algorithm was used, they can potentially use that to brute force the pepper.
  2. You pick a string at random and use it a the pepper, but don't store it at all, so every time the user logs in you basically have to brute force all the peppers until you find the right one. For it to not take ages, you use a small string, say 1-2 characters.
    • Pros: It makes the attackers X times slower because they have to try every possible pepper on each password.
    • Cons: the attackers will likely know which peppers are possible, since it's likely 1-2 characters out of the allowed character set. And users that got letters that hackers are likely to start with just out of convenience are at a serious disadvantage.

My method is a combination of the two:

Do note that I'm not saying no one has come up with this before, just that I didn't see it.

Generate some (say, ~100) really long numbers, and store them outside the database. When a user registers, you pick one of them at random and use it as their pepper. Whenever they login you try each of the stored peppers until one works. 

Pros over #1: 

  • Your peppers aren't easy to crack anymore, even if one pepper is brute forced, there's still all the other that remain unknown.
  • If the peppers leak, then the attackers still don't know which pepper is used for which user, and so are still slowed down by the amount of peppers you chose to generate.

Pros over 2#: 

  • The peppers are practically impossible to guess, while still giving the benefit of having a lot of different ones.
  • This makes cracking even a weak password like '123' impossible without already knowing the pepper used or which peppers to check.
  • Authentication isn't slower since appending is quick and you choose the amount of peppers you use.
  • You don't lose much security from stored peppers since the possible peppers were already quite predictable with the common method.
  • You have better control over the amount of possible peppers, and it's easy to gradually add more peppers with time.

I think this should be more secure than both methods, with little downside, and barely any increased complexity - Yet I wasn't able to find any source that describes something similar.

  1. ^

    Many sources talk of storing the pepper "outside the database", and a previous version of this article used this phrasing as well (though did mention it's not strictly necessary). The reason that, as the rest of the article explains, conventional implementations require a secrecy assumption. Though storing it "outside the database" does make it less likely the pepper would leak if the database leaks, assuming that you can keep the pepper secret if you can't keep the passwords secret is just a bad and insecure assumption. The method I suggest still benefits from secrecy (so storing "outside the database" won't hurt), but doesn't require it.

New Comment
9 comments, sorted by Click to highlight new comments since:

The core problem is that using a decent password hashing algorithm with non-secret per-service per-user salts gives you really, really good security with respect to salts.

Adding secret keys (salts, whatever) to your security assumptions is the kind of thing that gives cryptographers an allergic reaction - and I think you're seriously underestimating the difficulty of implementing the system too, for very little gain. Better to spend your time protecting users from social engineering attacks, or insider compromise, or any of the myriad other attacks which are far more likely to defeat the sensible conventional approach.

I agree that salts already give very good security on their own (even if I personally like the idea of adding peppers), and it's entirely possible that I'm underestimating the difficulty of implementing it. I also agree that using secrecy assumptions is bad (that's my problem with the first method, which is fully based on secrecy and becomes useless if the secret is revealed).

My main curiosity here is how does my method compare to the usual ones. Since, if we take the secrecy example, mine doesn't depend on secrecy (unlike #1, like #2) but still benefits from it (like #1, unlike #2).

The purpose of password hashing is to preserve security even when a hacker gets access to your data. Saying you store the really long numbers "outside the database" is cheating. The numbers must be accessible to the server. If a hacker compromises the server then they have access to both the hashed passwords and the really long numbers. You basically just said "hide the salt". If you can hide the salt then you can hide the passwords themselves and there is no need to hash them in the first place.

If the hacker has access to your really long numbers then your proposed system is on average equivalent to a 50× slower hashing algorithm. But a 50× slower hashing algorithm is superior because it has constant time performance whereas yours fluctuates randomly between 100× slowdown to no slowdown.

Edit: Actually, the slower hashing algorithm is even better because it can't be parallelized.

Edit: Actually, the slower hashing algorithm is even better because it can't be parallelized.

That's true when you're checking one password, but a slower hashing algorithm doesn't stop you from checking multiple passwords at once (which is something you do when cracking passwords but not when authenticating them). Still, it's something I haven't thought about, so thanks for pointing that out.

Yes, password security is supposed to provide security even when a hacker gets access to all your data, but it doesn't mean secrecy is completely disregarded, after all no one makes their password hashes database public, and leaks are still a security breach even if the passwords were hashed and salted correctly. So storing it in a harder to compromise place (outside the database, outside the hard drive, outside the computer, whatever) is only "cheating" if you completely rely on it and assume it would work. Which I didn't. My problem with the first method is exactly that, that it's useless if the secret stops being secret.

But, if you really want you can still store them in the database, The important point is that you don't store which peppers are used for which users, which you still do for salts even if you try to hide them somehow.

The hacker does get slowed down almost 100X with a 100 peppers when cracking passwords, since to reject a password they have to try all the peppers, and most likely they have to try a very large number of passwords before finding the right one. On authentication it does take on average half the time, since you usually check correct passwords.

Well, except that now checking a user's password may take up to 100* time. So you're just trading 100* time for you to check the password against 100* time for the attacker to crack it. What's the difference with just using a more complex hashing method? AFAIK it's the same tradeoff.

Edit: wait, that's only if the peppers are leaked. If cracking a single pepper is the most time-consuming part for the hacker, then this method allows to make that part specifically more difficult. I... guess it works (not a specialist).

You don't even need a more complex hashing method. You can just repeat the same hashing algorithm 100 times.

Yup, and it's already common practice to do 100K+ iterations (Django's default, for example, is 260,000, and in one of their next updates the default is going to be raised to 360,000. And it doesn't feel slow even on a weak laptop like mine).

It's actually asymmetric even when the peppers are leaked, since for authentication you only have to check half of the peppers on average before you find the correct one (if the user inputs the correct password, which is most of the time), but to reject a password you have to try all the peppers, which is what you have to do if you don't know the password and are trying to crack it.