Password Security: A bit deeper dive into Hashes, Salts, Bcrypt and Node's Crypto module.
Stephen
Posted on November 12, 2021
As I finished much of the administration portion of the current project I'm working on, I started to explore possible solutions for authentication within the app. I spent a good portion of last month going through a tutorial series made by Zach Gollwitzer (link to it on freeCodeCamp's youtube page as well as the playlist Zach's own youtube channel). The tutorial seems pretty comprehensive, it begins with a review of using Express middle-ware, because moving forward, you use lots of middle-ware throughout the rest of the series. The series then moves on to using PassportJS, the Passport Local Strategy and Sessions, and then to using PassportJS with Javascript Web Tokens (JWT) with Passport and then to JWTs by themselves in an angular front-end. I didn't actually go through the very end, which was the part where you implement a JWT in a Angular front-end because I wanted to get back to working on my own project. Transitioning back, I had to think about how I wanted to implement a user authentication system. Did I want to use Passport-Local with sessions? Did I want to use JWTs? I decided to go with what I found easiest while following along with the tutorial, which was using Passport Local and sessions, especially since this is the first time I'd be implement authentication and because I want to get this project done with.
In Zach's course, he used NodeJS' built-in 'crypto' module to produce hashes and salts. Admittedly, I followed the tutorial and just let the magic happen without trying to understand what was actually going on because I wanted to understand how passport worked. In my own research that followed, I found that many developers seemingly preferred the bcrypt standard as opposed to Node's crypto module. A google search will send you down multiple rabbit holes comparing the pros and cons of using Node's Crypto module, or one of npm's bcrypt packages (there's a version that hooks into the V8 engine's C++ underpinnings, while there's another that is totally written in JS and can be run in the browser). So here's some of the advantages of using Node's Crypto Module or Bcrypt that I gleaned:
Bcrypt Advantages:
- Seeming industry standard and widely accepted
- bycrypt npm package drills into V8's C++ underpinnings, more performant
- bcryptjs is javascript from the ground up, and can be used in the browser (for whatever reason)
- the bcrypt modules parse salt and hash automagically and only requires one db table column
Node Crypto Module Advantages:
- Built into Node, no reliance on dependencies that could one day inject malicious code
- No need to install
- I'm already familiar with it 😁
In the process of learning more about bcrypt, I learned more about how hashing and salting worked by watching a number of great and informative videos on the topic. I'll attempt to briefly explain it in my own words in the next section.
Hashing and Salting: a quick and dirty primer
If I could explain what hashing and salting is in my own words (because, it turns out it's a beautifully thought out method of password protection and authentication):
A password is first hashed using a hashing algorithm. Algorithms like blowfish and argon2 associate a random set of characters to a given word or string of characters (in the case of a user, a password). After the assignment of those random characters (in the case of bcrypt, 31 characters represents a given entered string), the actual password is lost forever, and whatever the user enters is converted to a hash and compared to the hash that was stored in the database after their initial database entry. If the two compared hashes match (the initial passwords hash will always look like that even if the password is forgotten), the user is granted access because they entered a string of characters whose hash equivalent matches the stored hash in the database.
While this solution itself is clever and elegant, there's an underlying inherent problem; if the password is too simple, a simple word or number combination, or even a word and combination (such as 'BuffaloBills99' or 'NWO4Life'), the corresponding hash may already have been discovered by hackers generating whats called rainbow tables using dictionaries of words and concatenating digits at the end. If a system is hacked, and malicious parties obtain the database with these simple hashes, they could match at least a few of the entries using rainbow tables because there are at least a few users who will, unfortunately enter easily cracked password entries. Along with that, there may be users who use the same password, which would, in turn, generate the same hash value, and if a hacker or malicious party figures out that hash value for one, they could search the whole password hash table for matches.
The solution to adding complexity to a given hash by default is by adding a salt, another random set of strings to the hashed password to derive a new hash. Ideally, each password hash would have its very own random salt paired it to create individually unique hashes so that even if there are multiple matches among the first round of hashes, those matches would be obscured by the newly given salts. While its necessary to record the explicit salt paired with each password hash, and the salt is known to anyone who accesses the database table, a malicious party would have to first un-salt each salted hash to even start seeing matching hashes. This may sound like a worthwhile task, but what if the password table contains millions of rows? And the malicious party may still not be able to figure out what the unsalted hashes mean even if there are matches! Remember time is a resource we can't get back!
So, anyways, the way Node's Crypto and Bcrypt/BcryptJS handle hashes is a bit different between their two paradigms. Node's crypto produces a salted hash and the salt, requiring the developer to make two database columns to store each, while the bcrypts return a value with the combine salted hash and salt, and bcrypt has its own methods that can use the integrated salt value to unsalt the salted hash value. This in turn requires a single table column in a given database.
A given bcrypt method produces a bcrypt hashstring in the following format:
$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
\__/\/ \____________________/\_____________________________/
Alg Cost Salt Hash
Bcrypt produces a string where the salt is 22 characters long and the (salted) hash is 31 characters long, along with a few characters that indicate the precise algorithm being used and the 'cost' (or how many times a salt string is randomized/salted?... I'm still a bit murky on understanding that).
While, again, the node-crypto
module, provides individual hash and string values that you can conveniently store in two columns of your preferred database:
The hash value is 128 characters long, above, so it couldn't be all be shown on screen.
Two distinctive ways to handle the same problem, with bcrypt's being a bit more clever, at least in my opinion.
This is a coding blog afterall...
I guess I could share some comparative samples of code used to generate the hash and salt using a bcrypt
and crypto
npm module. The crypto sample is from tutorial I wrote about earlier, and the bcrypt sample is from a little node project/sandbox I conjured up this past Sunday to better understand how bcrypt worked without mucking up my current main project:
const bcrypt = require('bcryptjs');
async function hashesString(userInput) {
const password = userInput
const saltRounds = 15;
const userInputHashed = await bcrypt.hash(password, saltRounds)
return userInputHashed;
}
module.exports.hashesString = hashesString
The bcrypt
module provides bcrypt.hash()
(also bcrypt.compare()
) methods that you can nest within async functions so that the server can do other things while all of the computationally intensive hashing rounds happen.
I then imported that async function to the file holding the relevant route (in this small project, I just put the routes in app.js):
const { hashesString, passwordCheck } = require('./bcryptUtil');
app.post('/submit-form', (req, res) => {
const userInput = req.body.string;
hashesString(userInput)
.then((output) => {
console.log(output);
res.send(JSON.stringify({ output: output }))
})
.catch(err => console.log(err))
})
Now for the code from the authentication tutorial I followed:
const crypto = require('crypto');
function genPassword(password) {
let salt = crypto.randomBytes(32).toString('hex');
let genHash = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex');
return {
salt: salt,
hash: genHash
}
}
The hash generation functionality within the crypto
module is a bit more involved, here's what the parameters are for the method:
crypto.pbkdf2Sync(password, salt, iterations, keylen, digest)
Where password is normally the string input entered by a given user, salt itself can be derived by another method (in the above code its created using the crypto.randomBytes
method, 'iterations' are seemingly the crypto module's equivalent of asking for the number of rounds, 'keylen' allows the developer to determine the length of the resulting hash, and 'digest' seems to be the algorithm used to generate the hash.
Here's genPassword
being used in a route method, my comments and all:
const genPassword = require('../lib/passwordUtils').genPassword
router.post('/register', (req, res, next) => {
const saltHash = genPassword(req.body.pw);
/*^ passes collected password to genPassword from passwordUtils*/
const salt = saltHash.salt;
/* ^Holds value of salted saltHash
returned from genPassword */
const hash = saltHash.hash;
/* ^Holds value of salted and hashed
saltHash returned from genPassword */
const username = req.body.username;
const admin = false;
let newUser = new User(
username,
//^takes username value entered from form
hash,
//^stores salted and hashed password
salt,
//^stores salted password
admin
)
newUser.save()
.then((newUser) => {
console.log(newUser)
});
//save is a method for the database
res.redirect('/login'); //redirects back to login page
});
The method above may look longer but it does much more than the bcrypt post method; as opposed to just console logging the hash, here, the username, hash, and salt could be sent to the a database (the method also console logs the new user info because dev environment), and then the server redirects the browser to the /login
route.
In Conclusion...
So if it isn't clear to you yet, I might have written this article to explain how hashes and salts work, as well as to try and figure out whether to use bcrypt
or node's crypto
module to do the hashing and salting in the project I'm currently working on. To be honest, I still can't pick one. I'm more partial to crypto,
and yeah, it's dependencies don't have the magic ability to become malicious one day because it's built into node. But bcrypt, bcryptjs,
to be precise, seems a bit easier to use. So the stalemate continues. I at least hope that you, the reader, come out of this with a better understanding of how passwords are normally stored in databases.
If you'd like to take a look at the little experimental sandbox I put together, here's the link. Here's the link to my implementation of Zach Gollwitzer's code throughout his authentication tutorial which I linked to above. I used MariaDB instead of MongoDB because I think relational databases are cool too.
**This post is also up on my wordpress blog at bxbytes.com. Link here.
Posted on November 12, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 12, 2021