Secure your PHP passwords

Hii guys

Despite my website being passwordless for a while now, passwords are still used a lot around the web.
I see a lot of PHP developers that implement passwords slightly wrong, or some do it just completely wrong.
This is not an attack on developers that do this (okay, it kinda is?) but more to educate them on the subject on how to securely implement passwords using PHP.
First, we'll go into how definitely not to do it, then some "okay-ish" ways followed by the "right" way.
Please do note that this post does not go into details on how you store them (eg. in your database), that is up to you (though if demand is there, I can make a post about that later?).

Without any further ado, let's dive into it!

How to definitely not to do it: Plaintext


Okay, so let's start with a way on how you should definitely not do it.
This is often what is being taught at schools at first, however, I feel like it should be learned to people the right way immediately so they don't get the chance to even develop a bad habit: Plaintext.

Yes, this occurs far, far too often, and I even see it on some business sites as well...
Facebook was one of them...
Plaintext is literally not even bothering with securing passwords.
If the user has the password "lamepassword", then that is what you store.
It is easy but definitely not the way to go, imagine if you leak your database, someone that would get their hands on it would be able to literally read them as-is.

It would look something like this:

// Take the password from the POST request
$password = $_POST['password'];
var_dump($password); // string(12) "lamepassword"

// ... Store the password to database  


Then in order to verify the password, you just have to do this:
// Take the password from the database
$expectedPassword = get_password_from_db();
var_dump($expectedPassword); // string(12) "lamepassword"

// Take the password from the request
$inputPassword = $_POST['password'];
var_dump($inputPassword); // string(12) "lamepassword"

// Verify the password against POST input
if($expectedPassword === $inputPassword) {
  echo "Password is correct";
} else {
  echo "Password is incorrect";
}  


It's very simple but if you do this, well... you should honestly be ashamed of yourself and may want to do a very thorough audit of your code since I doubt there aren't more vulnerabilities in there.

Still not okay-ish: Encryption


This is often the next step students tent to learn for securing passwords: encryption.
With encryption, you can reverse the data given the right key.
Encryption is fine when you want your data to be readable again by the recipient but is not desired when dealing with passwords.
It would look something like this (actual result may vary since the "$iv" is always random here):
// Take the password from the POST request
$password = $_POST['password'];
var_dump($password); // string(12) "lamepassword"

// Set out encryption key
$key = 'lamekey';

// Prepare for encryption
$cipher = "AES-128-CTR";
$ivLength = openssl_cipher_iv_length($cipher);
$iv = openssl_random_pseudo_bytes($ivLength);
$options = 0;

// Encrypt the password
$encrypted = openssl_encrypt($password, $cipher, $key, $options, $iv);

// Create object with some additional data needed for decryption
// Then encode it with JSON
$data = json_encode([
  'cipher' => $cipher,
  'iv' => base64_encode($iv),
  'encrypted' => base64_encode($encrypted),
]);
var_dump($data); // string(95) "{"cipher":"AES-128-CTR","iv":"3TxgnITMCaD+Y9weUbbs1w==","encrypted":"SFpuenl4Z0JjTGh5Y0lHSQ=="}"

// ... Store data object to database  


Then, to verify the password you'd do the following:
// Get our data from the database
$data = get_data_from_database();
var_dump($data); // string(95) "{"cipher":"AES-128-CTR","iv":"3TxgnITMCaD+Y9weUbbs1w==","encrypted":"SFpuenl4Z0JjTGh5Y0lHSQ=="}"

// Decode our data
$data = json_decode($data, true);

// Set our encryption key
$key = 'lamekey';

// Decrypt our password
$expectedPassword = openssl_decrypt(base64_decode($data['encrypted']), $data['cipher'], $key, 0, base64_decode($data['iv']));
var_dump(expectedPassword); // string(12) "lamepassword"

// Verify the password against POST input
if($expectedPassword === $_POST['password']) {
  echo "Password is correct";
} else {
  echo "Password is incorrect";
}  

This way is at least something, however, it's still far from ideal since all that needs to be leaked is the object containing the encrypted data and the keys used to encrypt them (which most of the time are stored somewhere on the server) and all passwords will be decryptable by an attacker.

Still not okay-ish: Simple Hashing


Hashing means running data through an algorithm that "digests" the data and returns something else.
Hashing differs from encryption in the way that hashes are a one-way algorithm, so it can't be reversed.
Implementing hashing could look something like this:
// Get the password from the POST request
$password = $_POST['password'];
var_dump($password); // string(12) "lamepassword"

