DEV Community

dewbiez
dewbiez

Posted on • Updated on

PHP Security: Passwords

My Password Rules

  • Minimum length of 16 characters.
  • Maximum length of 256, 512, 1,024 or 2,048 characters.
  • Make sure the password isn't pwned.
  • ALLOW PASTING OF PASSWORDS!

Microsoft has a maximum password length of 16 characters.

Does that sound good to you? Well, it certainly shouldn't. It's not often applications have such low maximum lengths for passwords(correct me if I'm wrong). But it's really bad to limit your password lengths like Microsoft did.

It's debatable whether you should enforce password rules such as special characters, uppercase letter, lowercase letter, etc. I don't believe that's the better thing to do. I strongly disagree with password rules like that.

All the application should do is make sure my secret is long enough, and hash it slowly so my secret won't be found out within anyone's lifetime.

A lot of sites have a minimum password length of 6 or 8 characters. That's not good. It really isn't. Even if they're using Argon2 with a high cost. 12 characters is okay ... but I like to set the minimum to around 16 characters long.

Why allow such a big maximum number of characters? It's ridiculous!

You're right. It probably is. But I like to make sure password managers are able to input super long passwords.

Storing Passwords

I'll show you how easy it is to securely hash passwords in PHP >= 5.6. It's automatically salted.

$hashed_password = password_hash('string', PASSWORD_DEFAULT);
Enter fullscreen mode Exit fullscreen mode

The default password hashing algorithm as of PHP 7.2 and lower is bcrypt. But note, while bcrypt is good, it truncates passwords at 72 characters, and is vulnerable to null bytes.

My preferred way of storing passwords will probably differ from yours.

My Technique

  • HMAC the password.
  • Hash the HMAC.
  • Encrypt the hash.

Why the heck are you HMACing the password?!

Well, that's because when say someone is registering to your site. What if they put in a 4 megabyte password, and send multiple requests? This could effectively DDoS you. However, you can prevent that, by pre-hashing with a fast algorithm such as SHA256. Or you can validate the password and make sure it doesn't go past a maximum number of characters. And if it passes the validation, then you can hash the password, etc.

But that's not the only problem it can solve. What if you're storing PINs? Only 4 digit numbers, can be brute-forced within a reasonable amount of time. Think about it, you're only hashing 4 characters. You can turn 4 characters, into a really long string with an HMAC. And there you go, it's better than just storing 4 characters.

How does it DDoS you?

It requires a lot of computing power for say Argon2. Now remember, password hashing algorithms are meant to be slow. So if it takes 1 second to hash a 8 character password, it might take 10 seconds to hash a 1 megabyte password. Which is why we shorten or lengthen a password to a set length so we don't have that issue.

But mixing up different algorithms is dangerous!

It can be, but I highly doubt hashing a SHA3-512 hexadecimal output will do anything bad in a Argon2 hash function, because it's literally just numbers and letters. Isn't that what a password usually is?

Okay, but why are you encrypting the hash?

It's not a performance issue if you're using fast encryption algorithms.

Still! Why? It adds no benefits, and makes your application harder to maintain.

That's actually a good argument. However, say your database server is separate from your application server. If an attacker only gets access, to the database, they first have to find the encryption key to decrypt the hash, or eventually break it, only to have to stop dead in their tracks with a really slow hashed password. But in the real world, I doubt someone would try to brute-force AES encryption, they'd probably start looking for the key. But doing this can buy you time, possibly enough time to rotate your keys and tell users their data has been compromised.

The Code

I can also show you a quick basic implementation in PHP.

First, we of course need a password.

$password = 'my super secret password';
Enter fullscreen mode Exit fullscreen mode

Now that's done, we can setup the HMAC's algorithm and key.

$hmac_algorithm = 'sha3-512';
$hmac_key       = random_bytes(32);
Enter fullscreen mode Exit fullscreen mode

Step 1: Make the HMAC.

$hmac = hash_hmac($hmac_algorithm, $password, $hmac_key);
Enter fullscreen mode Exit fullscreen mode

Ta da! Now that's done, let's move on to setting the password hashing up.

$password_hash_cost      = 4;
$password_hash_algorithm = PASSWORD_ARGON2I;
$password_hash_options   = [
    'memory_cost' => $password_hash_cost * PASSWORD_ARGON2_DEFAULT_MEMORY_COST,
    'time_cost'   => $password_hash_cost * PASSWORD_ARGON2_DEFAULT_TIME_COST,
    'threads'     => $password_hash_cost * PASSWORD_ARGON2_DEFAULT_THREADS
];
Enter fullscreen mode Exit fullscreen mode

Step 2: Make the hash.

$hash = password_hash(
    $hmac,
    $password_hash_algorithm,
    $password_hash_options
);
Enter fullscreen mode Exit fullscreen mode

Yep! That's all done. We need to setup that next step though.

$encryption_key   = random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
$encryption_nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
Enter fullscreen mode Exit fullscreen mode

Step 3: Make the ciphertext.

$ciphertext = sodium_crypto_secretbox(
    $hash,
    $encryption_nonce,
    $encryption_key
);
Enter fullscreen mode Exit fullscreen mode

I ran the above code, and these were the following outputs:

HMAC output: 876dd083d78abd57c267a4cb3b64788c468f7ff9a88ab91800e5ae3cc3e25f646510fc2e2a9ccd9395ba01b814dbe76efa2acb985a7733330f4abc6b5157474c

Hash output: $argon2i$v=19$m=4096,t=8,p=8$ZEJXdlN2QU8vb3RIU3RxeA$0hU3ZokcZpPJwfmdmzwXD5KFfdmh/MyZRAFx4tLIJkc

Ciphertext output: 319dfbac430ad505b7daccfb8c827f36389b1dc747d3b3fc6cd1334e060b156800dec5268c79fe46367ad1be3ddac70540ddc19a6fb70348018a5ed27bcd2fb822a83e289833ec6d3294881eed6e45b94fa8c5a6f502ddb0851956587d6a2817bc45f82251b7e633de9cc3c2779e9b
Enter fullscreen mode Exit fullscreen mode

Note, I converted the ciphertext to hexadecimal. And by default the HMAC output is converted to hexadecimal. I was using PHP 7.2.9, and I was using libsodium for encryption.

I am by no means an expert in cryptography, or it's implementation. And this was my opinion on passwords, yours will probably differ.

That'll be it for today, I hope you enjoyed the article, and that perhaps you opened your mind a little more about passwords.

EDIT, PLEASE READ:

Don't use my technique. Read the comments. Just use password hashing algorithms such as Argon2 or Bcrypt to store your passwords.

Like I said before. Encrypting the password hashes will only be effective if you keep the application and database on separate servers/hardware, assuming the attackers only gain access to your database.

Top comments (21)

Collapse
 
speculative profile image
Jeffrey Tao

You can turn 4 characters, into a really long string with an HMAC. And there you go, it's better than just storing 4 characters.

Why would this be better than storing 4 characters? You can easily enumerate all 10000 possibilities in spite of the HMAC. Even if the string is longer, the bits of entropy remain the same.

Collapse
 
devmazee2057282 profile image
dewbiez

Though this proves effective since the HMAC is basically peppering the PIN?

This wouldn't be more effective? Even if the attacker didn't know the PIN was being saved with an HMAC?

Collapse
 
speculative profile image
Jeffrey Tao

Full disclosure: I am not an expert.

So there's two things at play here:

Why the heck are you HMACing the password?!
What if they put in a 4 megabyte password, and send multiple requests? This could effectively DDoS you. However, you can prevent that, by pre-hashing with a fast algorithm such as SHA256.

Hashing is inherently throwing away some of the entropy, since it's a function mapping an arbitrary-sized input space (say, a 4MB password) into the output space of the hash function (in the case of SHA256, 256 bits). In this case, not storing 4 MB means that you're not really getting the "power" of a 4 MB long password.

Only 4 digit numbers, can be brute-forced within a reasonable amount of time. Think about it, you're only hashing 4 characters. You can turn 4 characters, into a really long string with an HMAC. And there you go, it's better than just storing 4 characters.

So it may seem like the output is longer, but really a hash/HMAC of an input space constrained to 4 numeric digits is not made harder to guess by passing it through a hash function. Since the hash function necessarily maps the same input to the same output, the range of possible outputs is still just 10,000 values. For example, a dictionary attack targeting pins would still be as effective against this scheme -- it does not inherently strengthen a weak password at all.

If the concern is simply to obscure the nature of the password (that it's a PIN) from an attacker, well, that smells a bit like security by obscurity to me :)

Thread Thread
 
devmazee2057282 profile image
dewbiez

Okay. Thanks.

Collapse
 
tadman profile image
Scott Tadman

While there’s some good ideas here, this all sounds like a really bad idea. Bcrypt does a really good job and can be tuned to be even harder to crack, making it very good at resisting brute force dictionary attacks. There’s other similar algorithms that can be better, but Bcrypt is a good baseline.

This HMAC approach, even using SHA3, seems really high risk, and the AES layer is just a minor inconvenience for any attacker. GPU accelerated cracking can smash through those with unnerving speed.

It’s also extremely doubtful that a password over 72 letters long is more secure than a 72 letter one. If that’s a random password, it’s already impossible to crack. That length is enough to accommodate even generously long pass-phrases, and most aggressively random passwords are 32-64 letters at most.

While opinions are fine, they don’t matter when you’re compromised, which is why I find these recommendations really concerning. Security is a huge responsibility when building a site that people will trust their passwords with. Don’t take short-cuts or be clever. Follow recommended best practices, and keep up to date with your techniques.

Today that means use Bcrypt and always encrypt your production data backups.

Collapse
 
devmazee2057282 profile image
dewbiez

Alright, thank you, means a lot.

Collapse
 
devmazee2057282 profile image
dewbiez

But, I'm curious. What do you mean the HMAC seems really high risk?

Collapse
 
tadman profile image
Scott Tadman • Edited

HMAC is intended to be fast, which makes it ideal for crackers. For passwords you want something deliberately slow. Bcrypt is one such function, and it can be tuned to be even slower if you prefer.

For example, you can crunch through potentially a million passwords per second using a GPU-accelerated rig and software like HashCat if it's just HMAC-SHA3.

Bcrypt with default settings is around 13K per second on the same hardware. It's hundreds of times slower, plus you can increase the factor which can make it even slower still.

When protecting passwords you need to consider how long you need to keep them protected. Eventually every hashing method will be compromised, MD5 and SHA1 have been effectively beaten. By using a stronger Bcrypt setting you can make it harder to crack passwords on not just today's hardware, but hardware that exists ten years from now, protecting your application against future attacks.

Twenty-five years ago when using SHA-based password hashes it was inconceivable that a consumer-grade GPU could have several thousand threads, that would be more powerful than any supercomputer on the planet at the time. What hardware will exist twenty-five years from now? What if having a half-million threads is considered no big deal?

As much as twenty-five years seems like a long time, what if ten years from now your app gets hacked, and ten years after that the database starts floating around as part of a Torrent? Those passwords might still be in use, some people are really bad at changing them, which means you've got a responsibility to do your best to protect them.

There's a lot to understand here, but this post covers a lot of ground.

Thread Thread
 
devmazee2057282 profile image
dewbiez • Edited

Yeah, I used HashCat to attack a Bcrypt hash before. I could do roughly 10-11 thousand attempts per second.

You did take note I wasn't just HMACing and encrypting the password though, right? I don't want any confusion. I was using a password hashing function.

Thread Thread
 
tadman profile image
Scott Tadman

That's what makes Bcrypt so great. Smashing through a dictionary against a compromised database is painful, and you can make it even more painful by cranking up the difficulty factor. It's very resistant against brute force attacks.

HMAC is meant for other things, like signing, where you're not dealing with brute-force attacks, where instead performance, authentication and verification are what matters. It's not in any way intended for, nor suitable to use as a password hash.

Collapse
 
devmazee2057282 profile image
dewbiez

And why do you make it sound like it's so easy to break AES encryptions? Is it?

Collapse
 
tadman profile image
Scott Tadman

Since the application must keep the AES key around somewhere handy, in the event of a compromise it's going to get stolen as well and then your encryption is worthless as they have the key.

From there dealing with a single layer of HMAC is pretty trivial.

Collapse
 
jonlauridsen profile image
Jon Lauridsen

Cryptography requires solid understanding of entropy and randomness and I’m not seeing that in this article. We need an adult in here, I know just know enough to be suspicious but not enough to refute and could use the insights of experts.

Collapse
 
devmazee2057282 profile image
dewbiez

Yeah, probably a good idea.

Collapse
 
adhocore profile image
Jitendra

passwords are not tweets or blogs to be too long? your disclaimer that you are by no means expert, invalidates all the opinions of your anyway. 😊

Collapse
 
devmazee2057282 profile image
dewbiez

I just don't want to label myself as an expert, but I've done extensive research. I still prefer my opinion over others.

Collapse
 
stevefutcher profile image
Steve Futcher

I have a much longer password than 16 characters for Windows. Where did you get that limit from?

Collapse
 
devmazee2057282 profile image
dewbiez • Edited

The Microsoft website, or at least it did.

Collapse
 
computersmiths profile image
ComputerSmiths

Security is hard. Many folks get it wrong. I have some Axis cameras with 8-character maximum passwords! (Plus they silently truncate when saving but not comparing, so you can set it to 123456789 but then you have to enter 12345678 to unlock).

Collapse
 
devmazee2057282 profile image
dewbiez

It is hard. I'm trying to get better at it. And yeah that sucks. Truncating your password silently.

Collapse
 
fribba_com profile image
Fribba.com

Interesting insights.
I also wrote a similar article on my blog about this topic. If anyone is interested in reading it here is the link