random thoughts a journey to nowhere

Symmetric Encryption With ChaCha20Poly1305

Table Of Contents

This post is not completed yet

Recently, I’ve started learning cryptography and in general different cryptographic concepts. So this blog post is a documentation of the process. I’ve linked a book where you’ll be able to find most of it in great detail.

First, I want to recommend this book to any software developer. If you haven’t read this book yet, I highly recommend reading it. It contains most of the commonly used cryptographic concepts which I believe enable any developer to write secure code in any domain.

In this note, I’ll try to explain Symmetric Encryption with ChaCha20 and Poly1305. We’ll build up the required knowledge to understand and implement Symmetric Encryption in golang. I’ve chosen golang mostly because I’ve been working with golang for quite some time and I know its crypto library. golang is easy to understand. I think you won’t have any problems reading the code. With that being said let’s dive into the madness.

In future posts, we’ll be looking into more topics related to this, like key derivation, MAC and so on.

Symmetric Encryption

Before going any further we need to understand what Symmetric Encryption is. It’s a process where encryption and decryption are performed with one single key. So we’ll be using a key to encrypt as well as decrypt a message.

ChaCha20

Now that we understand what Symmetric Encryption is, let’s see how can we perform this in our case. We’ll be using ChaCha20 stream cipher.

ChaCha20 is the succsessor of Salsa20. Both are designed by djb (Daniel J. Bernstein) in 2005. This guy also designed Poly1305, Curve25519 and so on. You can see this guy has quite some impact on current crypto world!

Encryption

Let’s dive into code. The interesting thing is golang library does all the heavy lifting and we need to do very less. In a future post, we’ll separate chacha20 from poly1305 and will see how these two fit together.

First, we need a key or password which we’ll use to encrypt our message. The key must be 32 bytes long. We’ll see KDF in the next post where we’ll develop a process to derive 32 bytes of data from an arbitrary-sized password.

Okay, enough talking. Let’s see which API we’re going to use. The interface we’ll work with is AEAD

Two functions we’re interested in from the AEAD interface,

AEAD definition from wikipedia: Authenticated Encryption (AE) and Authenticated Encryption with Associated Data (AEAD) are forms of encryption which simultaneously assure the confidentiality and authenticity of data.

type AEAD interface {
	// skipped other methods...

	// Seal encrypts and authenticates plaintext, authenticates the
	// additional data and appends the result to dst, returning the updated
	// slice. The nonce must be NonceSize() bytes long and unique for all
	// time, for a given key.
	Seal(dst, nonce, plaintext, additionalData []byte) []byte

	// Open decrypts and authenticates ciphertext, authenticates the
	// additional data and, if successful, appends the resulting plaintext
	// to dst, returning the updated slice. The nonce must be NonceSize()
	// bytes long and both it and the additional data must match the
	// value passed to Seal.
	Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error)
}

We’ll use Seal to encrypt the plaintext to ciphertext and Open to decrypt the ciphertext back to plaintext.

First, we need to create the chacha20poly1305 object which implements the AEAD interface. If we wrap the idea into a function named encrypt then the function should look like this.

func encrypt(key, nonce, plaintext []byte) ([]byte, error) {
	aead, err := chacha20poly1305.New(key)
	if err != nil {
		return nil, err
	}
	return aead.Seal(nil, nonce, plaintext, nil), nil
}

encrypt takes 3 bytes slices. The first one is the key, we already know that it needs to be exactly 32 bytes long. The second one is nonce. A nonce in chacha20 is 12 bytes of data. There’s another variant of chacha20 which takes 24 bytes nonce. That’s called XChaCha20. The idea here is one shouldn’t encrypt 2 messages with the same key and the same nonce. So we should use one nonce to encrypt one message. The third and final parameter is plaintext. Here the plaintext is a byte slice. Byte slice allows us to encrypt any data.

Cipher Text Size

