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.
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:
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
with
the hash data (Example 5-4). You can keep
updating a hash
.update()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
method. This will output the digest of the data that was input into the
hash with hash
.digest()
. No
more data can be added after you call hash
.update()
.hash
.digest()
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
, as in
Example 5-5.hash
.digest
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
again, we get an error. This is because once hash
.digest()
is
called, the hash
.digest()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
output are hash
.digest()binary
(default), hex
, and base64
.
Because data in
calls
is concatenated, the code samples in Example 5-6
are identical.hash
.update()
It is also important to know that although
looks
a lot like a stream, it isn’t really. You can easily hook a stream to
hash
.update()
, but
you can’t use hash
.update()stream.pipe()
.
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.
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).
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
.
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.
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:
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.
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
.
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.
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.