Credit: IBM
Keep password data safe even after a breach
In this article, back-end developers learn why it is important to use
encryption and how to use it effectively to protect user information on
the cloud, especially passwords, so that even a data leak can’t be cracked
in less than decades. Security is an ever important topic in the cloud
that is crucial to full-stack development and is essential on all products
and services.
Let’s begin by addressing these straightforward things to do (or not to do)
when you are considering security in development:
- Always choose to use someone else’s hashing/encryption library that
others have already scrutinized and reviewed. - Don’t print passwords to logs!
- Use some form of key management service.
- Don’t commit secrets (API keys, passwords) in the code
repository.
In this article, I’m going to walk you through an example app to focus on
ways in which to encrypt critical data. For the storage of passwords that
are covered in this article, we will be using a SQLite
database, since it is readily available on almost any system. The
same principles and ideas are in use almost everywhere, and the database
system should not really matter (although depending on the chosen
database, there can be better ways to hash and secure user information). I
also want to show what happens if you were to lose the database file, but
still keep user hash intact and
uncrackable.
Using bcrypt
bcrypt is one of the
most widely used functions available today for password hashing. It is
available for most programming languages and often there are
super-specific modules that are available for specific frameworks and
databases. Let’s look at this repo example.
This code is commonly used with Node.js and is incredibly straightforward
(it allows the salting and hashing functions to be called either
sync
or async
). It also allows you to not worry
about the implementation details or how salting is done, and instead
allows you to focus on preventing accidental password leaks.
What is hashing, salting, and
encryption?
While hashing and encryption might not seem different and can be used
interchangeably, they are actually quite different and have different use
cases. A hash function takes some input and has one-way mapping to an
output. While there is a spectrum of hashing techniques and algorithms, I
recommend bcrypt for passwords. You can read more about cryptographic hash functions here, but it is generally not
necessary to understand the underlying details for these functions.
Salting is used during hashing, and acts as additional
information that is supplied to the hash functions so that if one hash is
found out (either by accident or brute force), you are unable to check
other hashes that might have a similar input. For example,
user_1
has a password that is identical to
user_2
‘s password. These users would not have had their
passwords found out if salting was used in the hash function. To read more
about this function, there are a variety of information
and examples here.
Alternatively, encryption is a one-to-one mapping of some input to an
output. An important key difference is that it is reversible if you have
the encryption key.
You can use hashing to check an input to another input later, but
you don’t want to store the input outright (passwords, pin numbers, and
more). Encryption can be used when you are sending messages (and both
parties have a key to encode/decode), or if you want to store some private
information (such as home address or credit card), but need a way to
retrieve this information later.
The front end
Because the focus of this article is not on the front end, we are not
going to get into using anything that would add complications or another
framework to worry about. Instead, we are going to use two forms for
login/register on the same page. We won’t do anything with these forms
besides using super simple bootstrapping, since that isn’t the focus in
this article.
<form action="/signin" method="post"> <div class="row"> <div class="col"> <input name="email" type="email" class="form-control" placeholder="email"/> </div> <div class="col"> <input name="password" type="password" class="form-control" placeholder="password"/> </div> <div class="col"> <button class="btn btn-dark">sign in</button> </div> </form> <form action="/register" method="post"> <div class="row"> <div class="col"> <input name="email" type="email" class="form-control" placeholder="email"/> </div> <div class="col"> <input name="password" type="password" class="form-control" placeholder="password"/> </div> <div class="col"> <button class="btn btn-dark">register</button> </div> </div> </form>
We are also posting the inputs to the back end from the form and not
dealing with checking/creating/setting sessions, since that doesn’t follow
the scope of this article either, and can be quite expansive, depending
on what the goal or aim of your application is.
Creating the back end
Next, we are going to run the back end in Node.js by using the Express
framework and SQLite to make the most basic system possible for the
purposes of this article.
const path = require('path') const bcrypt = require('bcrypt') const bodyParser = require('body-parser') const sqlite = require('sqlite') const express = require('express') const app = express() app.use(bodyParser.json()) app.use(bodyParser.urlencoded({ extended: true })) const dbPromise = sqlite.open('./database.sqlite', { Promise }) const saltRounds = 10
All that we are doing here is creating a promise for the database, generating a
salt, and creating the application and simple middleware to get the
username/password, along with loading some libraries that we want to
use.
Routes
In terms of what our server will do, we will have a route to log in and a
route for the user to register. They are separated to understand what is
happening in the system, yet are not doing anything (well, anything
that is related to sessions/cookies/etc.). Once the passwords match, we (rather
simply) show how hashing a password would be done and then be checked. The
login route is almost identical to the register route, and we are not
doing any data validation on either route, although we are checking for an
email on the HTML form.
app.get('/', async (req,res) => { res.sendFile(path.join(__dirname, '/main.html')) }) app.post('/register', async (req, res) => { const db = await dbPromise // check if user already exists const checkUser = await db.get('SELECT * FROM Users WHERE email = ?', req.body.email) if (checkUser) { return res.send('user already exists') } const hashedPassword = await bcrypt.hash(req.body.password, saltRounds) const resp = await db.run(`INSERT INTO Users VALUES(?,?)`, req.body.email, hashedPassword) res.send('registered') })
The register route checks whether a user exists in the database and whether
or not we inserted them into the database with a hashed password. Keep in
mind that we are not doing anything to mitigate SQL injections or various
other forms of hacking/abuse. If the user does not exist, we hash the
password with the bcrypt hash function and it salts it for us, since we
provided the number of rounds to salt. This hashing allows us to store the
user’s password in such a way that we can check the password in the future
if he/she types it in. We are not personally capable of looking up the
password. Also, we should not be printing the password to the user’s logs,
and we would likely want to create the ability to check a password and save a user’s password into a hash by using a database
model.
While the login route is almost identical (and we could easily refactor
this to make it more DRY, but we are going for understanding here), there is a slightly
different line with:
const passwordMatch = await bcrypt.compare(req.body.password, user.password)
All this does is to use Bcrypt to compare the hashed password and the
password that the user typed in on the front end for us and returns true or
false. Since the salt is incorporated into the hash, we do not need to
explicitly use it to compare. Below is the complete server.js
to run:
While the login route is almost identical (and we could easily refactor
this to make it more DRY, but we are going for understanding here), there
is a slightly different line with:
const passwordMatch = await bcrypt.compare(req.body.password, user.password)
All the above line does is to use Bcrypt to compare the hashed password and
the password that the user typed on the front end for us and returns as
true or false. Since the salt is incorporated into the hash, we do not
need to explicitly use it to compare. The below code listing is the
complete server.js
to
run:
const bcrypt = require('bcrypt') const bodyParser = require('body-parser') const express = require('express') const app = express() app.post('/register', async (req, res) => { const db = await dbPromise const hashedPassword = await bcrypt.hash(req.body.password, saltRounds) const resp = await db.run(`INSERT INTO Users VALUES(?,?)`, req.body.email, hashedPassword) res.send('registered') }) app.post('/signin', async (req, res) => { const db = await dbPromise const user = await db.get('SELECT * FROM Users WHERE email = ?', req.body.email) if (!user) { return res.send('user doesnt exist') } const passwordMatch = await bcrypt.compare(req.body.password, user.password) if (passwordMatch) { return res.send('signed in') } res.send('password does not match') }) app.listen(PORT, async () => { console.log(`app listening at http://localhost:${PORT}`) })
Now install the dependencies:
yarn add bcrypt express body-parser sqlite
.
Run the server Node server.js
, and open http://localhost:8080
.
Then try both signing in and creating a user, and sign in again.
Sending unencrypted passwords over the
net!
Although this article is just to show you how to store and hash passwords,
and you are not keeping the plaintext password of users, we are still
sending the plaintext between the browser and the back end since we are
not using HTTPS. If this example was in production, hackers can easily see
these passwords (both the sign in and register) sent between the server
and the client if they were in the middle of this communication. There are
tons of different ways to actually deal with preventing man-in-the-middle hacks, but for the sake of
simplicity we are going to deal with it in Express and generate
self-signed SSL certificates as an example for how this would work. Keep
in mind that these are not signed in the same way as getting certificates
from LetsEncrypt or various other providers of SSL/TLS certs.
First, we need to install OpenSSL either via a package manager or from their official website. On
macOS, if you have homebrew already installed, you
can simply write:
brew-install Openssl
Next, you will want to run the command to generate a key and a
certificate:
openSSL req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem
-days 30
This command will require you to enter some information, but in the end you
will have a key.pem
and a cert.pem
. With these,
you can add the following to the top of server.js
(note that
we are using the https standard library from Node.js now):
const fs = require('fs') const https = require('https') const options = { key: fs.readFileSync('key.pem'), cert: fs.readFileSync('cert.pem') }
At the bottom of our code, we previously had:
const PORT = 8080 app.listen(PORT, async () => { const db = await dbPromise await db.run("CREATE TABLE IF NOT EXISTS Users (email TEXT, password TEXT)") console.log(`app listening at http://localhost:${PORT}`) })
We will change the previous code above to:
const PORT = 8081 https.createServer(options, app) .listen(PORT, async () => { const db = await dbPromise await db.run("CREATE TABLE IF NOT EXISTS Users (email TEXT, password TEXT)") console.log(`app listening at https://localhost:${PORT}`) })
At this point, we would be only using HTTPS and would be sending the
password encrypted to our server, and the passwords would be hashed when
saving to our database.
Worst case scenario: The database gets leaked
Let’s imagine that our server was hacked, or some
other exploit occurred, and our SQLite (or any database) got
leaked. While this scenario is terrible, we can at least be confident that
the user passwords themselves should be safe from being used and
we minimize the chances of requesting users to change their passwords elsewhere.
For instance, Figure 1 below shows that instead of seeing a password
secret
for user graham@test.xyz
, the hash is
useless to hackers who try to use it.
fig01.png
Conclusion: Other alternatives for cloud security
While the instance of creating and storing user data might be the most
straightforward route, there are alternatives that allow you to maintain
information about who a user is (via something like OAuth that allows a
user to sign in on a peripheral service and you get back authentication
information, or signing in through an email). Alternative routes mean that
you don’t have to store secret information about users that can be a
potential liability. While there might be many ways to obfuscate and
protect a user’s information that you are storing in some database, there
is almost always guidance in whatever server frameworks documentation and
database you are using for security best practices.
For instance, visit Express.js’s Best Practices as it lists numerous topics lthat
were not covered in this article. One example that was not covered here was using
Helmet
to prevent some well-known HTTP headers vulnerabilities. There are
also recommendations for things such as SQL injection mitigation and
rate-limiting to prevent brute force guessing. While there is no single
and simple way to mitigate all these issues, it never is extremely
difficult to look over the docs or suggestions online.
Downloadable resources
Related topics
Credit: IBM