Initial consideration on password crypto
Passwords can be stored in three possible ways:
- plain text,
- hash with proper salt and proper stretching,
- anything between the two above.
Plain text is... well, a plain text. Not much to explain here. You need to be sure that nobody can access the storage and its backup. On the other hand, you don’t have to deal with the pesky cryptography and wonder if your password is secure because everything is obvious: the emperor has no clothes. Therefore you know he has to be hidden from public, because he’s naked.
Hashing your password is the opposite. You use it to make sure, that even when the hash is reveled, it’s not obvious for anyone to find out what the password in the plain text is. Please note that I didn’t say just “hashing” but “hashing with proper salt and proper stretching”. There are countless examples on the Internet what these two really mean, but let me just rephrase it for you, so you don’t have to google to much.
Hashing is a process in which you take the input, being the password in our case, and produce an output, know as a hash. It can differ in length and the content, you can imagine it as a short text. For example, if the password is “password” and you hash it with SHA512 what you get is “9151440965cf9c5e07f81eee6241c042a7b78e9bb2dd4f928a8f6da5e369cdffdd2b70c70663ee30d02115731d35f1ece5aad9b362aaa9850efa99e3d197212a”.
The important thing is that your mileage may not vary. The SHA512 function is a standard and no matter where and when you run it, whether it’s called at your smartphone, my GPU or someone’s Bitcoin mine, if it’s written in C++, Java, PHP or Python, it will always produce the same result, “915...212a” for “password”. (The only possible difference might be the upper or lower case, but it doesn’t really matter as they’re equal in this case). And this is a desired behaviour, because I can send you some data (let’s say a file) and the corresponding hash. Then you can run the hashing function and if you get the same hash, we can be pretty sure, that the file wasn’t corrupted.
Producing always the same output makes “plain” hashing functions really good choice for creating checksums and really bad choice for storing passwords at the same time.
If the “password” is always represented by the same hash, always and everywhere, it’s easy for everyone to find out that your password is “password” if they were able to grab the hash to their dirty hands and see that the hash is “915...212a”. Various kinds of attacks can “recover” your password from its hash, like lookup table or reverse lookup table, or rainbow table.
The principle is always the same: if the hash is X, then the plain text password is Y.
In our example: if the hash is “915...212a”, then the password is “password”.
Worse, if the nasty guys got hashes of all your users, they can quickly determine, that not only you, but also Joe and Susan have the same hash, being “915...212a”. I guess you know what their passwords are?
Even worse, if you e.g. decided to have the same password in many environments to be the same for the root user (which is something purely hypothetical, I hope ;-) ), the nasty guy can instantly see that just by looking at the hashes always being the same.
This is where “proper salting” comes into the game…
Salt in password hashing is more or less some random text added to the password. It can be added before the real password or after the real password, it doesn’t matter, as long as it’s always added in the same manner. Of course the salt has to be stored along with the hash, else it wouldn’t be possible to verify users’ passwords when they try to log in. It may look like this:
- for your password the system has created random salt being “ah6e5M?q”, so the input to the hashing function is “ah6e5M?qpassword” and the hash is: “49abb7a38334f0a6acd85b9b0e16aea5b95f56bdf0d16c01032ec55539c42b80cc13a5bb294c169554f167fcc227842abcd1cf9ed6a85201fd852a7d8d5a8e20”
- for Susan’s password the system has created random salt being “42Lg+v#w”, so the input to the hashing function is “42Lg+v#wpassword” and the hash is: 17aac33ceb1eeb88942c998e4846d27c32aaa09d61b3b1ee664fbb855f5b4eb19b81c96168adf3a42c3387444627f9bbd131235b0b563aafc68ae76757ebd09b
As you can see, despite your and Susan’s password are the same, they result in different hashes because the salts are different. This makes easy guessing that you guys have both chosen “password” more difficult. Simple lookup tables won’t help, unless the salts aren’t unique or are small.
However, the hashing functions are fast. And this is very good, if you’d like to calculate a checksum of the heavy video file from the last party we had. After all, you wouldn’t like to wait few hours to verify every downloaded file being a few gigabytes big.
Hashing functions are way too fast when it comes to storing passwords. Say, you have chosen 6 alphanumeric characters for your passwords, that gives about 57 billion combinations in short scale: (26 small letters plus 26 big letters plus 10 digits) to the power of six is about that.
If you can run hashing function one billion times per second, it means that your password will be “recovered” in the worst case in a minute.
And hashing functions aren’t that slow to be honest. You don’t need to use a sophisticated supercomputer to achieve much better results, using multicore CPU or GPU can give really nice speed of “recovering” passwords. Of course, each of them has to be “revealed” separately, because they’re salted, but still...
This is where “proper stretching” comes into the game…
Instead of running hashing function just once, we can run it over, and over, and over. Thousands or millions of times, depending on the function and the environment.
It’s not easy to say “run X times and you’re safe”. It’s better to say “run for as long as you can afford to”. For a website, with many concurrent connections and no fragile content, it might be okay for the hashing to take e.g. 0.05 of a second (50 milliseconds). This will make checking only 1/0.05 = 20 passwords per second.
If the system doesn’t have so many users, if it doesn’t observe peaks in users’ connections or if the data processed by the system is more important, it may take longer, like 0.1 second, 0.5 second or even more, depending how much you can afford to.
Based on that the so called cost parameter should be chosen to allow you verify your legitimate users’ passwords in a reasonable time and also to slow the cracker so much, that they decide to “recover” some other passwords.
There are two more important factors of cost selection. First, different kind of users may have different costs selected, e.g. a board member who has access to some secret data may have higher cost than an “ordinary” employee. Second, the cost should constantly increase in time, as computers are faster and faster every year. (Or roughly speaking 2 times faster every 18 months if we paraphrase Moore’s Law.)
So the passwords should be hashed with proper salt and proper stretching.
There are many password hashing functions which can make that for you if you use them properly. There are crypt, PBKDF2, bcrypt, scrypt, argon2 and more. There are constant flame wars on the Internet which is the best and which should “rule them all”. Their usage may depend on the context, but if they’re used properly, they can all perform a decent job. The important thing is to not re-invent the wheel, do some fancy looking security by obscurity, but correctly use the well known and widely tested password hashing functions.
Let’s take a look at the password hashes stored in the OPNSense®, both for web usage in /conf/config.xml file, both for system passwords in /etc/master.password.
If you have installed OPSense® 16.7 like I did, you can find in the mentioned files the following hash for the root user:
$6$$Y8Et6wWDdXO2tJZRabvSfQvG2Lc8bAS6D9COIsMXEJ2KjA27wqDuAyd/CdazBQc3H3xQX.JXMKxJeRz2OqTkl.
I know a little about password hashes so I can tell you that if you see the hash beginning with $6$ it means that the crypt function was used and the hashing algorithm is SHA512. You can check it here: https://en.wikipedia.org/wiki/Crypt_(C)#SHA2-based_scheme
If you scroll a bit higher to https://en.wikipedia.org/wiki/Crypt_(C)#Key_derivation_functions_supported_by_crypt you can see the exact format of the hash, being:
$<id>[$<param>=<value>(,<param>=<value>)*][$<salt>[$<hash>]]
Our hash looks like that:
$6$$Y8Et6wWDdXO2tJZRabvSfQvG2Lc8bAS...qTkl.
This means two things:
- if there are no parameters, especially rounds=X, then the defaults are used and rounds=5000,
- there is no salt, because $6$salt_should_be_here$
Let’s think about it for a moment. If the cost parameter is 5000, this (in case of crypt/SHA512) means, that the password hashes will be calculated 5000 times slower. But if your PC can calculate billions of hashes every second, it means that billions/5000 will be the the actual speed.
And since there’s no salt used, you can calculate the hashes for every password or every user at every device running OPNSense®.
This means not only that you can calculate the hashes of most popular passwords really fast, but you can do it only once and forever and easily create a lookup table.
There are at least a few ways of improving the password hashing.
We could add a nice salt and a cost parameter to SHA512 and stick to $6$ marker. The salt generating function could look like this (for PHP <7):
function generate_sha512_password_salt($rounds = 5000) { $salt = base64_encode(openssl_random_pseudo_bytes(12, $strong)); $salt = str_replace("+", ".", $salt); if ($rounds >= 1000 && rounds <= 999999999 && $rounds != 5000) { //add rounds if provided parameter in range and not default $salt = "rounds=$rounds\$$salt\$"; } $salt = "\$6\$$salt"; return $salt; }
The question is: how many rounds should be chosen? There is no simple and valid-for-everyone answer, like ‘Use the X rounds and you gonna fine’. As in many cases, the question alone is not enough to formulate an answer, a context in which this question is asked is also needed.
Normally the question should be: ‘What is the maximum time you can spend in your environment on password hashing?’ This requires predicting overall system performance, possible peaks in multiple-user systems, benchmarking for maximum cost which you (your system) can afford, etc.
I decided to check how many rounds are needed for my APU1D to make the process taking about 0.1 second. Turns out it’s about 30 000.
However, the password stretching is not to annoy your users with log-in taking a bit longer, but to make the “password recovery” taking much longer for the hacker in the first place.
Turns out that running the very same dummy benchmark at my PC produces about 193 000 hashes every 100 milliseconds. This is ca. 6.5 times more.
Therefore I decided to try with Bcrypt, which starts with $2b$. The good news is that there’s a nice function in PHP, password_hash, which employs Bcrypt under the hood and creates a salt on its own. (The bad is that we need change $2y$ marker produced by PHP to $2b$, since 2y isn’t recognised by FreeBSD yet.)
The Bcrypt has a different range and scale of cost than crypt/SHA512, so for my appliance and PC I got 10 and 11 respectively. TL;DR: hashing at my PC/CPU is ~6 times faster for crypt/SHA512 and ~2 times faster than my APU1D. This is not a surprise knowing the algorithms and the hardware.
The following function can be used in all places where the passwords are hashed
function generate_password_hash($password, $cost = 10) { $hash = password_hash($password, PASSWORD_BCRYPT, ["cost" => $cost]); // at the moment of writing FreeBSD can't recognise $2y$... as bcrypt, $2b$ is needed $hash[2] = 'b'; return $hash; }
And all places in which the password is checked can employ password_verify. This also ensures backward compatibility with crypt using $6$ and passwords stored previously.
Is 100 milliseconds too fast or too slow may depend on your needs. For a webpage I’d probably choose less, for some highly critical systems I’d choose more, and then adjust the cost.
So what should you do after the patch is applied? Set a new password for every user, it will calculate the hashes again.
In case of a custom system, I’d recommend bcrypting current hashes, so we have something like bcrypt(crypt(password, ‘$6$’)) and adjust the system to take that into account when authenticating users. But in case of FreeBSD this seems to be an overkill and would require changes to the underlying OS, since this isn’t supported by FreeBSD and would cause problems with password-based authenticating in SSH or serial terminal.
Lets see how the config.xml differs with this patch applied and after password change. The password has been reset in the terminal menu and the diff is provided by DynFi®.
Let’s change it once more, this time in WWW, from the default ‘opnsense’ to the same ‘opnsense’ and see what changes are reported by DynFi®.
As you can see, despite the password being the same, it results in a different hash after each change. And checking this password at my testing device takes ca. 0.12 second.
This is not the end. API passwords might be converted to bcrypt too. Variable cost per user (or group) might be introduced, along with some benchmarking.
Last but not least: just because the passwords aren’t hashed with proper salt and proper stretching, doesn’t mean that OPNSense® isn’t performing well as a firewall. The mechanism described and corrected above wakens your passwords once their hashes might have been accessed or seen by some hackers. So if you keep your config.xml backups in a safe way, there’s no need to panic.
Hopefully the next release of OPNSense® won't use unsalted password hashes.
Piotr Przybył
Attachment | Size |
---|---|
OPNSense password hash patch | 2.27 KB |
Leave a comment.