Effective encryption is a fundamental part of online security. Node provides the crypto
module which we can use to generate our own MD5 or SHA1 hashes for user passwords. Cryptographic hashes, such as MD5 and SHA1 are known as message digests. Once the input data has been digested (encrypted), it cannot be put back into its original form (of course if we know the original password, we can regenerate the hash and compare it to our stored hash).
We can use hashes to encrypt a user's password before we store them. If our stored passwords were ever stolen by an attacker, they couldn't be used to log in because the attacker would not have the actual plain text passwords. However, since a hash algorithm always produces the same result, it could be possible for an attacker to crack a hash by matching it against hashes generated from a password dictionary (see the There's more ... section for ways to mitigate this).
See http://en.wikipedia.org/wiki/Cryptographic_hash_function for more information on hashes.
In this example, we will create a simple registration form, and use the crypto
module to generate an MD5 hash of a password gained via user input.
As with Basic Authentication, our registration form should be posted over HTTPS, otherwise the password is sent as plain text.
In a new folder, let's create a new server.js
file along with an HTML file for our registration form. We'll call it regform.html
.
We'll use the Express framework to provide the peripheral mechanisms (parsing POST requests, serving regform.html
, and so on), so Express should be installed. We covered more about Express and how to install it in the previous chapter.
First, let's put together our registration form (regform.html
):
<form method=post> User <input name=user> Pass <input type=password name=pass> <input type=submit> </form>
For server.js
, we'll require express
and crypto
. Then create our server as follows:
var express = require('express'), var crypto = require('crypto'), var userStore = {}, app = express.createServer().listen(8080); app.use(express.bodyParser());
bodyParser
gives us POST capabilities and our userStore
object is for storing registered user details. In production we would use a database.
Now to set up a GET route as shown in the following code:
app.get('/', function (req, res) { res.sendfile('regform.html'), });
This uses Express' sendfile
method to stream our regform.html
file.
Finally, our POST route will check for the existence of user
and pass
inputs, turning a user's specified password into an MD5 hash.
app.post('/', function (req, res) { if (req.body && req.body.user && req.body.pass) { var hash = crypto .createHash("md5") .update(req.body.pass) .digest('hex'), userStore[req.body.user] = hash; res.send('Thanks for registering ' + req.body.user); console.log(userStore); } });
When we use our form to register, the console will output the userStore
object, containing all registered user names and password hashes.
The password hashing portion of this recipe is:
var hash = crypto .createHash("md5") .update(req.body.pass) .digest('hex'),
We've used the dot notation to chain some crypto
methods together.
First, we create a vanilla MD5 hash with createHash
(see the There's more ... section on how to create unique hashes). We could alternatively create a (stronger) SHA1 hash by passing sha1
as the argument. The same goes for any other encryption method supported by Node's bundled openssl
version (0.9.8r as of Node 0.6.17).
For a comparison of different hash functions, see http://ehash.iaik.tugraz.at/wiki/The_Hash_Function_Zoo.
This site labels certain hash functions as broken, which means a weakness point has been found and published. However, the effort required to exploit such a weakness will often far exceed the value of the data we are protecting.
Then we call update
to feed our user's password to the initial hash.
Finally, we call the digest
method, which returns a completed password hash. Without any arguments, digest returns the hash in binary format. We pass hex
(base 16 numerical representation format of binary data, see http://en.wikipedia.org/wiki/Hexadecimal) to make it more readable on the console.
The crypto
module offers some more advanced hashing methods for creating even stronger passwords.
HMAC stands for Hash-based Message Authentication Code. This is a hash with a secret key (authentication code).
To convert our recipe to using HMAC, we change our crypto
portion to:
var hash = crypto .createHmac("md5",'SuperSecretKey') .update(req.body.pass) .digest('hex'),
Using HMAC protects us from the use of rainbow tables (pre-computed hashes from a large list of probable passwords). The secret key mutates our hash, rendering a rainbow table impotent (unless an attacker discovers our secret key, for instance, by somehow gaining root access to our server's operating system, at which point rainbow tables wouldn't be necessary anyway).
PBKDF2 is the second version of Password-Based Key Derivation Function, which is part of the Password-Based Cryptographic standard.
A powerful quality of PBKDF2 is that it generates hashes of hashes, thousands of times over. Iterating over the hash multiple times strengthens the encryption, exponentially increasing the amount of possible outcomes resulting from an initial value to the extent that the hardware required to generate or store all possible hashes becomes infeasible.
pbkdf2
requires four components: the desired password, a salt value, the desired amount of iterations, and a specified length of the resulting hash.
A salt is similar in concept to the secret key in our HMAC in that it mixes in with our hash to create a different hash. However, the purpose of a salt differs. A salt simply adds a uniqueness to the hash and it doesn't need to be protected as a secret. A strong approach is to make each salt unique to the hash being generated, storing it alongside the hash. If each hash in a database is generated from a different salt, an attacker is forced to generate a rainbow table for each hash based on its salt rather than the entire database. With PBKDF2, thanks to our salt, we have unique hashes of unique hashes which adds even more complexity for a potential attacker.
For a strong salt, we'll use the randomBytes
method of crypto
to generate 128 bytes of random data, which we will then pass through the pbkdf2
method with the user-supplied password 7,000 times, finally creating a hash 256 bytes in length.
To achieve this, let's modify our POST route from the recipe.
app.post('/', function (req, res) { if (req.body && req.body.user && req.body.pass) { crypto.randomBytes(128, function (err, salt) { if (err) { throw err;} salt = new Buffer(salt).toString('hex'), crypto.pbkdf2(req.body.pass, salt, 7000, 256, function (err, hash) { if (err) { throw err; } userStore[req.body.user] = {salt : salt, hash : (new Buffer(hash).toString('hex')) }; res.send('Thanks for registering ' + req.body.user); console.log(userStore); }); }); } });
randomBytes
and pbkdf2
are asynchronous, which is helpful because it allows us to perform other tasks or improve the user experience by immediately taking them to a new page while their credentials are being encrypted. This is done by simply placing res.send
outside of the callbacks (we haven't done this here but it could be a good idea since encryption of this magnitude could take around a second to calculate).
Once we have both our hash and salt values we place them into our userStore
object. To implement a corresponding login we would simply compute the hash in the same way using that user's stored salt.
We chose to iterate 7,000 times. When PBKDF2 was standardized the recommended iteration count was 1,000. However, we need more iterations to account for technology advancements and reductions in the cost of equipment.
13.59.34.87