Crypto

Cryptography is used in lots of places for a variety of tasks. Node uses the OpenSSL library as the basis of its cryptography. This is because OpenSSL is already a well-tested, hardened implementation of cryptographic algorithms. But you have to compile Node with OpenSSL support in order to use the methods in this section.

The cryptograph module enables a number of different tasks. First, it powers the SSL/TLS parts of Node. Second, it contains hashing algorithms such as MD5 or SHA-1 that you might want to use in your application. Third, it allows you to use HMAC.[12] There are some encryption methods to cipher the data with to ensure it is encrypted. Finally, HMAC contains other public key cryptographic functions to sign data and verify signatures.

Each of the functions that cryptography does is contained within a class (or classes), which we’ll look at in the following sections.

Hashing

Hashes are used for a few important functions, such as obfuscating data in a way that allows it to be validated or providing a small checksum for a much larger piece of data. To use hashes in Node, you should create a Hash object using the factory method crypto.createHash(). This returns a new Hash instance using a specified hashing algorithm. Most popular algorithms are available. The exact ones depend on your version of OpenSSL, but common ones are:

  • md5

  • sha1

  • sha256

  • sha512

  • ripemd160

These algorithms all have different advantages and disadvantages. MD5, for example, is used in many applications but has a number of known flaws, including collision issues.[13] Depending on your application, you can pick either a widely deployed algorithm such as MD5 or (preferably) the newer SHA1, or a less universal but more hardened algorithm such as RIPEMD, SHA256, or SHA512.

Once you have data in the hash, you can use it to create a digest by calling hash.update() with the hash data (Example 5-4). You can keep updating a Hash with more data until you want to output it; the data you add to the hash is simply concatenated to the data passed in previous calls. To output the hash, call the hash.digest() method. This will output the digest of the data that was input into the hash with hash.update(). No more data can be added after you call hash.digest().

Example 5-4. Creating a digest using Hash

> var crypto = require('crypto');
> var md5 = crypto.createHash('md5');
> md5.update('foo');
{}
> md5.digest();
'¬½\u0018ÛLÂø\\íïeOÌĤØ'
>

Notice that the output of the digest is a bit weird. That’s because it’s the binary representation. More commonly, a digest is printed in hex. We can do that by adding 'hex' as a parameter to hash.digest, as in Example 5-5.

Example 5-5. The lifespan of hashes and getting hex output

> var md5 = crypto.createHash('md5');
> md5.update('foo');
{}
> md5.digest();
'¬½\u0018ÛLÂø\\íïeOÌĤØ'
> md5.digest('hex');
Error: Not initialized
    at [object Context]:1:5
    at Interface.<anonymous> (repl.js:147:22)
    at Interface.emit (events.js:42:17)
    at Interface._onLine (readline.js:132:10)
    at Interface._line (readline.js:387:8)
    at Interface._ttyWrite (readline.js:564:14)
    at ReadStream.<anonymous> (readline.js:52:12)
    at ReadStream.emit (events.js:59:20)
    at ReadStream._emitKey (tty_posix.js:280:10)
    at ReadStream.onData (tty_posix.js:43:12)
> var md5 = crypto.createHash('md5');
> md5.update('foo');
{}
> md5.digest('hex');
'acbd18db4cc2f85cedef654fccc4a4d8'
>

When we call hash.digest() again, we get an error. This is because once hash.digest() is called, the Hash object is finalized and cannot be reused. We need to create a new instance of Hash and use that instead. This time we get the hex output that is often more useful. The options for hash.digest() output are binary (default), hex, and base64.

Because data in hash.update() calls is concatenated, the code samples in Example 5-6 are identical.

Example 5-6. Looking at how hash.update( ) concatenates input

> var sha1 = crypto.createHash('sha1');
> sha1.update('foo');
{}
> sha1.update('bar');
{}
> sha1.digest('hex');
'8843d7f92416211de9ebb963ff4ce28125932878'
> var sha1 = crypto.createHash('sha1');
> sha1.update('foobar');
{}
> sha1.digest('hex');
'8843d7f92416211de9ebb963ff4ce28125932878'
>

It is also important to know that although hash.update() looks a lot like a stream, it isn’t really. You can easily hook a stream to hash.update(), but you can’t use stream.pipe().

HMAC

HMAC combines the hashing algorithms with a cryptographic key in order to stop a number of attacks on the integrity of the signature. This means that HMAC uses both a hashing algorithm (such as the ones discussed in the previous section) and an encryption key. The HMAC API in Node is virtually identical to the Hash API. The only difference is that the creation of an hmac object requires a key as well as a hash algorithm.

