What would happen to user data if criminals were to get ahold of your database? Cybercrime is a persistent threat, and bad actors lurk at every corner seeking to pass malicious scripts to clone your database. What extra steps can you take to protect user information?
For instance, when a user creates an account in an application, their passwords and usernames need to be kept securely in the database, possibly by encrypting. However, to login into their accounts, the user’s password and username are verified against sets of credentials that are already in the database.
This cannot work if the passwords in the database — which are encrypted into gibberish — are used to compare the password/email the user inputs. That’s why the credentials in the database have to be decrypted while comparing them to the user’s input. Passwords can either be hashed or encrypted; hashing is a one-way encryption method.
The best solution is to employ cryptography on sensitive information before sending it to the database. This way, when cybercriminals get hold of your database, all they see are random characters.
In this tutorial, we’ll go over the basics of cryptography in Node.js and demonstrate how to use the Node.js crypto module to secure user data. We’ll build a sample app to demonstrate how to encrypt and decrypt usernames and passwords in Node.js using crypto.
Here’s what we’ll cover:
To follow along with this tutorial, you should have:
Cryptography is the process of converting plain text into unreadable text and vice-versa. This way, only the sender and receiver of the information understand its content.
With cryptography in Node.js, you can hash passwords and store them in the database so that data cannot be converted to plain text after it is hashed; it can only be verified. When malicious actors get ahold of your database, they cannot decode the encrypted information. You, can also encrypt other user data so that it can be decrypted during transmission.
The kind of encryption you employ on your application depends on your needs. For instance, cryptography can be symmetric-key (such as hashing), public-key (such as encrypting or decrypting), and so on.
An end party that receives encrypted data can decrypt it to plain text for their consumption. Cybercriminals cannot decrypt encrypted data if they do not have the key. This is exactly what the Node.js crypto module does.
The Node.js crypto module provides cryptographic functions to help you secure your Node.js app. It includes a set of wrappers for OpenSSL’s hash, HMAC, cipher, decipher, sign, and verify functions.
crypto is built into Node.js, so it doesn’t require rigorous implementation process and configurations. Unlike other modules, you don’t need to install Crypto before you use it in your Node.js application.
crypto allows you to hash plain texts before storing them in the database. For this, you have a hash class that can create fixed length, deterministic, collision-resistant, and unidirectional hashes. For hashed data, a password cannot be decrypted with a predetermined key, unlike encrypted data. An HMAC class is responsible for Hash-based Message Authentication Code, which hashes both key and values to create a single final hash.
You may need toencrypt and decrypt other user data later for transmission purposes. This is where the Cipher
and Decipher
classes come in. You can encrypt data with the Cipher
class and decrypt it with the Decipher
class. Sometimes, you may not want to encrypt data before storing them in the database.
You can also verify encrypted or hashed passwords to ensure they are valid. All you need is the Verify
class. Certificates can also be signed with thesign
class.
All these are reasons developers love to use the crypto module. Let’s explore the various crypto classes and discover how to implement cryptography with them.
Let us look at the classes in crypto that enable us to implement cryptography.
Cipher
The Cipher
class is responsible for encrypting information. When the user inputs a password during registration, the C``ipher
class is called to encrypt the password.
First, we’ll generate a key from an algorithm. After that, we’ll generate a random initialization number (iv
) before encrypting the text.
To use this class, you have to create a cipher instance using either the crypto.createCipher()
or crypto.createCipheriv()
. It‘s advised to use crypto.createCipheriv()
since crypto.createCipher()
is depreciated.
The program below shows how to encrypt passwords with the Cipher
method:
// Import module into your application const crypto = require('crypto'); const algorithm = 'aes-192-cbc'; const password = '2001MyForever'; // We will first generate the key, as it is dependent on the algorithm. // In this case for aes192, the key is 24 bytes (192 bits). crypto.scrypt(password, 'salt', 24, (err, key) => { if (err) throw err; // After that, we will generate a random iv (initialization vector) crypto.randomFill(new Uint8Array(16), (err, iv) => { if (err) throw err; // Create Cipher with key and iv const cipher = crypto.createCipheriv(algorithm, key, iv); let encrypted = ''; cipher.setEncoding('hex'); cipher.on('data', (chunk) => encrypted += chunk); cipher.on('end', () => console.log(encrypted));// Prints encrypted data with key cipher.write('some clear text data'); cipher.end(); }); });
Decipher
The Decipher
class is responsible for decrypting encrypted texts. When you intend to send information securely to another developer, you have to encrypt it. The only way the receiver of the information can read the information is to decrypt it. This is exactly what the Decipher
class does.
You cannot create decipher objects directly with the new keyword. The crypto.createDecipher()
or crypto.createDecipheriv()
methods are used to create decipher instances.
crypto.createDecipher()
is depreciated, so you should use the crypto.createdeCipheriv()
method instead.
Here’s how to decipher encrypted text with Decipher
:
// Import module into your application const crypto = require('crypto'); const algorithm = 'aes-192-cbc'; const password = 'Password used to generate key'; // We will first generate the key, as it is dependent on the algorithm. // In this case for aes192, the key is 24 bytes (192 bits). // We will use the async `crypto.scrypt()` instead for deciphering. const key = crypto.scryptSync(password, 'salt', 24); // The IV is usually passed along with the ciphertext. const iv = Buffer.alloc(16, 0); // Initialization vector. // Create decipher with key and iv const decipher = crypto.createDecipheriv(algorithm, key, iv); let decrypted = ''; decipher.on('readable', () => { while (null !== (chunk = decipher.read())) { decrypted += chunk.toString('utf8'); } }); decipher.on('end', () => { console.log(decrypted); // Prints: some clear text data }); // Encrypted with same algorithm, key and iv. const encrypted = 'e5f79c5915c02171eec6b212d5520d44480993d7d622a7c4c2da32f6efda0ffa'; decipher.write(encrypted, 'hex'); decipher.end();
Hash
The Hash
class is used for plain text hashing purpose. Hashing simply converts plain text into hash functions. Hashed text cannot be converted back to its original version. You cannot create hash objects directly with the new
keyword.
To create a hash instance, use the crypto.createHash()
method, as shown in the example below:
// Import module into your application const crypto = require('crypto'); // create hash algorithm const hash = crypto.createHash('sha256'); hash.on('readable', () => { // Only one element is going to be produced by the // hash stream. const data = hash.read(); if (data) { console.log(data.toString('hex')); // Prints: // 6a2da20943931e9834fc12cfe5bb47bbd9ae43489a30726962b576f4e3993e50 } }); hash.write('some data to hash'); hash.end();
Certificate
A Certificate
is made up of a key pair and other information that is used for encrypting electronic documents.
A certificate can produce a session key for the purpose of transmitting information securely over the internet. With the crypto Certificate
class, you can work with Signed Public Key and Challenge (SPKAC) using OpenSSL’s SPKAC implementation.
Below is an example of how to use the Certificate
class:
const { Certificate } = require('crypto'); const spkac = getSpkacSomehow(); const challenge = Certificate.exportChallenge(spkac); console.log(challenge.toString('utf8')); // Prints: the challenge as a UTF8 string
DiffieHellman
To successfully decipher a cryptograph — especially encrypted cryptographs — you need a key. A key is like a shared secret between the sending party and the receiving party. If keys are not kept securely, hackers can get ahold of them and cause havoc with user information.
Crypto’s DiffieHellman
class utilizes Diffie-Hellman key exchanges. Diffie-Hellman key exchange is a method for securely passing cryptographic keys in public channels. This secures keys that are specifically for information senders and receivers.
To use the DiffieHellman
method, follow the example shown below:
onst crypto = require('crypto'); const assert = require('assert'); // Generate Alice's keys... const alice = crypto.createDiffieHellman(2048); const aliceKey = alice.generateKeys(); // Generate Bob's keys... const bob = crypto.createDiffieHellman(alice.getPrime(), alice.getGenerator()); const bobKey = bob.generateKeys(); // Exchange and generate the secret... const aliceSecret = alice.computeSecret(bobKey); const bobSecret = bob.computeSecret(aliceKey); // OK assert.strictEqual(aliceSecret.toString('hex'), bobSecret.toString('hex'));
ECDH
Elliptic-curve Diffie–Hellman (ECDH) is used for establishing a shared public-private key pair with elliptic-curve.
The example below shows how to use the ECDH
class in your Node.js application.
const crypto = require('crypto'); const assert = require('assert'); // Generate Alice's keys... const alice = crypto.createECDH('secp521r1'); const aliceKey = alice.generateKeys(); // Generate Bob's keys... const bob = crypto.createECDH('secp521r1'); const bobKey = bob.generateKeys(); // Exchange and generate the secret... const aliceSecret = alice.computeSecret(bobKey); const bobSecret = bob.computeSecret(aliceKey); assert.strictEqual(aliceSecret.toString('hex'), bobSecret.toString('hex')); // OK
HMAC
Hash-based message authentication code (HMAC) enbles you to provide digital signatures with the use of shared secret. Crypto’s HMAC
class uses the HMAC method for digital signing.
const crypto = require('crypto'); const hmac = crypto.createHmac('sha256', 'a secret'); hmac.on('readable', () => { // Only one element is going to be produced by the // hash stream. const data = hmac.read(); if (data) { console.log(data.toString('hex')); // Prints: // 7fd04df92f636fd450bc841c9418e5825c17f33ad9c87c518115a45971f7f77e } }); hmac.write('some data to hash'); hmac.end();
sign
The sign
class is for generating signatures. For efficient cryptography, cryptographs need to be signed and later verified for authentication. This way, when the receiver gets a cryptograph, they can tell whether it is genuine by verifying the signature on it.
const crypto = require('crypto'); const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'sect239k1' }); // Create const sign = crypto.createSign('SHA256'); sign.write('some data to sign'); sign.end(); const signature = sign.sign(privateKey, 'hex');
verify
If you have a hashed cryptograph, the only way to ascertain its value is with the verify
method.
For instance, when you hash a user password and store it in your database during registration, you need to confirm the password/username entered by the user during login. Since we cannot decipher hashed passwords, the only way to confirm that the password/username combo is correct is with verify
.
// Verify signed token from `sign` example above const verify = crypto.createVerify('SHA256'); verify.write('some data to sign'); verify.end(); console.log(verify.verify(publicKey, signature, 'hex')); // Prints: true
To demonstrate how to encrypt and decrypt user information in a Node.js app using crypto, we’ll use a sample Node.js app in which users register with a username and password and then use those credentials to log in.
Let’s say that when the user registers, their details are stored in plain text, giving hackers easy access to steal their data. We’ll show you how to solve this problem by adding crypto to the application.
This application uses Passport for user authentication — in our case, to compare the user password/email combination.
First, download the sample Node.js application and clone it with this command:
git clone https://github.com/hannydevelop/Crypto.git
After cloning the application, navigate to the location of the application on your system with your terminal.
cd crypto
Install dependencies and start the application by running the command below in your terminal:
# Install dependencies npm install # Start application node index.js
Navigate to http://localhost:3000/register
in your web browser to view the application. If you register an account and use MongoDB Compass to view the database, you would see that the user password is in plain text.
We want to avoid this. To convert a password to a cryptograph, we can add crypto
to our application.
To add crypto to your Node.js application, follow the steps below.
// Import module into the application const crypto = require('crypto') // Creating salt for all users let salt = 'f844b09ff50c'
// Add this right above User.create(userData) // Hash user password and salt using 1000 iterations let hash = crypto.pbkdf2Sync(userData.password, salt, 1000, 64, `sha512`).toString(`hex`); userData.password = hash
usercontroller.js
should now look like this:
//import all dependencies required const express = require('express'); const cors = require('cors'); const crypto = require('crypto') //set variable users as expressRouter var users = express.Router(); //import user model var { User } = require('../models/User'); //protect route with cors users.use(cors()) // Creating salt for all users let salt = 'f844b09ff50c' // Handling user signup users.post('/register', (req, res) => { const userData = { //values should be those in the user model important username : req.body.username, password: req.body.password, first_name: req.body.first_name, last_name: req.body.last_name, } User.findOne({ //ensure username is unique, i.e the username is not already in the database username: req.body.username }) .then(user => { //if the username is unique if (!user) { let hash = crypto.pbkdf2Sync(userData.password, salt, 1000, 64, `sha512`).toString(`hex`); userData.password = hash //if the username is unique go ahead and create userData after hashing password and salt User.create(userData) .then(user => { //after successfully creating userData display registered message res.redirect('/login') }) .catch(err => { //if an error occured while trying to create userData, go ahead and display the error res.send('error:' + err) }) } else { //if the username is not unique, display that username is already registered with an account res.json({ error: 'The username ' + req.body.username + ' is registered with an account' }) } }) .catch(err => { //display error if an error occured res.send('error:' + err) }) })
//check to see if a username and password match like this is in the database username: req.body.username, password: crypto.pbkdf2Sync(req.body.password, salt, 1000, 64, `sha512`).toString(`hex`)
users.post('/login')
should now look like this:
users.post('/login', (req, res) => { User.findOne({ //check to see if a username and password match like this is in the database username: req.body.username, password: crypto.pbkdf2Sync(req.body.password, salt, 1000, 64, `sha512`).toString(`hex`) }) .then(user => { //if the username and password match exist in database then the user exists if (user) { const payload = { username: user.username, password: user.password } //after successful login display token and payload data res.redirect('/'); } else { //if user cannot be found, display the message below res.json({ error: 'user not found' }) } }) //catch and display any error that occurs while trying to login user .catch(err => { res.send('error:' + err) }) })
Now that we’ve added crypto’s hash method to our Node.js application, let’s run it and see the difference.
To run your Node.js application, run the command below in your terminal:
node index.js
Navigate to http://localhost:3000/register
to register and log into your application. This time, the snippet from Compass is different (it’s a cryptograph):
In this article, we demonstrated how to secure user data with the Node.js crypto module.
There are some other cryptography packages you can use Node.js, such as JWT, Bcrypt, and more. However, these packages are not built-in and sometimes require additional dependencies to do the job crypto can do on its own. For instance, if using Bcrypt, you would need to sign your keys with JWT.
crypto has a crypt and sign method that performs the same jobs as Bcrypt and JWT, respectively. However, if the application you’re building is solely for authenticating users and not ciphering messages, Bcrypt and JWT are better options. That’s because it’s not a good practice to use the same salt for all users; rather. you should create a unique salt for each user is a good practice this.salt = crypto.randomBytes(16).toString('hex');
.
This process comes with its own challenge: the ability to compare a hashed password with a user-entered password. Bcrypt’s compareSync()
method provides an easy means to compare hashed passwords and plain passwords and is, therefore, a better alternative.
Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third-party services are successful, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
5 Replies to "Node.js crypto module: A tutorial"
Would suggest changing your aes example to use aes-256 and the password example mention why a higher iteration count is important, and may want to switch to the async methods, especially for higher iterations and mention countermeasures as this can be a point for DDoS depending on configuration and implementation details..
Hello!
Thank you for the kind feedback. Although larger key sizes exist mostly to satisfy some US military regulations which require several distinct security levels, the larger key sizes imply some CPU overhead (+20% for a 192-bit key, +40% for a 256-bit key. This is why most applications use 192-bit key. Also, the reason why most people will use a higher iteration is to make it difficult for attackers to easily decipher passwords. Sure! It’ll be a better idea to apply asynchronous programming for higher iteration, otherwise synchronous method as applied here presents no delay.
Do well to reach out if you have further questions or suggestions. Thank you!
Hi Ukpai, I have not finished reading the full article, but isn’t it better, when authenticating a user, to encrypt the password they supply and compare that result with the stored (encrypted) value? Tebb
Hello Tebb!
This is what I tried to implement in the login method.
password: crypto.pbkdf2Sync(req.body.password, salt,
1000, 64, `sha512`).toString(`hex`)
Crypto doesn’t have a compare method like bcrypt. This is why developers opt for bcrypt whenever it involves ciphering login details
Built in methods blocks event loop and these are synchronous.
https://nodejs.org/en/docs/guides/dont-block-the-event-loop/#blocking-the-event-loop-node-js-core-modules