Encrypting with AES

Encrypting with AES

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”

AES is an immensely strong cipher, one of the most effective ways of keeping secrets ever invented. It’s definitely kid-sister-proof, provided you use it correctly. In this tutorial, let’s see how to do exactly that, by using Go’s crypto/aes package to write a simple encryption and decryption tool.

Enciphering with AES-CBC

You might be surprised at how little Go code you actually need to write to encrypt data with AES:

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? The standard library authors have already done most of the heavy lifting for us, as we explored in the tutorial about Go’s AES implementation. All we need to do is call their code to make the magic happen.

My book Explore Go: Cryptography is a guide to everything you need to know to use cryptography securely as a Go programmer, from the principles of ciphers and keys, to understanding algorithms like AES, SHA, and even post-quantum ciphers. We’ll learn in a fun, interactive way, by building lots of little tools like this one, which are ideal if you don’t want your little sister reading your diary.

Of course, we waved away some of the details here with a carefree “…”. To turn this into a practical program that we could run from the command line to encrypt data, we’ll need to add a bit more code, but not too much.

Writing an encipher program using AES

Here’s something that will get us started:

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)

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. That’s why one of the admin tasks we need to do is add padding bytes where necessary, using the Pad function.

Let’s see if it all 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.

The corresponding decipher program is pretty similar to encipher, as you’d expect, just with a few tweaks in the 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

The security of our messages depends not only on what cipher we’re using, but also the particular mode we use it in. That is, the choices we make about how plaintext data is broken up into blocks and encrypted, and how each block relates to the next.

For example, one simple idea is to just encrypt each block separately with the same key. The problem with this is that each plaintext block always encrypts to the same ciphertext, meaning that Eve, our hypothetical eavesdropper, can compile an exhaustive code book that, once complete, can instantly decode any message.

That’s why this simple Electronic Code Book (ECB) mode is not often used in practice. A better idea 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.

Because the enciphered blocks thus form an unbreakable chain, this mode is known as Cipher Block Chain (CBC). AES’s Galois Counter Mode (GCM) combines CBC mode with the useful feature of authentication. Bob can use this to prove 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

To update our encipher and decipher programs for AES-GCM instead of AES-CBC, the only changes we 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:

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.

Programming is fun

Programming is fun

func to functional

func to functional