If we run the encrypt function on plaintext Hello, World! then we’ll get 29 bytes of encrypted data. Where the plain text length is only 13. This is because the Seal function adds extra 16 bytes of data at the end, which is the Poly1305 MAC. When we decrypt we’ll get some data, but how do we know that this is the correct data. That is where MAC comes into the picture.

Decryption

The decrypt function is quite similar to the encrypt one.

func decrypt(key, nonce, ciphertext []byte) ([]byte, error) {
	aead, err := chacha20poly1305.New(key)
	if err != nil {
		return nil, err
	}
	return aead.Open(nil, nonce, ciphertext, nil)
}

Similarly, we can use the decrypt function. And we’ll get our original data back.

What if you use the wrong key? You’ll get an authentication failure error.

You can play with the code in GoPlayground.

Where to put the nonce?

We can keep the nonce with the data. Like the MAC. Then the ciphertext slice will look like this

// -------------------------------------------------------
// | 12 bytes nonce | ciphertext | 16 bytes Poly1305 MAC |
// -------------------------------------------------------

To use this pattern we can directly look at the example from chacha20poly1305 package. You can play with it here in GoPlayground.

The Full Code

package main

import (
	"bytes"
	"crypto/rand"
	"encoding/hex"
	"errors"
	"fmt"

	"golang.org/x/crypto/chacha20poly1305"
)

func decrypt(key, ciphertext []byte) ([]byte, error) {
	aead, err := chacha20poly1305.New(key)
	if err != nil {
		return nil, err
	}
	if len(ciphertext) < aead.NonceSize() {
		return nil, errors.New("ciphertext too short")
	}

	// Split nonce and ciphertext.
	nonce, ciphertext := ciphertext[:aead.NonceSize()], ciphertext[aead.NonceSize():]

	// Decrypt the message and check it wasn't tampered with.
	return aead.Open(nil, nonce, ciphertext, nil)
}

func encrypt(key, plaintext []byte) ([]byte, error) {
	aead, err := chacha20poly1305.New(key)
	if err != nil {
		return nil, err
	}

	totalLen := aead.NonceSize() + len(plaintext) + aead.Overhead()
	nonce := make([]byte, aead.NonceSize(), totalLen)
	if _, err := rand.Read(nonce); err != nil {
		return nil, err
	}

	// Encrypt the message and append the ciphertext to the nonce.
	return aead.Seal(nonce, nonce, plaintext, nil), nil
}

func main() {
	plaintext := []byte("Hello, World!")
	key := hex2bytes("a5ad47647a055644c4d86c840c60c51f7955a3fc9a5cc85acee0e82b808beb64")
	ciphertext, err := encrypt(key, plaintext)
	if err != nil {
		panic(err)
	}
	fmt.Println("encrypted:", bytes2hex(ciphertext))

	decrypted, err := decrypt(key, ciphertext)
	if err != nil {
		panic(err)
	}
	fmt.Println("decrytped:", string(decrypted))

	// fmt.Println("plain text len:", len(plaintext))
	// fmt.Println("ciphertext len:", len(ciphertext))

	// If we use wrong password
	wrongKey := bytes.Repeat([]byte{'b'}, 32)
	decrypted, err = decrypt(wrongKey, ciphertext)
	if err != nil {
		fmt.Println("Err: Wrong Key: ", err)
	}
	// You'll get error: chacha20poly1305: message authentication failed
	fmt.Println("decrytped:", bytes2hex(decrypted))
}

func bytes2hex(b []byte) string {
	return hex.EncodeToString(b)
}

func hex2bytes(hexStr string) []byte {
	b, err := hex.DecodeString(hexStr)
	if err != nil {
		panic(err)
	}
	return b
}

Variable Sized Password

Here we’ve used 32 bytes fixed-sized keys. But that’s not practical in real life. We want to support variable-length passwords so that users can insert passwords of any length and the encryption still works.

comments powered by Disqus