Cryptography in Go: AES encryption

Cryptography in Go: AES encryption

There are two kinds of cryptography in this world: cryptography that will stop your kid sister from reading your files, and cryptography that will stop major governments from reading your files.
—Bruce Schneier, “Applied Cryptography”

This is the third in a three-part series about cryptography in Go using the AES cipher.

  1. AES explained
  2. AES implementation
  3. AES encryption

AES is an immensely strong cipher, one of the most effective ways of keeping secrets ever invented. In this series, excerpted from my book Explore Go: Cryptography, we’ve explored how AES works under the hood, and the elegant implementation of AES that’s included in the Go standard library.

In this final post of the series, let’s see what it all means for us as working developers, and how to use the crypto/aes package to encrypt and decrypt secrets using Go programs.

Cipher modes: ECB and CBC

In Explore Go: Cryptography, I’ll introduce you to not only the fundamentals of ciphers, but also the various modes in which we can operate them. A cipher mode determines how plaintext data is broken up into blocks and encrypted, and how each block relates to the next, and there are several possibilities.

For example, one simple idea is to just encrypt each block separately with the same key. The problem with this is that, for a given size of block (say, 16 bytes), each plaintext block always encrypts to the same ciphertext block, and there are only so many possibilities. If Eve, our hypothetical eavesdropper, can compile an exhaustive code book mapping all the possible inputs of the cipher to the corresponding outputs, she can instantly decode any message.

That’s why this simple mode is known as Electronic Code Book (ECB), and why it’s not often used in practice. And there are some other problems with ECB that I cover in more detail in the book, such as block dropping and replay attacks: Mallory, a hypothetical person-in-the-middle, can interfere with our cipher traffic, omitting or repeating certain blocks without this alteration being detectable.

A big improvement on ECB mode, then, is to make each block dependent on the previous one, by using the previous encrypted block as input to the cipher, along with each block of plaintext. That way, not only is each encrypted block always different, even for the same plaintext, our messages are resistant to replay attacks. If any block is dropped or repeated the recipient will know right away, because they won’t be able to decrypt any subsequent blocks.

Because the enciphered blocks thus form an unbreakable chain, this mode is known as Cipher Block Chain (CBC). Let’s see what enciphering in CBC mode with the AES cipher looks like in Go.

Enciphering with AES-CBC

You might be surprised at how little code you actually need to write to encrypt data using the crypto/aes package:

block, err := aes.NewCipher(key)
...
iv := make([]byte, aes.BlockSize)
_, err = rand.Read(iv)
...
enc := cipher.NewCBCEncrypter(block, iv)
...
enc.CryptBlocks(ciphertext, plaintext)

(Listing aes/1)

Not too bad, is it? Of course, this is just a snippet showing the part that actually does the encrypting, omitting error checks and so on. To turn this into a useful program that you could run from the command line to encrypt data, we’ll need to add a bit more code, but not too much.

First, have a try yourself, and see what you can come up with, using the above code snippet and the crypto/aes docs as a starting point.

Writing an encipher program using AES

GOAL: Your challenge here is to write a simple CLI tool in Go that reads data from its standard input, enciphers it with AES using a key supplied on the command line, and writes the resulting ciphertext to the standard output. It’s not too difficult in principle: it’s just a matter of connecting the dots. If you’ve read Explore Go: Cryptography already, you’ll certainly have no trouble with this program. If not, and you’d like some hints on how to get started, read on.


HINT: If you’ve written command-line tools in Go before, you’ll know how to use the flag package to define a flag (for example, key) and get the value supplied by the user. You probably also know how to use os.Stdin and os.Stdout to read and write data from the standard input and output streams. Nothing fancy needed here. All you have to do is get the user’s plaintext as a slice of bytes, ready to encrypt.

Because AES is a block cipher, it requires the data we encrypt to be an exact multiple of the block size: in this case, 16 bytes. For this challenge, you needn’t worry about handling data that doesn’t contain an exact number of blocks. But if you like, you can write some code to add padding bytes where necessary; that’s something else I explain in detail in the book. See what you can do.


