Contents


A cryptographic approach to protecting passwords in the cloud

Keep password data safe even after a breach

Comments

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


Comments

Sign in or register to add and subscribe to comments.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Security
ArticleID=1056674
ArticleTitle=A cryptographic approach to protecting passwords in the cloud
publish-date=01172018