A safer way to deal with passwords

Salvador Guerrero
7 min readMay 7, 2020

--

If you turn on the Developer Tools and inspect the traffic when logging in to a website you’re going to be surprised to see that many websites for big companies are sending your password in plain text, yes plain text, ok ok, it’s encrypted but only for the middle-man, not for the servers receiving the password; there are several reasons why I think this is wrong:

  • Someone with access to the servers can easily look/log at the incoming plain-text passwords.
  • The server log can accidentally be logging plain-text password.
  • Just imagine…

The websites we’re logging into will most likely contain more personal information like pictures, address, credit cards, etc, which is more important than a simple password, but we should treat passwords with care because they are almost always reused on other websites (or slightly changed), and if the password gets compromised the user will have to worry about information from multiple sites not just one.

We should be using different passwords for different sites, but the only way for this to work is to use a password manager, and if you’re talking about a family then you will have to pay for a service to stores your and your family members passwords, P.M. subscriptions are not that expensive, but subscripturation is real and it adds up; websites should be protecting passwords as if it was shared with other websites.

I think that companies can put a little more efforts in protecting user passwords, in just a couple of hours a websites can be sending cryptographic hashes instead of plain-text passwords, hopefully this story can give you the energy and some ideas to add an extra layer of security/privacy.

Disclaimer: I’m not a security expert and I’m not giving advices how your login should work, nor how to store your users passwords, I’m sharing my findings and my thoughts on how I think passwords should be handled… with care.

Here’s what I’m going to be covering in this story:

  • Send cryptographic hashes instead of passwords.
  • Store strong cryptographic hashes on the server.

I’ll be reusing the source code from my previous story: Parsing post data 3 different ways in Node.js without third-party libraries — application/json, application/x-www-form-urlencoded, and multipart/form-data

Send cryptographic hashes instead of passwords

The first thing we need to do is to adapt the login/create-account <form> to hash the password before sending it to the server, the example below is the simplest of all 3 types of forms, if you would like to know how to send the form data as JSON or include binary data take a look at the full source code below:

<form action="javascript:" onsubmit="onSubmit(this)">
<input type="text" name="username" placeholder="username">
<input type="password" name="password" placeholder="password">
<input type="submit">
</form>

With the above <form> when the user presses the submit button is will call onSubmit() instead of the default behavior, the below code shows the implementation of onSubmit():

function onSubmit(form) {
(async()=> {
const formData = new FormData(form)
const password = formData.get("password")
const passwordHash = await securePasswordHash(password)
formData.append("passwordHash", passwordHash)
try {
const response = await fetch('/', {
method: 'POST',
body: formData
})
const text = await response.text()
if (response.status !== 200) {
if (text && text.length > 0) {
console.error(text)
} else {
console.error('There was an error')
}
return
}
document.body.innerHTML = text
} catch (e) {
console.error(e.message)
}
})()
}

For the purpose of this example I’m going to be sending the plain-text password and the hashed password so that we can verify that the client hash on the server side.

The first thing I do in onSubmit() is to bind the html form with FormData with const formData = new FormData(form), then I get the password value and create a secure hash using securePasswordHash() and I append it to the formData with the passwordHash key, then I create a fetch request and send the formData which will contain the original content of the form plus the data we appended to it. In real applications replace the password key/value with formData.set("password", passwordHash) instead of creating a new key/value pair for the password like in this example.

The client hashing magic happens within the securePasswordHash() method using PBKDF2 HMAC-SHA-256 (same used by LastPass and 1Password) that requires a salt and iteration count, the higher the iteration the slower it gets for hackers but also the user experience, so for client hashing I’m going to be setting 5,000 as the iteration count because the computer where the website is running might be slow, LastPass in 2011 used 5000 iterations for JavaScript clients, because the salt is safe to be public I’m going to be using the password in reverse as the salt, I could of used the username but I didn’t want to tied the hash to the username.

async function securePasswordHash(password) {
if (!password) return null
let textEncoder = new TextEncoder()
let saltBuffer = textEncoder.encode(reverseString(password))
let encodedPassword = textEncoder.encode(password)
let baseKey = await window.crypto.subtle.importKey(
"raw",
encodedPassword,
"PBKDF2",
false,
["deriveBits"]
)
let keyBuffer = await window.crypto.subtle.deriveBits(
{
"name": "PBKDF2",
"hash": "SHA-256",
salt: saltBuffer,
"iterations": 5000
},
baseKey,
256
)
return arrayBufferToBase64(keyBuffer)
}

