Since Node v11.2.0 was released we can now use ChaCha20-Poly1305 as an AEAD cipher! However, if you search for ChaCha20 on the crypto documentation page, you will find nothing. This article is a quick guide on how to use chacha20-poly1305 AEAD and how to use chacha20 cipher streams. This article is not a discussion on best practices for using this cipher, so be mindful.

At the time of this writing, all active releases of Node.js support atleast OpenSSL 1.1.1. You can check the supported ciphers available in Node.js by running:

$ node
Welcome to Node.js v12.10.0.
> crypto.getCiphers()
[
  'aes-128-cbc',
  'aes-128-cbc-hmac-sha1',
  'aes-128-cbc-hmac-sha256',
  'aes-128-ccm',
  'aes-128-cfb',
  'aes-128-cfb1',
  'aes-128-cfb8',
  'aes-128-ctr',
  'aes-128-ecb',
  'aes-128-gcm',
  'aes-128-ocb',
  'aes-128-ofb',
  'aes-128-xts',
  ...

Somewhere in that list we can see the ChaCha20 support:

> crypto.getCiphers().filter(p => p.startsWith('chacha'))
[ 'chacha20', 'chacha20-poly1305' ]

However, just because it's in OpenSSL doesn't mean it's supported by Node.js. chacha20-poly1305 is only supported in Node v11.2.0+.

ChaCha20-Poly1305 AEAD

Encrypting data requires:

  • a 96-bit nonce
  • 256-bit key
  • any length associated data
  • any length data to encrypt

The steps involved are:

  1. creating the cipher using createCipheriv
  2. optionally adding the associated data
  3. adding data to the encryption stream
  4. finalizing the encryption
  5. obtain the authorization tag
// example 96-bit nonce
let nonce = Buffer.alloc(12, 0xff);

// example 256-bit key
let key = Buffer.alloc(32, 0x01);

// some associated data 
let assocData = Buffer.alloc(16, 0xaa);

// some data to encrypt
let data = Buffer.alloc(64, 0xbb);

// construct the cipher
let cipher = crypto.createCipheriv('chacha20-poly1305', key, nonce, { authTagLength: 16 });

// add associated data to cipher
cipher.setAAD(assocData);

// encrypt the data which will return an encrypted Buffer
// that is of equal length to the overall input to the
// stream cipher
cipher.update(data);
// 25805b670d5834ecb8a018ea87b6ff864117762481880fc723690d0e2d0cfd08a43c144291eb2df148b0d6981b66ca101344ea27c7a0860c2e5f1a7eed1e70eb

// finalize cipher which allows us to calculate the MAC
cipher.final();

// obtain the 128-bit poly1305 MAC that includes our associated data
cipher.getAuthTag()
// 9ef622cec7a5719261031e9ca91049d4

Decrypting is similar and has the following steps:

  1. create the decipher object usig createDecipheriv
  2. optionally set the associated data
  3. update the decipher stream with the ciphertext
  4. set the authorization tag
  5. finalize the decipher, which will validate the authorization tag for the associated data and the encrypted stream
// same 96-bit nonce
let nonce = Buffer.alloc(12, 0xff);

// same 256-bit key
let key = Buffer.alloc(32, 0x01);

// same associated data
let assocData = Buffer.alloc(16, 0xaa);

// auth tag from encryption output
let authTag = Buffer.from("9ef622cec7a5719261031e9ca91049d4", "hex");

// cipher data from encryption output
let cipherData = Buffer.from("25805b670d5834ecb8a018ea87b6ff864117762481880fc723690d0e2d0cfd08a43c144291eb2df148b0d6981b66ca101344ea27c7a0860c2e5f1a7eed1e70eb", "hex");

// create decipher
let decipher = crypto.createDecipheriv("chacha20-poly1305", key, nonce, { authTagLength: 16 });

// set associated data
decipher.setAAD(assocData);

// decrypt data, which should be the initial data
decipher.update(cipherData);
// bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb

// set the auth tag received during encryption
decipher.setAuthTag(authTag);

// validate the MAC is ok => this will throw an
// exception if the cipher, assoc data, or auth tag are
// inaccurate
decipher.final();

There you have it.

ChaCha20 Stream Cipher

You can also use plain chacha20 without the poly1305 MAC. Instead of creating the cipher as chacha20-poly1305 use chacha20. You will also need to use a 128-bit nonce instead of a 96-bit nonce.

// example 128-bit nonce
let nonce = Buffer.alloc(16, 0xff);

// example 256-bit key
let key = Buffer.alloc(32, 0x01);

// some data to encrypt
let data = Buffer.alloc(64, 0xbb);

// construct the cipher
let cipher = crypto.createCipheriv('chacha20', key, nonce);

// encrypt the data which will return an encrypted Buffer
// that is of equal length to the overall input to the
// stream cipher
cipher.update(data);
// 0f4ea69574568fe3f49ba430d871111540dd87d61ae0f9a2b7dc9f2381f8eefaac02ce2958f21537304b0ecb109c1c1c35ea60e3888ed6f11295d65a32b017c9

// finalize the cipher
cipher.final();

You can then decrypt using:

// same 128-bit nonce
let nonce = Buffer.alloc(16, 0xff);

// same 256-bit key
let key = Buffer.alloc(32, 0x01);

// cipher text
let cipher = Buffer.from("0f4ea69574568fe3f49ba430d871111540dd87d61ae0f9a2b7dc9f2381f8eefaac02ce2958f21537304b0ecb109c1c1c35ea60e3888ed6f11295d65a32b017c9", "hex");

// construct the decipher
let decipher = crypto.createDecipheriv('chacha20', key, nonce);

// decrypt the cipher text 
decipher.update(cipher).toString("hex");
// bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb

// finalize
decipher.final();

There you have it!