// Hash our password
$hash = hash('sha256', $password);
var_dump($hash); // string(64) "5e9dd8b43e30b80cb55cc34e774d42dc56849274cf60a9a35a4d4ba271efab74"

// ... Store our hash to the database  


Then to verify the password, this is how it could look like:
// Get the password from the POST request
$password = $_POST['password'];
var_dump($password); // string(12) "lamepassword"

// Get the hash from our database
$hash = get_hash_from_database();
var_dump($hash); // string(64) "5e9dd8b43e30b80cb55cc34e774d42dc56849274cf60a9a35a4d4ba271efab74"

// Hash our password
$passwordHash = hash('sha256', $password);
var_dump($passwordHash); // string(64) "5e9dd8b43e30b80cb55cc34e774d42dc56849274cf60a9a35a4d4ba271efab74"

// Check our passwordHash to our database hash
if($hash === $passwordHash) {
  echo "Password is correct";
} else {
  echo "Password is incorrect";
}  


A secure hashing algorithm should tick the following boxes:
- They are irreversible (only a "brute-force attack" can "reverse" it).
- A single bit change should cause a significant change in the hash.
- Should be deterministic (Identical inputs should return the same output).
- Should be fast enough to be used in practice.
- Are resistant to collisions.

What is a collision you may ask? Great question!
Collisions happen when two different inputs give the same output hash.
MD5 is a well-known algorithm that is deemed "insecure" due to the practical possibility of a hash collision.
Let's take the following two hexadecimal strings, turn them into bytes and run them through both MD5 and SHA256 (which a secure hashing algorithm, atleast, at the time of writing this):

$input = '4dc968ff0ee35c209572d4777b721587d36fa7b21bdc56b74a3dc0783e7b9518afbfa202a8284bf36e8e4b55b35f427593d849676da0d1d55d8360fb5f07fea2';
var_dump(hash('md5', hex2bin($input))); // string(32) "008ee33a9d58b51cfeb425b0959121c9"
var_dump(hash('sha256', hex2bin($input))); // string(64) "90774a6455a2bdb7d106e533923ecbefe81392ca55bed0ce81cfab2c1a7f0afe"  

$input = '4dc968ff0ee35c209572d4777b721587d36fa7b21bdc56b74a3dc0783e7b9518afbfa200a8284bf36e8e4b55b35f427593d849676da0d1555d8360fb5f07fea2';
var_dump(hash('md5', hex2bin($input))); // string(32) "008ee33a9d58b51cfeb425b0959121c9"
var_dump(hash('sha256', hex2bin($input))); // string(64) "54bcb9a4fda31e4f254303e3959acd5e420ad18a80949d56a3000c3716fbd1a0"  

Both return the same hash, which goes against our definitions of a strong hashing algorithm.
But when we run those same strings through SHA256, we can see the strings definitely aren't the same and return a different hash.

Of course, having a collision isn't secure because now, there are multiple values to the same hash, which means that if the actual password is "lamepassword" but the attacker enters "Iaml33th@x0r" and there is a collision, well... your actual password doesn't matter much.
As computing power increases and other vulnerabilities may be found, the recommendations on what is and what isn't a secure hashing algorithm may change after writing this post.
Using simple hashes is far better than leaving them in plaintext and still significantly better than just encrypting them, there are some more flaws with this approach, to which I'll get back to in a moment.

Pretty okay: Salting your hash


Alright, now that we know that hashing is a good way to start, we should discuss the major issue when using a simple hashed password: Rainbow Tables.
What is a Rainbow Table? That is actually a simple answer.
A Rainbow Table is a database of pre-computed hashes and their plaintext equivalent.
They aim to make "cracking" a password a lot easier by just looking up whether we already know the hash.

Cracking hashes from a secure hashing algorithm (like SHA256) can only be done through a brute-force attack.
This means going over every combination possible until you find a matching hash.
Doing this, however, is very computationally intensive and becomes exponentially more expensive the longer a password is, which is undesirable when you want to crack a list of hashes.
This is where Rainbow Tables come in.

By pre-computing all hashes, we can make it so that we have to do the intensive part only once and then look into our tables quickly to see if we have the plaintext value.
Using a Rainbow Table could look something like this:

// Build our Rainbow Table
$table = [
  '5e9dd8b43e30b80cb55cc34e774d42dc56849274cf60a9a35a4d4ba271efab74' => 'lamepassword',
  'ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f' => 'password123',
  'ad62252f71a484702b74f379ea3c3388ac8c147ef65a7d4f50bfa06d6d9aaf8b' => 'awesomepassword',
  '83ad936cde699a458d08b6d6afbbfacaa4e2a307d654282ce30a462adbaf802e' => 'finlaydag33kisawesome',
];

function lookupHash(string $hash, array $table) {
  // Check if our hash exists
  // If so, return the plaintext
  // Else, return null
  if(array_key_exists($hash, $table)) return $table[$hash];
  return null;
}

var_dump(lookupHash('5e9dd8b43e30b80cb55cc34e774d42dc56849274cf60a9a35a4d4ba271efab74', $table)); // string(12) "lamepassword"
var_dump(lookupHash('0fd0300b0aa2ee9684a0583f79933a41cc2976e56501b31a6a1d3e71794c3db2', $table)); // NULL  


Using Rainbow Tables, however, has a downside: Rainbow Tables can get really big really easily (easily over a few hundred GB!) and get exponentially bigger based on the character space you need to cover as well as the length of the password.
So there is a big trade-off to make:
- Save a lot of computational power but use a lot of storage.
- Save a lot of storage but use a lot of computational power.

In order to offset this, most rainbow tables only contain the most common passwords.

Additionally, using special characters not found on standard keyboards can completely nullify the effectiveness of most Rainbow Tables.
However, doing this would annoy most users, imagine having these password requirements shoved onto your user:
- must use at least 8 characters.
- must use at least 1 digit.
- must use at least 1 lowercase character.
- must use at least 1 uppercase character.
- must use at least 1 special character.
- must use at least 1 character of the Cyrillic alphabet.
- must use at least 1 character of the Hiragana alphabet.
- must use at least 1 character of the Greek alphabet.
- must use at least 1 sequence consisting of a 16-digit long prime number found in Pi.

Yea... that's not gonna fare too well now isn't it?...
Additionally, humans are a notoriously ineffective way of generating random passwords.
So, instead, you, the developer should protect your passwords from Rainbow Tables!
How to do this you ask?
Well, we have just learned that Rainbow Tables are just glorified lookup tables consisting of pre-computed hashes along with their plaintext value...
What if, we were to add a bit of organized chaos into the batter!
That's right, we're gonna need to add some salt and pepper!

No, I'm not talking about the spices.
Salting is done to add a bit of randomness to the passwords.
Using salts don't completely defeat a Rainbow Table, however, it significantly reduces the likelihood of the table containing the required hash and this increases exponentially the bigger the salt is.

Salts are generally pseudo-random on a per-hash basis, meaning that the salt should change each time a new hash is created.
As we've learned previously, when we change even a single bit, the hash completely changes.
Implementing a salt could look something like this:

function saltPasswordHash(string $password) {
  // Generate 8 pseudo-random bytes
  // Then turn them into hexadecimal resulting in 16 characters
  $salt = bin2hex(random_bytes(8));

  // Hash and return the values
  return [
    'hash' => hash('sha256', $salt . $password),
    'plaintext' => [
      'salt' => $salt,
      'password' => $password,
      'complete' => $salt . $password,
    ],
  ];
}

var_dump(json_encode(saltPasswordHash('lamepassword'))); // string(183) "{"hash":"d09964fc9c28cbb7a1b6b090b2ca4db043bf95f0990c1bdac9768f095d6c42cd","plaintext":{"salt":"cbcad7187e7019fa","password":"lamepassword","complete":"cbcad7187e7019falamepassword"}}"
var_dump(json_encode(saltPasswordHash('lamepassword'))); // string(183) "{"hash":"13b1f66bcc5689add212f662272b947531c7779d6444e31ef8fe341bfe1a2a3c","plaintext":{"salt":"e6a4ff3aae8d425d","password":"lamepassword","complete":"e6a4ff3aae8d425dlamepassword"}}"  


As you can see, while both of the input passwords are the same, they result in completely different hashes.
This is because, before we run it through the hashing algorithm, we add 16 pseudo-random characters in front (each byte is 2 characters in hexadecimal) of the password.
Doing this severely decreases the likelihood of a Rainbow Table containing our password's hash (seriously, what Rainbow Table would have "cbcad7187e7019falamepassword" or "e6a4ff3aae8d425dlamepassword" in it?... Don't you dare, person who reads this!).

Okay, now there's one problem... How do we verify the password?
Well, the problem is that now when the user tries to enter his/her password, it won't match the hash anymore as-is since we miss the salt, and we can't just obtain it from the hash itself since it's reversible...
We could save it in the database as well, but that would mean we need additional database lookups...
Oh, I know it...
Why not store it alongside the hash itself?

function saltPasswordHash(string $password) {
  $salt = bin2hex(random_bytes(8));
  return $salt . "." . hash('sha256', $salt . $password);
}

var_dump(saltPasswordHash('lamepassword')); // string(81) "b009f2f2565b571c.fc7ddc71fce52b7e45100b37ac7069562c1747137c2faed9ac3148bb8651b8c1"  

Now we can quite easily just verify the hash by obtaining the salt, using it to hash our password then compare the hashes:

function verifySaltedPasswordHash(string $password, string $salted) {
  list($salt, $hash) = explode(".", $salted);
  return hash('sha256', $salt . $password) === $hash;
}

var_dump(verifySaltedPasswordHash('lamepassword', 'b009f2f2565b571c.fc7ddc71fce52b7e45100b37ac7069562c1747137c2faed9ac3148bb8651b8c1')); // bool(true)  

There is one final step we can do to basically obliterate the effectiveness of Rainbow Tables: use multiple rounds!
Yes, by re-hashing our hash (along with our salt) a few times, we can basically obliterate the effectiveness of Rainbow Tables completely!
This is quite trivial actually by only slightly modifying our code:

function saltPasswordHash(string $password, int $rounds = 8) {
    $salt = bin2hex(random_bytes(8));

    $hash = $salt . $password;
    for($round = 0; $round < $rounds; $round++) {
      $hash = hash('sha256', $salt . $password);
    }
    return $salt . "." . $rounds . "." . $hash;
}

var_dump(saltPasswordHash('lamepassword', 8)); // string(83) "e3e3d8f60000e083.8.ba23f2791b51f13c44fd5de59ca72bf6c46804d08d63319dd7ed976d58aa98a8"  

This piece of code does the following:
- Take our password.
- Generate a pseudo-random salt.
- Hash our password with the salt N-times (in this case, 8)
- Return a string containing our salt, the amount of rounds used and our final hash.

Now, verification can be done as follows:

function verifySaltedPasswordHash(string $password, string $salted) {
  // Obtain the required values from our hashstring
  list($salt, $rounds, $hash) = explode(".", $salted);

  // Hash our input password for N times
  $inputHash = $salt . $password;
  for($round = 0; $round < $rounds; $round++) {
    $inputHash = hash('sha256', $salt . $password);
  }

  // Check whether the inputHash equals our expected hash
  return $inputHash === $hash;
}

var_dump(verifySaltedPasswordHash('lamepassword', 'e3e3d8f60000e083.8.ba23f2791b51f13c44fd5de59ca72bf6c46804d08d63319dd7ed976d58aa98a8')); // bool(true)  

Now we can both verify our password and we've obliterated the effectiveness of Rainbow Tables!

Okay, now the next problem...
Of course, we would need to also add the algorithm used during hashing and run the required algorithm during verification in case our hashing algorithm ever needs to change (eg. because SHA256 becomes deemed to be insecure), but you can probably figure that out...
No wait, there is an even better way of doing it!

The right way: password_hash() and password_verify()


Okay, so we've just found out ways to implement our own hashing and salting, but what if I told you, other people already did the grunt work for us?
That's right, PHP has 2 easy to use functions to do all that what we just did, but turned it into something that is far easier to integrate.
Let's have a look at how to hash a salted password using password_hash().
$hash = password_hash('lamepassword', PASSWORD_DEFAULT);
var_dump($hash); // string(60) "$2y$10$RqHj/cNwxD15.Ofu2Pmq/..v55E1Xr27SdhcnC16vJv.JhkhI99by"  

Well, that was pretty anti-climactic, surely verification is a lot more amazing right!?

var_dump(password_verify('lamepassword', '$2y$10$RqHj/cNwxD15.Ofu2Pmq/..v55E1Xr27SdhcnC16vJv.JhkhI99by')); // bool(true)  

Oh come on... that one is even more anti-climactic...
And here I was trying to build some HYPE.

