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:
, err := aes.NewCipher(key)
block...
:= make([]byte, aes.BlockSize)
iv , err = rand.Read(iv)
_...
:= cipher.NewCBCEncrypter(block, iv)
enc ...
.CryptBlocks(ciphertext, plaintext) enc
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() {
:= flag.String("key", "", "32-byte key in hexadecimal")
keyHex .Parse()
flag, err := hex.DecodeString(*keyHex)
keyif err != nil {
.Fprintln(os.Stderr, err)
fmt.Exit(1)
os}
, err := aes.NewCipher(key)
blockif err != nil {
.Fprintln(os.Stderr, err)
fmt.Exit(1)
os}
, err := io.ReadAll(os.Stdin)
plaintextif err != nil {
.Fprintln(os.Stderr, err)
fmt.Exit(1)
os}
:= make([]byte, aes.BlockSize)
iv , err = rand.Read(iv)
_if err != nil {
.Fprintln(os.Stderr, err)
fmt.Exit(1)
os}
.Stdout.Write(iv)
os:= cipher.NewCBCEncrypter(block, iv)
enc = Pad(plaintext, aes.BlockSize)
plaintext := make([]byte, len(plaintext))
ciphertext .CryptBlocks(ciphertext, plaintext)
enc.Stdout.Write(ciphertext)
os}
func Pad(data []byte, blockSize int) []byte {
:= blockSize - len(data)%blockSize
n := bytes.Repeat([]byte{byte(n)}, n)
padding return append(data, padding...)
}
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() {
:= flag.String("key", "", "32-byte key in hexadecimal")
keyHex .Parse()
flag, err := hex.DecodeString(*keyHex)
keyif err != nil {
.Fprintln(os.Stderr, err)
fmt.Exit(1)
os}
, err := aes.NewCipher(key)
blockif err != nil {
.Fprintln(os.Stderr, err)
fmt.Exit(1)
os}
, err := io.ReadAll(os.Stdin)
ciphertextif err != nil {
.Fprintln(os.Stderr, err)
fmt.Exit(1)
os}
:= ciphertext[:aes.BlockSize]
iv := make([]byte, len(ciphertext)-aes.BlockSize)
plaintext := cipher.NewCBCDecrypter(block, iv)
dec .CryptBlocks(plaintext, ciphertext[aes.BlockSize:])
dec= Unpad(plaintext, aes.BlockSize)
plaintext .Stdout.Write(plaintext)
os}
func Unpad(data []byte, blockSize int) []byte {
:= int(data[len(data)-1])
n return data[:len(data)-n]
}
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:
, err := aes.NewCipher(key)
block...
, err := cipher.NewGCM(block)
gcm...
:= make([]byte, gcm.NonceSize())
nonce , err = rand.Read(nonce)
_...
...
:= gcm.Seal(nonce, nonce, plaintext, nil) ciphertext
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:
, err := aes.NewCipher(key)
block...
, err := cipher.NewGCM(block)
gcm...
:= ciphertext[:gcm.NonceSize()]
nonce = ciphertext[gcm.NonceSize():]
ciphertext , err := gcm.Open(nil, nonce, ciphertext, nil) plaintext
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() {
:= flag.String("key", "", "32-byte key in hexadecimal")
keyHex .Parse()
flag, err := hex.DecodeString(*keyHex)
keyif err != nil {
.Fprintln(os.Stderr, err)
fmt.Exit(1)
os}
, err := aes.NewCipher(key)
blockif err != nil {
.Fprintln(os.Stderr, err)
fmt.Exit(1)
os}
, err := io.ReadAll(os.Stdin)
plaintextif err != nil {
.Fprintln(os.Stderr, err)
fmt.Exit(1)
os}
, err := cipher.NewGCM(block)
gcmif err != nil {
.Fprintln(os.Stderr, err)
fmt.Exit(1)
os}
:= make([]byte, gcm.NonceSize())
nonce , err = rand.Read(nonce)
_if err != nil {
.Fprintln(os.Stderr, err)
fmt.Exit(1)
os}
:= gcm.Seal(nonce, nonce, plaintext, nil)
ciphertext .Stdout.Write(ciphertext)
os}
And here’s decipher
:
package main
import (
"crypto/aes"
"crypto/cipher"
"encoding/hex"
"flag"
"fmt"
"io"
"os"
)
func main() {
:= flag.String("key", "", "32-byte key in hexadecimal")
keyHex .Parse()
flag, err := hex.DecodeString(*keyHex)
keyif err != nil {
.Fprintln(os.Stderr, err)
fmt.Exit(1)
os}
, err := aes.NewCipher(key)
blockif err != nil {
.Fprintln(os.Stderr, err)
fmt.Exit(1)
os}
, err := io.ReadAll(os.Stdin)
ciphertextif err != nil {
.Fprintln(os.Stderr, err)
fmt.Exit(1)
os}
, err := cipher.NewGCM(block)
gcmif err != nil {
.Fprintln(os.Stderr, err)
fmt.Exit(1)
os}
:= ciphertext[:gcm.NonceSize()]
nonce = ciphertext[gcm.NonceSize():]
ciphertext , err := gcm.Open(nil, nonce, ciphertext, nil)
plaintextif err != nil {
.Fprintln(os.Stderr, err)
fmt.Exit(1)
os}
.Stdout.Write(plaintext)
os}
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.