SOLUTION: Here’s my version of encipher, then; how does it compare to yours?

package main

import (
    "bytes"
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "encoding/hex"
    "flag"
    "fmt"
    "io"
    "os"
)

func main() {
    keyHex := flag.String("key", "", "32-byte key in hexadecimal")
    flag.Parse()
    key, err := hex.DecodeString(*keyHex)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    block, err := aes.NewCipher(key)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    plaintext, err := io.ReadAll(os.Stdin)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    iv := make([]byte, aes.BlockSize)
    _, err = rand.Read(iv)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    os.Stdout.Write(iv)
    enc := cipher.NewCBCEncrypter(block, iv)
    plaintext = Pad(plaintext, aes.BlockSize)
    ciphertext := make([]byte, len(plaintext))
    enc.CryptBlocks(ciphertext, plaintext)
    os.Stdout.Write(ciphertext)
}

func Pad(data []byte, blockSize int) []byte {
    n := blockSize - len(data)%blockSize
    padding := bytes.Repeat([]byte{byte(n)}, n)
    return append(data, padding...)
}

(Listing aes/1)

Let’s see if it works:

echo "Hello, world" | go run ./cmd/encipher -key \
  8e8c2771be5c2bb10d541a5bf6aa51203e0bce2d6d4fa267afd89a6e20df11f1 \
  > enciphered.bin

Well, it did something; it created the enciphered.bin file. So what’s in it?

cat enciphered.bin
����U
��p
   �hR��EN��GQ->�7�%  

Hmm. Well, that’s just what it should look like if it’s properly enciphered, of course: random noise. But the only way to know if we successfully encrypted it, rather than just scrambling it irrevocably, is to try deciphering it again with the same key.

Can you write the corresponding decipher program? Have a try.

Here’s what I came up with, and for obvious reasons it’s pretty similar to encipher, just with a few tweaks in appropriate places:

package main

import (
    "crypto/aes"
    "crypto/cipher"
    "encoding/hex"
    "flag"
    "fmt"
    "io"
    "os"
)

func main() {
    keyHex := flag.String("key", "", "32-byte key in hexadecimal")
    flag.Parse()
    key, err := hex.DecodeString(*keyHex)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    block, err := aes.NewCipher(key)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    ciphertext, err := io.ReadAll(os.Stdin)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    iv := ciphertext[:aes.BlockSize]
    plaintext := make([]byte, len(ciphertext)-aes.BlockSize)
    dec := cipher.NewCBCDecrypter(block, iv)
    dec.CryptBlocks(plaintext, ciphertext[aes.BlockSize:])
    plaintext = Unpad(plaintext, aes.BlockSize)
    os.Stdout.Write(plaintext)
}

func Unpad(data []byte, blockSize int) []byte {
    n := int(data[len(data)-1])
    return data[:len(data)-n]
}

(Listing aes/1)

Let’s put it to the test, then, and see if we can decipher the text we enciphered earlier:

cat enciphered.bin | go run ./cmd/decipher -key \
  8e8c2771be5c2bb10d541a5bf6aa51203e0bce2d6d4fa267afd89a6e20df11f1
Hello, world

That’s reassuring!

Encryption plus authentication

In Explore Go: Cryptography, we explore not just encryption, but the related issues of integrity—how can the recipient of a cipher message know that it hasn’t been interfered with along the way (by Mallory, for example)—and authentication: who is the message actually from? Is it really from the sender it claims to be, or is it a forgery?

AES has ways to deal with these, too. In our previous example, we used AES in CBC mode, which is fine. But we can do more. AES also provides another mode which includes authentication: Galois Counter Mode (GCM).

With AES-GCM, then, we get not only a strong block cipher, but a message integrity check too. It’s similar to the HMAC authentication scheme outlined in the book, or that you might be familiar with from using cryptographic tools yourself.

Though the implementation details are different, the effect is the same: Bob, our recipient, can be confident that the message he received is really from Alice, his correspondent, and that it hasn’t been surreptitiously (or even accidentally) altered along the way.

Using AES in GCM mode is just as easy as in CBC mode, if not easier:

block, err := aes.NewCipher(key)
...
gcm, err := cipher.NewGCM(block)
...
nonce := make([]byte, gcm.NonceSize())
_, err = rand.Read(nonce)
...
...
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)

(Listing aes/2)

Just as before, we create a new cipher object with NewCipher. Instead of creating a CBC encrypter with it, though, we use it to create a GCM instead.

To do the actual encryption, we call the GCM’s Seal method. If you’re wondering why it’s called this, imagine an old-fashioned wax seal being put on a letter so that no one can open it without being detected. AES-GCM not only encrypts the data, it seals it inside a tamper-proof, digital envelope.

Enciphering with AES-GCM

With our first encipher program, we attached our random input data, the nonce, or initialization vector (IV) as a prefix to the ciphertext, because the recipient needs it. AES-GCM does this for us automatically, and it also appends the authentication tag—the message integrity check—as a suffix to the output ciphertext.

The message that goes over the wire, then, includes these three components: the nonce, the ciphertext, and the authentication tag. Here’s how to “open”, or unseal, it at the other end:

block, err := aes.NewCipher(key)
...
gcm, err := cipher.NewGCM(block)
...
nonce := ciphertext[:gcm.NonceSize()]
ciphertext = ciphertext[gcm.NonceSize():]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)

(Listing aes/2)

Another nice feature of AES-GCM is that it does its own padding, so we can skip those steps. In fact, the code above is basically all we’ll ever need to write in Go to do modern cryptography. How convenient!

Updating our CLI tools to use AES-GCM

Over to you again for another challenge.

GOAL: Update the encipher and decipher programs to use AES-GCM instead of AES-CBC.


HINT: This isn’t too difficult, in fact. The only changes you actually need to make are to call Seal and Open instead of CryptBlocks, and to extract the nonce from the first part of the enciphered message before decrypting it.


SOLUTION: Here’s my GCM version of encipher:

package main

import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "encoding/hex"
    "flag"
    "fmt"
    "io"
    "os"
)

func main() {
    keyHex := flag.String("key", "", "32-byte key in hexadecimal")
    flag.Parse()
    key, err := hex.DecodeString(*keyHex)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    block, err := aes.NewCipher(key)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    plaintext, err := io.ReadAll(os.Stdin)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    gcm, err := cipher.NewGCM(block)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    nonce := make([]byte, gcm.NonceSize())
    _, err = rand.Read(nonce)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
    os.Stdout.Write(ciphertext)
}

(Listing aes/2)

And here’s decipher:

package main

import (
    "crypto/aes"
    "crypto/cipher"
    "encoding/hex"
    "flag"
    "fmt"
    "io"
    "os"
)

func main() {
    keyHex := flag.String("key", "", "32-byte key in hexadecimal")
    flag.Parse()
    key, err := hex.DecodeString(*keyHex)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    block, err := aes.NewCipher(key)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    ciphertext, err := io.ReadAll(os.Stdin)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    gcm, err := cipher.NewGCM(block)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    nonce := ciphertext[:gcm.NonceSize()]
    ciphertext = ciphertext[gcm.NonceSize():]
    plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    os.Stdout.Write(plaintext)
}

(Listing aes/2)

A quick double-check that everything’s still in order:

echo "Hello, world" | go run ./cmd/encipher -key \
  8e8c2771be5c2bb10d541a5bf6aa51203e0bce2d6d4fa267afd89a6e20df11f1 \
  | go run ./cmd/decipher -key \
  8e8c2771be5c2bb10d541a5bf6aa51203e0bce2d6d4fa267afd89a6e20df11f1
Hello, world

Pretty neat! So you now have everything you need to keep your secrets safe from Eve and Mallory, from major governments, and even from your kid sister. You might like to explore the crypto packages in Go further, trying out other ciphers and modes to see how they work.

If you’d like to know more about AES, about encryption in Go, or about cryptography in general, please consider buying my book Explore Go: Cryptography, and share this article with friends. Your generous support makes it possible for me to earn a living by writing! It’s much appreciated.

Functional programming in Go

Functional programming in Go

0