So what happens here, well, it's quite simple actually...
The code that we just wrote earlier that does all the salting, hashing for N-rounds and returning all data we need to verify the password, that is taken care of by password_hash() for us.
Breaking a password_hash() hash down (left to right), we get the following:
- $2y: Specifies our algorithm (in this case, "BCrypt").
- $10: Specifies the "cost" to create the hash (similar to our "rounds" we've used previously calculated as 2^N, so 1024 rounds in this case).
- $: Just a delimiter, doesn't do anything special.
- RqHj/cNwxD15.Ofu2Pmq/.: Our salt (in BCrypt, the first 22-characters after the previous "$").
- .v55E1Xr27SdhcnC16vJv.JhkhI99by: Make a guess? (spoiler alert: it's our hash).

Likewise, the code that we wrote earlier to get all the variables we need, run the hashing again and compare the hash, that is taken care of by password_verify() for us.
What is left for us is some clean and easy to read code, that doesn't leave too much room for error.
Additionally, because we used the "PASSWORD_DEFAULT" flag, we don't have to manually update our code to keep up with changes in the cryptography landscape.
When an algorithm is deprecated because it is deemed to be insecure, the PHP developers most likely will update PHP to change to a more secure algorithm for you.

Okay, so now what happens when an algorithm gets updated?
Quite simple really, we have a function for that.
Let's see the steps we need to take for it:
- First you need to verify the password (to make sure it was correct to begin with, you'll see why in a second).
- Second, you need to check whether you need to re-hash the password using password_needs_rehash().
- Next, run our password through the password_hash() function again if need be and update it in our database.

That would look something like this:

// Get our BCrypt hash
$hash = '$2y$10$RqHj/cNwxD15.Ofu2Pmq/..v55E1Xr27SdhcnC16vJv.JhkhI99by';

// Check if our password is correct
// Throw an exception if not
// Just act as if this is on a login page already
if(!password_verify($password, $hash)) throw new \Exception('Password is incorrect');

// "Oh no, BCrypt is insecure now, let's swap to Argon2i"
$algorithm = PASSWORD_ARGON2I;

// Check if our password needs re-hashing
// If so, re-hash it and store it to the database
if(password_needs_rehash($hash, $algorithm)) {
  $newHash = password_hash($password, $algorithm);

  // ... Store our new hash to database
}

// ... Continue on as always  

Obviously, instead of hardcoding "PASSWORD_BCRYPT" or "PASSWORD_ARGON2I", you'd add "PASSWORD_DEFAULT" instead but you get my point.
Now with this code, all we ever need to do is keep our PHP version up-to-date (which you should do anyway) and when a user now logs in, our server automagically checks whether a password needs re-hashing and does so if need be, saving us a lot of hassle!

BONUS: password_hash() vs sodium_crypto_pwhash_str()


After publishing this post Martijn (@sexybiggetje@mastodon.social) asked me to cover sodium_crypto_pwhash() and see how it compares to password_hash().

I honestly didn't know sodium_crypto_pwhash() at first, so it sounded like a fun thing to check out and compare the two.

So I opened the documentation and instantly noticed that this was gonna be a ride since it quite literally states I should just use password_hash() unless I have a specific reason to...
Interesting...

I had heard of Libsodium before, but never gotten to play with it either.
When I opened the documentation, I found this:


Hang on, let me read that again...

The function deriving a key from a password and a salt is CPU intensive and intentionally requires a fair amount of memory. Therefore, it mitigates brute-force attacks by requiring a significant effort to verify each password.


It could just be me, but that doesn't sound too promising?
I mean, sure, if you want a very high security, like to pay high electricity bills and want to use your servers as heat-source for a sauna then this looks interesting...
Scrolling a little bit lower, it mentions server relief by using a piece of Javascript that allows the client to do some pre-hashing, but that sounds kinda odd to have your visitor do a lot of the lifting as well...

So then, let's run some synthetic benchmarks to see whether it's all just poor wording, or that this is indeed as resource intensive as they say.
Please do note that a synthetic benchmark won't always show realistic results, however, they can be used just fine for demonstration purposes like these.
To do this, I have written a few pieces of code that will help me in doing so after enabling the sodium extension in my php.ini.
However, instead of using sodium_crypto_pwhash(), I'll instead be testing sodium_crypto_pwhash_str(), which is more close to how you'd use password_hash().
Now, obviously, before we start, I need to declare what the relevant specs of the machine running it are.
In this case:
- CPU: Ryzen 5 3600 running at 4.6GHz all-core.
- RAM: 64GB of G.Skill TridentZ Neo running at 3600MT/s CL16.
- OS: Windows 10 Pro 20H2 build 19042.746.
- PHP: PHP 7.4.11 (cli) (built: Sep 29 2020 13:17:42) ( NTS Visual C++ 2017 x64 ).
During all benchmarks, I left my PC untouched.
Unfortunately, I wasn't able to test memory due to the way the benchmark scripts work.

First, let's establish a baseline using password_hash() first, for that, I've used the following code.
The benchmark will be hashing the password "lamepassword" 1 thousand times.

$hashes = [];
$start = new DateTime();
for($i = 0; $i < 1_000; $i++) {
  $hashes[] = password_hash('lamepassword', PASSWORD_DEFAULT);
}
$end = new DateTime();

file_put_contents('password-hash.json', json_encode($hashes));
echo "Finished running benchmark in: " . $start->diff($end)->s . "seconds";  


I then ran it with the following command:
php -d memory_limit=1G password-hash.php  

It took the script 49 seconds to hash all these passwords.

Now let's see about sodium_crypto_pwhash_str(), for this, I used the following script:

$hashes = [];
$start = new DateTime();
for($i = 0; $i < 1_000; $i++) {
  $hashes[] = sodium_crypto_pwhash_str(
    'lamepassword',
    SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
    SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
  );
}
$end = new DateTime();

file_put_contents('sodium-hash.json', json_encode($hashes));
echo "Finished running benchmark in: " . $start->diff($end)->s . "seconds";  


Again, running it with this command:
php -d memory_limit=1G sodium-hash.php  

This finished in 54 seconds, which is significantly better than I expected considering what we've seen above.
Next, let's see how fast we can verify the hashes.
For this, I've written these 2 pieces of code:

$hashes = json_decode(file_get_contents('password-hash.json'));
$start = new DateTime();
foreach($hashes as $hash) {
  password_verify('lamepassword', $hash);
}
$end = new DateTime();

echo "Finished running benchmark in: " . $start->diff($end)->s . "seconds";  


$hashes = json_decode(file_get_contents('sodium-hash.json'));
$start = new DateTime();
foreach($hashes as $hash) {
  sodium_crypto_pwhash_str_verify($hash, 'lamepassword');
}
$end = new DateTime();

echo "Finished running benchmark in: " . $start->diff($end)->s . "seconds";  


Running these scripts gave me 48seconds for password_verify() and 52seconds for sodium_crypto_pwhash_str_verify().
That is certainly interesting...
Then what could be the difference between using password_hash() and sodium_crypto_pwhash_str(), especially since both of them can use Argon2 (which is kinda the new hype).
Frankly enough, I have no clue.
The only real difference I can imagine is for "legacy-purposes".
password_hash() has been around since PHP 5.5.0 and is generally the standard function that developers will implement (if they know what they are doing), so people probably just stuck with it.
Libsodium is only supported since PHP 7.2.0, which is still fairly new (relatively speaking), so people may not be aware of it just yet.
Additionally, it appears then when using a BCrypt hash generated by password_hash() can't be used with sodium_crypto_pwhash_str_verify(), for that, you'd need to first migrate all the hashes to Argon2, which may not always be desired, but on the flip side, you can use a hash generated by sodium_crypto_pwhash_str() with password_verify() just fine.

So should you use sodium_crypto_pwhash_str() over password_hash()?
No, you may choose so if you build a new application, however, that is up to you to decide.
Personally, I'd just stick with the regular password_hash() and use the PASSWORD_DEFAULT at all times.
Libsodium is fun to play with since it can do so much more than just hash passwords, however, it doesn't seem like it's that important at the moment unless you want top-notch security as opposed to "more than enough" in your new application.

Conclusion


We have learned a lot about protecting your passwords when using PHP, which should help you protect your users in the long run, sadly, it doesn't protect people from making silly insecure passwords or re-using the same password everywhere but at least, when your website gets hacked, you severely diminish the damage that can be done to the user, given you don't store pointless information that you no longer need.
There are way too many incidents where plain-text passwords or poorly hashed passwords are being leaked to the public, which is why I can't stress enough to use a unique password for every service.
Using a password manager like Bitwarden, you can have them manage your massive list of passwords (you can even self-host your own Bitwarden server if you'd fancy instead, just make sure you know what you're doing).

Additionally, if you'd like the challenge, you can go passwordless, like I have, to not have to bother with passwords again (although this does come at some minor risks as well if you implement it poorly), that'll teach those pesky password leaks!
I personally think passwords are a relic of the past and that we should really consider using passwordless methods going forward but at least, if you choose to still use passwords, you now know how to do so securely enough.

Anyways, that is it for this one, I hope you guys enjoyed it and learned a thing or two.
As always, feel free to join me on my subreddit or my Discord server to join the conversation or to leave a suggestion for the next topic I should cover.

Cheers.

Comments


Leave a comment


Please login to leave comment!