You've probably heard a lot about data leakage, malicious attacks, and especially hackers. Even the biggest tech companies are facing these problems nowadays. To mitigate this, it is necessary to use more effective tools to secure data. One such tool is the crypto module in Node.js. It provides a bunch of useful methods to secure data, therefore minimizing the risks that somebody gets hold of our private information. Let's take a look at the module and its core methods. You will learn about different classes present in the module, ways to implement hashing and ciphering, and see practical examples of where to use which method.
Cryptography in Node.js
Cryptography in Node.js is all about protecting personal data such as passwords, payment transactions, server communication, and other confidential info. In simple terms, cryptography transforms the original data into another value built up from random characters. This way, only the sender and the intended recipient of data will have access to the data, leaving third parties (hackers) empty-handed.
Imagine you are building a full-stack application with authorization. You need to save user information (email and password) into a database so that they can log in with their credentials. But what if somebody hacks your database and steals all the data? They'll be able to log in as original users and steal personal information, blackmail people, and commit unethical activities. Eventually, users will lose trust and no longer use your application. This is why cryptography is a must for every computer application or online service.
Cryptography consists of two main methods — hashing and ciphering (encryption). The methods are similar in their goal. Both methods convert a given string into a random and unreadable string. However, the difference is that hashing is a one-way method. So you can't revert the value back to the original readable text once you hash it. On the other hand, ciphering can encrypt data and then decrypt it if necessary. Another difference is that hashing outputs a fixed-length string, whereas ciphering can be of any length.
You can use hashing in authentication-based applications, i.e. through passwords, or e-commerce apps where you enter your credit card info. This info is turned into a hash digest and there's no way to revert it to the original plaintext. Whenever you enter your password, the server hashes it again and then compares it to the existing hashed password associated with your email. If they're equal, you're good to go. Otherwise, you won't be able to access the application.
Ciphering is good when two servers talk to each other — the first server sends some sensitive data by encrypting it and the second server has a key to decrypt it. This way servers can exchange data securely. Another example is messaging apps, where two people can see each other's messages in plain text format even though the application saves the messages as encrypted values in a database.
An overview of crypto classes
A few classes in the crypto module allow you to secure apps. These are Certificate, Cipher, Decipher, DiffieHellman, ECDH, Hash, Hmac, Sign, and Verify.
Let's briefly go over each class to get a general understanding of their function:
You can use
Certificateto create a session key to send data over the Internet securely. The class works with SPKAC (Signed Public Key and Challenge) format, which makes it easy to generate and manipulate a public key via OpenSSL.Cipheris a tool that encrypts a plaintext into a cipher using an algorithm. You can decrypt it back to plaintext using the opposite class calledDecipher.DiffieHellmanclass is responsible for exchanging a secret key between two parties in public channels. If you're curious and want to dive deeper, refer to an article on Diffie-Hellman key exchange.ECDHis a variation of the previous method that allows two parties to exchange a secret key with the help of a predefined curve.Use the
Hashclass to convert texts into hash. Keep in mind that they can't be reverted back.Hmacis an acronym for Hash-based Message Authentication Code. This class provides an encryption method that uses a hash function along with a secret key.
Hashing
You can generate a hash in three simple steps. First, decide on the hashing algorithm. Then add the text you want to hash and finally output it in a desirable format. You can choose from many algorithms, such as sha512, sha384, sm3, and more. You can import the crypto module and console log crypto.getHashes() to see all available algorithms. The final format can be binary, hex, base64, base64url. You can use any of those, though hex is more popular and convenient.
Take a look at this example:
const crypto = require('node:crypto');
const hashed = crypto.createHash('sm3')
.update('Hi folks!')
.digest('hex');
console.log(hashed);First, the code snippet imports the crypto module. Then it declares the hashed constant to save the result. The createHash() method returns a hash object with the specified algorithm (sm3). The algorithm is the base on which the hashed string is built. Then you pass the string you want to encode into the update method. Finally, it outputs the result in the hex format. If you print the hashed constant, you will get the following output:
6ad21a976304948ef4a2a9d6af7c88b069b20036a56d358a86e2cafe73ac472aThe code snippet uses chained code here, but you can do it differently. You can try saving the result of each step into a new constant.
HMAC
Imagine someone gets access to your code. They can easily figure out how to hack your database. Assume another scenario where you are building an app with another developer and you want your code to work only for the two of you. In this case, you need a secret key that you can share with your trusted peers.
HMAC (Hash-based Message Authentication Code) can do exactly that. It is similar to the previous example which uses crypto.createHash(), but has one small difference — it now requires a private key.
Here is how HMAC works:
const crypto = require('node:crypto');
const secret = 'shrek';
const hash1 = crypto.createHmac('sm3', secret)
.update('Hi folks!')
.digest('hex');
console.log(hash1);
// returns d751c2fae8264850797f7621cb8f2186bce3e453db8e824b02727959f7ca466d
const hash2 = crypto.createHmac('sm3', secret)
.update('Hi folks!')
.digest('hex');
console.log(hash2);
// returns d751c2fae8264850797f7621cb8f2186bce3e453db8e824b02727959f7ca466dThe name of the function is createHmac(). It takes in an algorithm and a key as parameters. The secret keys should be hidden and shared among trusted personnel only. The secret key is visible in this code snippet for demonstration purposes only.
What happens if we calculate the HMAC hash twice for the same input? Let's see:
As you can see, both hash1 and hash2 produce identical results. This demonstrates a key property of hash functions, including HMAC: for the same input and the same secret key, they always produce the same output.
This consistency is crucial for verifying data integrity and authenticity. However, it's important to note that this predictability can also be a weakness in certain scenarios. If hackers somehow figure out that a specific plaintext results in a given hash, it could potentially make it easier for them to decode other data.
You can use both crypto.createHash() and crypto.createHmac(). But ideally, you would want two unique hashes even when passing the same string ("Hi folks!" in this case). Now, let's learn about some advanced hashing!
Hashing with scrypt
The way to get a unique hash is to add salt to it. In simple terms, salt is the random data you add to the beginning of a given string. Usually, salt is different each time, and it's impossible to figure out where it starts and ends in the output. Just like with meals, when you add salt, you can no longer separate it from food. That's why salting is a good technique to secure data.
Here is a function that returns unique hashes. It uses crypto.scrypt() (pronounced "ess crypt") method:
const hashWithScrypt = (string) => {
const currentSalt = crypto.randomBytes(16);
crypto.scrypt(string, currentSalt, 32, (err, buffer) => {
if (err) {
console.log(err);
}
console.log(buffer.toString('hex'));
});
};The function accepts one parameter. It is the string you want to hash. Then it generates salt using crypto.randomBytes() method. The method takes in size in bytes; 16 in this case. You can specify a different value if you want. Also, you can run the method asynchronously. This way it takes two parameters — the size and the callback. For simplicity, this snippet works synchronously.
Now, let's talk about crypto.scrypt() method's arguments in detail:
string— the value you want to hashcurrentSalt— the saltlength— the length of the output in bytescallback— the function that runs when you successfully create a hash. It takeserras a parameter andbuffer.errlets you know when something goes wrong. Thebufferis the actual output.
Working with buffer isn't convenient, so the code converts it to hex using toString() method.
Let's check if you can get two unique hash values for identical strings:
hashWithScrypt('Hi folks!');
hashWithScrypt('Hi folks!');
// returns 1196f1f865941941c99d4cf9fd1ad01850aab299e7c5de65a30eca5d1e43ff9c
// returns ced4544c3d2e9d94cd56dfc21fc84d10a39a45e8c1b1637fa5f754b146d472fcIt returns two different values for an identical input string.
Ciphering
The crypto.createCipheriv() and crypto.createDecipheriv() are two methods with opposite functions. The first one creates a cipher method that you can use to encrypt messages, whereas the second one allows you to decrypt those messages. However, their syntax is similar.
Let's start with createCipheriv(). Just like with hashes, it takes three steps to encode a message.
Step 1: Create a cipher
crypto.createCipheriv(algorithm, key, iv, options)algorithm— a name of the ciphering algorithm. You can check the existing ones by console loggingcrypto.getCiphers()key— a secret key shared among the dev teamiv— initialization vector — a sort of salt that randomizes the outputoptions — additional properties you need to include when working withCCM,OCBalgorithms
Here's a visual representation of step one:
const iv = crypto.randomBytes(16);
const key = '-32bits--32bits--32bits--32bits-';
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);First, it generates iv (a random value of 16 bytes). Then it initializes a key that is 32 characters long (equivalent to 32 bytes). This is important because 256-bit algorithms only accept 32-byte keys (32 bytes = 256 bits). When you try to use any other length, the program throws an error. Finally, you make a cipher that takes in the algorithm, the key, and iv.
Step 2: Add a message
Now that you have a cipher, use it to add your message:
const updated = cipher.update('Hey beautiful people!', 'utf-8', 'hex');In this line of code, you pass the message to the update method along with the input encoding (utf-8) and output encoding (hex). If you don't specify the latter two parameters, you'll get buffer format by default.
Step 3: End the process
In the last step, you need to call the final method to let the program know that you want to finish the process. Once you call it, you will no longer be able to use the Cipher object to encrypt data:
const ciphered = updated + cipher.final('hex');The code line above concatenates the result of the previous step and the result of calling the final method. Here, the hex argument specifies the output encoding.
That's it! If you console log the ciphered constant, you'll see a ciphered string.
Now let's become hackers and try to decode the message. It's simple and takes three steps:
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
const updatedDecipher = decipher.update(ciphered, 'hex', 'utf-8');
const deciphered = updatedDecipher + decipher.final('utf-8');
console.log(deciphered);It looks similar to the previous method. The difference is that you are passing in the ciphered message instead of plain text and you are swapping the encodings — you are giving the hex encoding and awaiting the utf-8 format as a result. The key and iv are the same as you used in the initial encryption process above. You were able to decrypt the hashed message, which is:
Hey beautiful people!Conclusion
Cryptography is an interesting and useful field of study that helps you build more secure apps. In this topic, you read mainly about hashing and ciphering methods of the crypto module. Once you become comfortable using this module, you can look at some alternatives like bcrypt.