Thursday, May 05, 2022

Introduction to Authenticated Encryption for the Working Developer

In Introduction to Symmetric Encryption for the Working Developer, I presented an overview of symmetric encryption. Which as explained in the post, is a process for establishing confidentiality during communication between two parties.

The only problem is that just encrypting data using symmetric encryption does not provide all the security requirements needed during secure communication.

The issue is that, it is still possible for an eavesdropper to tamper with the message even without being able to make sense of the ciphertexts, and the receiving party won't be made aware that the message they are decrypting has been tampered with and is not exactly the original one that was sent.

Hence even though encryption gives us confidentiality, done alone, it does not guarantee the other security requirements like authenticity/integrity that is often needed - i.e. it does not guarantee that a message came from the right party (authenticity) and it has not been modified in any way (integrity).

Encryption alone is not enough, we need authenticated encryption. This post will introduce the concept of authenticated encryption.

What is Authenticated Encryption?

In Hash Function in Action: Message Authentication Codes a similar argument was made regarding hash functions. Where it was stated that hash functions are never enough and there is the need for message authentication to also ensure things like authenticity and integrity.

How then can authenticated encryption be achieved? Do we use a similar approach that was used in message authentication?

Well, an approach to achieving authenticated encryption is to take the output of encryption and then apply message authentication code to it.

For example, using HMAC to create an authentication tag (hash/digest of the cipher text)). Both the ciphertext and the authentication tag is then sent during communication.

In general such manual constructions are referred to as EtM (Encrypt-then-MAC) and a specific construction of it would be AES-CBC-HMAC. Which is a construction where you encrypt using AES-CBC and then create an authentication tag using HMAC.

The problem with this approach is that it is error prone. It is fickle and possible to make simple errors that lead to tremendous security issues.

For example one needs to ensure that the same key is not used for both encryption and generating the HMAC.

Also special care needs to be taken that the IV values used for the AES are truly random and never reused. Also because two separate cryptographic primitives need to be separately implemented, it does not make for the most efficient approach.

Because of these problems with manually constructed EtM schemes, it is not recommended to manually construct authenticated encryptions. Instead the safe advice is to make use of all-in-one constructions that simplify the process of having authenticated encryption.

This is what is discussed in the next section.

All-in one Authenticated Encryption Constructions and Associated Data

All-in-one authenticated encryptions can either be based on Advanced Encryption Scheme (AES) or using another algorithm apart from AES.

The all-in-one construction for achieving authenticated encryption that builds on AES does so by having an AES mode of operations that include authentication. For more about modes of operation, see Introduction to Symmetric Encryption for the Working Developer.

They do so in such a way as to hide the complexity and the various moving parts, presenting a much simpler interface to developers.

These all-in-one constructs also provide the ability to optionally attach some metadata with the ciphertext, if that is the case, then they are called Authenticated Encryption with Associated Data (AEAD).

An example of such a mode that supports both authentication with the ability to attach associated data is Galois/counter mode (GCM).

The associated data that is attached is unencrypted.

What then is the use you might ask?

The associated data is for scenarios where you have data that does not need to be hidden, but must go along with some other data that needs to be hidden.

An example of such a scenario is a network packet, where the body which contains the data should be encrypted, while the headers, which do not contain any sensitive data and are used by network equipment like routers, can remain in plaintext. But even though the headers are in plaintext, each header needs to be associated with the encrypted data it belongs with. Using AEAD, the header can be the associated data. This means that the same associated data used during encryption, needs to be provided on decryption. If the associated data has been tampered with, the decryption process will fail.

The associated data is a flexible component of AEAD, and can be used for anything. The general idea is that it provides some sort of encryption context. This encryption context can be whatever that is required for it to be.

There are other modes of operation that can be used with AES that are AEAD. This Wikipedia link provides an overview of some of this mode.

There is also ChaCha20-Poly1305 which is an AEAD. It is a separate encryption scheme and not just a mode of operation of AES.

Even though ChaCha20-Poly1305 is newer than AES and not a standard, it has grown to be a solid scheme and a popular alternative to AES.

The rest of this post looks at AES-GCM and ChaCha20-Poly1305.

AES-GCM Authenticated Encryption and Associated Data.

AES-GCM is an AEAD. It is constructed from AES used with the Galois/Counter Mode. By it being an AEAD, it means it provides authenticated encryption out of the box (the AE part in AEAD), with the ability to add associated data (the ED part in AEAD).

The Web Crypto API supports AES-GCM, hence can be used within a browser or Deno. Using it is similar to the code snippet presented in Introduction to Symmetric Encryption for the Working Developer. The only difference is, instead of passing in "AES-CBC" and the algorithm identifier, the string "AES-GCM" is used instead. The updated code is reproduced below. It also makes use of associated data.

// We generate a key
// See https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/generateKey
let key = await crypto.subtle.generateKey({
   name: 'AES-GCM',
   length: 128
}, false, ['encrypt', 'decrypt']);

// We generate random 16 bytes to be used as initialization vector
let iv = new Uint8Array(16);
// and fill it with random numbers
// see https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues
await crypto.getRandomValues(iv);