crypto.createHmac() returns an instance of Hmac, which offers update() and digest() methods that work identically to the Hash methods we saw in the previous section.

The key required to create an Hmac object is a PEM-encoded key, passed as a string. As shown in Example 5-7, it is easy to create a key on the command line using OpenSSL.

Example 5-7. Creating a PEM-encoded key

Enki:~ $ openssl genrsa -out key.pem 1024
Generating RSA private key, 1024 bit long modulus
...++++++
............................++++++
e is 65537 (0x10001)
Enki:~ $

This example creates an RSA in PEM format and puts it into a file, in this case called key.pem. We also could have called the same functionality directly from Node using the process module (discussed later in this chapter) if we omitted the -out key.pem option; with this approach, we would get the results on an stdout stream. Instead we are going to import the key from the file and use it to create an Hmac object and a digest (Example 5-8).

Example 5-8. Creating an Hmac digest

> var crypto = require('crypto');
> var fs = require('fs');
>
> var pem = fs.readFileSync('key.pem');
> var key = pem.toString('ascii');
> 
> var hmac = crypto.createHmac('sha1', key);
> 
> hmac.update('foo');
{}
> hmac.digest('hex');
'7b058f2f33ca28da3ff3c6506c978825718c7d42'
>

This example uses fs.readFileSync() because a lot of the time, loading keys will be a server setup task. As such, it’s fine to load the keys synchronously (which might slow down server startup time) because you aren’t serving clients yet, so blocking the event loop is OK. In general, other than the use of the encryption key, using an Hmac example is exactly like using a Hash.

Public Key Cryptography

The public key cryptography functions are split into four classes: Cipher, Decipher, Sign, and Verify. Like all the other classes in crypto, they have factory methods. Cipher encrypts data, Decipher decrypts data, Sign creates a cryptographic signature for data, and Verify validates cryptographic signatures.

For the HMAC operations, we used a private key. For the operations in this section, we are going to use both the public and private keys. Public key cryptography has matched sets of keys. One, the private key, is kept by the owner and is used to decrypt and sign data. The other, the public key, is made available to other parties. The public key can be used to encrypt data that only the private key owner can read, or to verify the signature of data signed with the private key.

Let’s extract the public key of the private key we generated to do the HMAC digests (Example 5-9). Node expects public keys in certificate format, which requires you to input additional “information.” But you can leave all the information blank if you like.

Example 5-9. Extracting a public key certificate from a private key

Enki:~ $ openssl req -key key.pem -new -x509 -out cert.pem 
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgets Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (eg, YOUR name) []:
Email Address []:
Enki:~ $ ls cert.pem
cert.pem
Enki:~ $

We simply ask OpenSSL to read in the private key, and then output the public key into a new file called cert.pem in X509 certificate format. All of the operations in crypto expect keys in PEM format.

Encrypting with Cipher

The Cipher class provides a wrapper for encrypting data using a private key. The factory method to create a cipher takes an algorithm and the private key. The algorithms supported come from those compiled into your OpenSSL implementation:

  • blowfish

  • aes192

Many modern cryptographic algorithms use block ciphers. This means that the output is always in standard-size “blocks.” The block sizes vary between algorithms: blowfish, for example, uses 40-byte blocks. This is significant when you are using the Cipher API because the API will always output fixed-size blocks. This helps prevent information from being leaked to an attacker about the data being encrypted or the specific key being used to do the encryption.

Like Hash and Hmac, the Cipher API also uses the update() method to input data. However, update() works differently when used in a cipher. First, cipher.update() returns a block of encrypted data if it can. This is where block size becomes important. If the amount of data in the cipher plus the amount of data passed to cipher.update() is enough to create one or more blocks, the encrypted data will be returned. If there isn’t enough to form a block, the input will be stored in the cipher. Cipher also has a new method, cipher.final(), which replaces the digest() method. When cipher.final() is called, any remaining data in the cipher will be returned encrypted, but with enough padding to make sure the block size is reached (see Example 5-10).

Example 5-10. Ciphers and block size

> var crypto = require('crypto');
> var fs = require('fs');
>
> var pem = fs.readFileSync('key.pem');
> var key = pem.toString('ascii');
>
> var cipher = crypto.createCipher('blowfish', key);
> 
> cipher.update(new Buffer(4), 'binary', 'hex');
''
> cipher.update(new Buffer(4), 'binary', 'hex');
'ff57e5f742689c85'
> cipher.update(new Buffer(4), 'binary', 'hex');
''
> cipher.final('hex')
'96576b47fe130547'
>

To make the example easier to read, we specified the input and output formats. The input and output formats are both optional and will be assumed to be binary unless specified. For this example, we specified a binary input format because we’re passing a new Buffer (containing whatever random junk was in memory), along with hex output to produce something easier to read. You can see that the first time we call cipher.update(), with 4 bytes of data, we get back an empty string. The second time, because we have enough data to generate a block, we get the encrypted data back as hex. When we call cipher.final(), there isn’t enough data to create a full block, so the output is padded and a full (and final) block is returned. If we sent more data than would fit in a single block, cipher.final() would output as many blocks as it could before padding. Because cipher.final() is just for outputting existing data, it doesn’t accept an input format.

Decrypting with Decipher

The Decipher class is almost the exact inverse of the Cipher class. You can pass encrypted data to a Decipher object using decipher.update(), and it will stream the data into blocks until it can output the unencrypted data. You might think that since cipher.update() and cipher.final() always give fixed-length blocks, you would have to give perfect blocks to Decipher, but luckily it will buffer the data. Thus, you can pass it data you got off some other I/O transport, such as the disk or network, even though this might give you block sizes different from those used by the encryption algorithm.

Let’s take a look at Example 5-11, which demonstrates encrypting data and then decrypting it.

Example 5-11. Encrypting and decrypting text

> var crypto = require('crypto');
> var fs = require('fs');
> 
> var pem = fs.readFileSync('key.pem');
> var key = pem.toString('ascii');
> 
> var plaintext = new Buffer('abcdefghijklmnopqrstuv');
> var encrypted = "";
> var cipher = crypto.createCipher('blowfish', key);
> ..
> encrypted += cipher.update(plaintext, 'binary', 'hex');
> encrypted += cipher.final('hex');
> 
> var decrypted = "";
> var decipher = crypto.createDecipher('blowfish', key);
> decrypted += decipher.update(encrypted, 'hex', 'binary');
> decrypted += decipher.final('binary');
> 
> var output = new Buffer(decrypted);
>
> output
<Buffer 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76>
> plaintext
<Buffer 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76>
>

It is important to make sure both the input and output formats match up for both the plain text and the encrypted data. It’s also worth noting that in order to get a Buffer, you’ll have to make one from the strings returned by Cipher and Decipher.

Creating signatures using Sign

Signatures verify that some data has been authenticated by the signer using the private key. However, unlike with HMAC, the public key can be used to authenticate the signature. The API for Sign is nearly identical to that for HMAC (see Example 5-12). crypto.createSign() is used to make a sign object. createSign() takes only the signing algorithm. sign.update() allows you to add data to the sign object. When you want to create the signature, call sign.sign() with a private key to sign the data.

Example 5-12. Signing data with Sign

> var sign = crypto.createSign('RSA-SHA256');
> sign.update('abcdef');
{}
> sig = sign.sign(key, 'hex');
'35eb47af5260a00c7bad26edfbe7732a897a3a03290963e3d17f48331a42...aa81b'
>

Verifying signatures with Verify

The Verify API uses a method like the ones we’ve just discussed (see Example 5-13), verify.update(), to add data—and when you have added all the data to be verified against the signature, verify.verify() validates the signature. It takes the cert (the public key), the signature, and the format of the signature.

Example 5-13. Verifying signatures

> var crypto = require('crypto');
> var fs = require('fs');
> 
> var privatePem = fs.readFileSync('key.pem');
> var publicPem = fs.readFileSync('cert.pem');
> var key = privatePem.toString();
> var pubkey = publicPem.toString();
> 
> var data = "abcdef"
> 
> var sign = crypto.createSign('RSA-SHA256');
> sign.update(data);
{}
> var sig = sign.sign(key, 'hex');
> 
> var verify = crypto.createVerify('RSA-SHA256');
> verify.update(data);
{}
> verify.verify(pubkey, sig, 'hex');
1


[12] Hash-based Message Authentication Code (HMAC) is a crytographic way of verifying data. It is often used like hashing algorithms to verify that two pieces of data match, but it also verifies that the data hasn’t been tampered with.

[13] It’s possible to deliberately make two pieces of data with the same MD5 checksum, which for some purposes can make the algorithm less desirable. More modern algorithms are less prone to this, although people are finding similar problems with SHA1 now.

Get Node: Up and Running now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.