After checking that password is non-empty I’m reversing it and encoding the real password and the reverse one toUint8Array.

To be able to create a derived key off the password using PBKDF2 we first need to create a base key with crypto.subtle.importKey() where we specify that it’s a raw password, we give it the password array, we tell it to use PBKDF as the algorithm and that we only want the key to derive the key bits.

Next up is to derive the key bits using crypto.subtle.deriveBits() which require the base key created with importKey() the salt, the algorithm to use for hashing, the length of the output which I want it to be the same length as SHA-256’s output, and of course the iteration count which is specified to be 5,000.

I’m then returning the derived key bits in base64 so that I’m able to send it to the server and apply it a second stronger cryptographic hash on the server side.

At this point you have learned how to send cryptographic hashes instead of plain-text passwords using HTML forms, this is a great improvement already! Continue reading to learn how to create an even stronger cryptographic hash when storing the password on the server database.

Store strong cryptographic hashes on the server.

My personal opinion about this is that anything less than the below example to store passwords on web servers should not be acceptable, you should not be using plain simple SHA-2 (SHA-224, SHA-256, etc.) to store passwords on the server side because they are easy to crack using Dictionaries, Lookup Tables, and Rainbow Tables, if you would like to learn more about this I recommend reading Secure Salted Password Hashing.

Alright after the request data has been parsed either JSON or multipart/form-data, it’s time to get the secure hash created by the client and hash it on top of that, this second hash is the one you’re going to be storing on the server database and also be using to validate user login.

If this is a new account or for changing password the first thing the server needs to do is to generate a new secure random salt, the US National Institute of Standards and Technology recommends a salt length of 128 bits, I’m going to be making it the same size as the output of the hash function, which is 256 bits.

let saltBuffer = crypto.randomBytes(32)

Next up is to create the derived key, which is so much simpler on Node.js than plain JavaScript on the client side:

let derivedKey = crypto.pbkdf2Sync(data.passwordHash, saltBuffer, 100000, 32, 'sha256')

In the above line data.passwordHash is the hash creted by the client, 32 is the lenght in bytes of the output and because servers are a lot more powerful than clients I set it to do 100,000 iterations,

Following crackstation.net recommendation, if your website has over 100,000 users you should be adding an extra step of security, which is to add a secret to the derived key using something like HMAC, I’m going to leave this task as homework for you.

The next thing I do after creating the derived key I encode it to base64, the same thing I do for the salt and store both on the database, but for this example I’m going to be returning the derived key and salt together delimited with ‘:’

data.serverPasswordHash = saltBuffer.toString('base64') + ':' + derivedKey.toString('base64')

Just for kicks and for this particular example, next I show how I validate the hash generated by the client with a hash created by the server using the same values:

saltBuffer = new Buffer(reverseString(data.password))
derivedKey = crypto.pbkdf2Sync(data.password, saltBuffer, 5000, 32, 'sha256')
data.matchClientPasswordHash = derivedKey.toString('base64')
data.matched = data.matchClientPasswordHash === data.passwordHash

The data object is the data that we’re returning to the client, below is an example output:

{
"username": "sal",
"password": "MyW3@kP@$$!",
"passwordHash": "IH+kRSeTPHvudfLZMCldXFmXnARQMgaCWGWu3gEX4mw=",
"serverPasswordHash": "a3k1RzHeGrkqRQRhy2qohCu6Cfcb+5UhcsZ3Bjtg8K4=:kuh0eM7sm+ujX5pQoAhCIyEUtmxPZb+/yBF/uINK8DA=",
"matchClientPasswordHash": "IH+kRSeTPHvudfLZMCldXFmXnARQMgaCWGWu3gEX4mw=",
"matched": true
}

“username” — key containing the original username.
“password” — key containing the original password (do not send to server).
“passwordHash” — secure password hash created by the client, use this to replace “password”.
“serverPasswordHash” — secure password hash on top of “passwordHash” to store on the server and for validation.
“matchClientPasswordHash” — secure hash created by the server, should be the same as what the client generated (just for debug).
“matched” — true if the hash created by the client matches with the one created on the server (just for debug).

And with that you can create a web app that on top of HTTPS you hash passwords before sending them to your server and on top of that you are also storing strong password hashes on the server.

Below you will find the full source code for the above example

References:

Online PBKDF2 validation tools:

--

--

Salvador Guerrero

Computer Science Engineer, Cross-Platform App Developer, Open Source contributor. 🇲🇽🇺🇸