// The web crypto requires data to be in ArrayBuffer
let plaintext = new TextEncoder().encode("hello world");

// The encryption and decryption requires specifying certain parameters:
// the algorithm and initialization vector, associated data etc
// see https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams
let associatedData = new TextEncoder("utf-8").encode("Greeting");

let param = {
   name: 'AES-GCM',
   iv,
   associatedData,
};

// We encrypt the plaintext to get the ciphertext
let ciphertext = await crypto.subtle.encrypt(param, key, plaintext);

// We decode and console logged the ciphertext
console.log(new TextDecoder("utf-8").decode(ciphertext))

// We decrypt the ciphertext back to plaintext
let deCryptedPlaintext = await window.crypto.subtle.decrypt(param, key, ciphertext);

// We tranform the plaintext from bytes to string and then print it out
console.log(new TextDecoder("utf-8").decode(deCryptedPlaintext));

// Causing decryption to fail by providing different associated data

// Create another associated data and param
let wrongAssociatedData = new TextEncoder("utf-8").encode("Payment");

let wrongParam = {
   name: 'AES-GCM',
   iv,
   wrongAssociatedData,
};


// Decryption would fail because the associated data used for decryption "Payment"
// is different from one used for encryption "Greeting"
await window.crypto.subtle.decrypt(wrongParam, key, ciphertext);

The code above makes use of AES-GCM as the AEAD. It provided an associated data. It demonstrates encryption and decryption. It also shows that the decryption will fail, if a different associated data, from the one used at encryption is attempted for decryption.

ChaCha20-Poly1305

Unlike AES-GCM, (which is an AES), ChaCha20-Poly1305 is an entirely different encryption scheme designed by Daniel J. Bernstein. It is really a combination of a stream cipher (ChaCha20) and a MAC (Poly1305).

Even though ChaCha20-Poly1305 is not standardised by NIST, it has been specified as an RFC by Google in rfc7539. It has also seen massive adoption in the industry, being supported in protocols like OpenSSH, TLS etc. Hence it is a battle tested encryption scheme and can be safely used.

As of this writing, Web Crypto API does not support ChaCha20-Poly1305, so by extension is not available in a browser environment or Deno.

On the other hand, ChaCha20-Poly1305 can be used from Node since the Node crypto API depends on OpenSSL. So if you have the version of OpenSSL that supports ChaCha20-Poly1305, it can be used via Node.

The code snippet below shows how to use ChaCha20-Poly1305 in Node.

const { randomBytes, createDecipheriv, createCipheriv } = await import('node:crypto');

// chacha20-poly1305 requires 256 bit (32 byte) as key 
const key = randomBytes(32);
// Also a 96 bit iv/nonce
const iv = randomBytes(12);;
// create some associated data
let aad = Buffer.from("Greeting")

// create the cipher
let cipher = createCipheriv('chacha20-poly1305', key, iv, { authTagLength: 16 });

// Add the associated data to the cipher
cipher.setAAD(aad, {
  plaintextLength: Buffer.byteLength(plaintext)
})

// Data to encrypt
let plaintext = "Hello world";

// Encrypt and finalize encryption
const ciphertext = cipher.update(plaintext);
cipher.final()

// Console log the cipher text in hex
console.log("ciphertext:", ciphertext.toString('hex'))

// Decrytpion

// Create the decipher using exactly same parameters used to create the cipher
let decipher = createDecipheriv('chacha20-poly1305', key, iv, { authTagLength: 16 });
// Get the auth tag from the cipher and set it on the decipher
// This ensures the authentication done is done on decryption
decipher.setAuthTag(cipher.getAuthTag())
// We set the same associated data used in encryption
decipher.setAAD(aad);
// We start the decryption
let decrypted = decipher.update(ciphertext)
// We finalise the decryption and save the plaintext in a variable
decrypted = Buffer.concat([decrypted, decipher.final()])
// Print out the plaintext
console.log("plaintext:", decrypted.toString())

console.log("Encryption then Decryption Works", plaintext.toString() === decrypted.toString());

// Causing decryption to fail by providing different associated data

// Create the decipher using exactly same parameters used to create the cipher
let wrongDecipher = createDecipheriv('chacha20-poly1305', key, iv, { authTagLength: 16 });
// Get the auth tag from the cipher and set it on the decipher
// This ensures the authentication done is done on decryption
wrongDecipher.setAuthTag(cipher.getAuthTag())
// We set the another associated data, that differs from one used in encryption
let wrongAssocData = Buffer.from('Payment');

// We set the wrong associated data
wrongDecipher.setAAD(wrongAssocData, {
  plaintextLength: ciphertext.length
});

// We start the decryption
let wrongDecrypted = wrongDecipher.update(ciphertext)
// We finalise the decryption and save the plaintext in a variable
try {
  // This leads to exception
  wrongDecipher.final();
} catch (err) {
  throw new Error('Authentication failed!', { cause: err });
}

The code above makes use of chacha20-poly1305 as the AEAD. It provided an associated data. It demonstrates encryption and decryption. It also shows that the decryption will fail, if a different associated data, from the one used at encryption is attempted for decryption.


No comments: