Go & AES-GCM: A Security Deep Dive
Understanding AES-GCM Nonces. A Go Security Deep Dive. Once, while working on a project to upskill a bit, I came across countless resources recommending GCM (Galois/Counter Mode) for encrypting data in Go. Many snippets I found looked similar to this one: package main import ( "crypto/aes" "crypto/cipher" "crypto/rand" "fmt" "io" ) func main() { key := []byte("verysecretkey123") // Never hardcode keys in real life! plaintext := []byte("This is the data to encrypt") block, _ := aes.NewCipher(key) aesgcm, _ := cipher.NewGCM(block) nonce := make([]byte, aesgcm.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { panic(err.Error()) } ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil) fmt.Printf("%x\n", ciphertext) } The lack of error handling and hardcoded secrets immediately should raise the eyebrows of any Go developer. Let’s quickly fix the issues that make this code problematic: package main import ( "bufio" "crypto/aes" "crypto/cipher" "crypto/rand" "fmt" "io" "os" ) func main() { key := []byte(os.Getenv("AES_KEY")) if len(key) == 0 { panic("AES_KEY environment variable not set") } // Read input from stdin reader := bufio.NewReader(os.Stdin) plaintext, err := reader.ReadBytes('\n') if err != nil && err != io.EOF { panic(err.Error()) } block, err := aes.NewCipher(key) if err != nil { panic(err.Error()) } aesgcm, err := cipher.NewGCM(block) if err != nil { panic(err.Error()) } nonce := make([]byte, aesgcm.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { panic(err.Error()) } ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil) fmt.Printf("%x\n", ciphertext) } This version addresses hardcoded secrets and improves error handling. You would use it like this: cat my-secret-file.txt | AES_KEY=verysecretkey123 ./not-so-secure-cipher # or: echo "This is the data to encrypt" | AES_KEY=verysecretkey123 ./not-so-secure-cipher GCM from the crypto/cipher library seems a quick and convenient solution for encrypting data. But wait; is this it? I thought developing secure systems is hard?! We must be doing something wrong… GCM 101 And yes, there are some critical issues beyond the obvious ones that we addressed briefly in the last example. For ciphers, you have to assume that the ciphertext is accessible to anyone. Imagine this code snippet ends up as a component to encrypt the message exchange between two company branches. Charly, your typical threat, was patiently monitoring the encrypted communication between the two company branches. Over time, he has intercepted a vast amount of ciphertext. Knowing that the company uses AES-GCM, he assumes the following message format: + - - - - - - + - - - - - + - - - -- - + - - -- -+ | NONCE (12B) | TAG (16B) | CIPHERTEXT | PADDING | + - - - - - - + - - - - - + - - - -- - + - - -- -+ Charlie’s main focus is analyzing the nonce values within the intercepted messages. After painstaking analysis of the captured traffic, Charly notices a pattern. Before we can continue, we need some theoretical background. GCM’s strength lies in its combined encryption and authentication capabilities. However, it heavily relies on unique nonces (Number used ONCE) for each encryption operation. Reusing a nonce twice breaks any security GCM offers, potentially exposing the plaintext of all messages where the nonce was reused and allowing the attacker to bypass the authentication protection. The Forbidden Attack Nonce reuse allows an attacker to recover the authentication key, enabling them to modify or craft messages. This issue is detailed in this document by NIST. Copying others’ code from the web (or an AI system) could have severe consequences in this case. Translating the requirements for proper nonce usage into code means tracking all nonces used with a given key, which is impractical with a significant amount of traffic. An alternative would be to keep track of the number of messages encrypted with the same key and rotate the key when the probability of a nonce collision becomes too high. GCM typically uses 96-bit nonces. Let's assume that this is approximately 2⁴⁸ (about 281 trillion) messages until you have a 50% collision risk. 50% may be too risky for your use case. The official rule of thumb is: The total number of invocations of the authenticated encryption function shall not exceed 2³², including all IV lengths and all instances of the authenticated encryption function with the given key. NIST Special Publication 800-38D (8.3 Constraints on the Number of Invocations) Ok, a counter? Problem solved? Well, no. Now we would also need to protect the nonce counter from being altered without authorization. Hashing will do, but guess what—we need another key f

Understanding AES-GCM Nonces. A Go Security Deep Dive.
Once, while working on a project to upskill a bit, I came across countless resources recommending GCM (Galois/Counter Mode) for encrypting data in Go. Many snippets I found looked similar to this one:
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"fmt"
"io"
)
func main() {
key := []byte("verysecretkey123") // Never hardcode keys in real life!
plaintext := []byte("This is the data to encrypt")
block, _ := aes.NewCipher(key)
aesgcm, _ := cipher.NewGCM(block)
nonce := make([]byte, aesgcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
panic(err.Error())
}
ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil)
fmt.Printf("%x\n", ciphertext)
}
The lack of error handling and hardcoded secrets immediately should raise the eyebrows of any Go developer.
Let’s quickly fix the issues that make this code problematic:
package main
import (
"bufio"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"fmt"
"io"
"os"
)
func main() {
key := []byte(os.Getenv("AES_KEY"))
if len(key) == 0 {
panic("AES_KEY environment variable not set")
}
// Read input from stdin
reader := bufio.NewReader(os.Stdin)
plaintext, err := reader.ReadBytes('\n')
if err != nil && err != io.EOF {
panic(err.Error())
}
block, err := aes.NewCipher(key)
if err != nil {
panic(err.Error())
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
panic(err.Error())
}
nonce := make([]byte, aesgcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
panic(err.Error())
}
ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil)
fmt.Printf("%x\n", ciphertext)
}
This version addresses hardcoded secrets and improves error handling. You would use it like this:
cat my-secret-file.txt | AES_KEY=verysecretkey123 ./not-so-secure-cipher
# or:
echo "This is the data to encrypt" | AES_KEY=verysecretkey123 ./not-so-secure-cipher
GCM from the crypto/cipher
library seems a quick and convenient solution for encrypting data. But wait; is this it? I thought developing secure systems is hard?! We must be doing something wrong…
GCM 101
And yes, there are some critical issues beyond the obvious ones that we addressed briefly in the last example. For ciphers, you have to assume that the ciphertext is accessible to anyone.
Imagine this code snippet ends up as a component to encrypt the message exchange between two company branches. Charly, your typical threat, was patiently monitoring the encrypted communication between the two company branches. Over time, he has intercepted a vast amount of ciphertext. Knowing that the company uses AES-GCM, he assumes the following message format:
+ - - - - - - + - - - - - + - - - -- - + - - -- -+
| NONCE (12B) | TAG (16B) | CIPHERTEXT | PADDING |
+ - - - - - - + - - - - - + - - - -- - + - - -- -+
Charlie’s main focus is analyzing the nonce values within the intercepted messages. After painstaking analysis of the captured traffic, Charly notices a pattern.
Before we can continue, we need some theoretical background. GCM’s strength lies in its combined encryption and authentication capabilities. However, it heavily relies on unique nonces (Number used ONCE) for each encryption operation. Reusing a nonce twice breaks any security GCM offers, potentially exposing the plaintext of all messages where the nonce was reused and allowing the attacker to bypass the authentication protection.
The Forbidden Attack
Nonce reuse allows an attacker to recover the authentication key, enabling them to modify or craft messages. This issue is detailed in this document by NIST.
Copying others’ code from the web (or an AI system) could have severe consequences in this case. Translating the requirements for proper nonce usage into code means tracking all nonces used with a given key, which is impractical with a significant amount of traffic.
An alternative would be to keep track of the number of messages encrypted with the same key and rotate the key when the probability of a nonce collision becomes too high. GCM typically uses 96-bit nonces. Let's assume that this is approximately 2⁴⁸ (about 281 trillion) messages until you have a 50% collision risk. 50% may be too risky for your use case.
The official rule of thumb is:
The total number of invocations of the authenticated encryption function shall not exceed 2³², including all IV lengths and all instances of the authenticated encryption function with the given key.
NIST Special Publication 800-38D (8.3 Constraints on the Number of Invocations)
Ok, a counter? Problem solved? Well, no. Now we would also need to protect the nonce counter from being altered without authorization. Hashing will do, but guess what—we need another key for this. Lastly, we would need to rotate the key at some point. In the context of persisted data, we may even have to keep the history of the keys.
Back to Charly. The pattern he discovered was that due to a flaw in the company’s implementation, the nonce generation process occasionally resets after a server restart or network glitches. This flaw, while seemingly minor, has allowed Charly to collect a significant number of messages encrypted with the same nonce, most notably the all-zero nonce, which is the default value after a reset.
Charly realizes that this is his golden opportunity. He focuses on the subset of messages that share the all-zero nonce. This allows him to gain access to sensitive information across a large number of messages.
I’ll leave it to your imagination what will happen next.
Conclusion
What a great find for Charly; what a disastrous day for the devs and the company. Let's take these lessons from the scenario to challenge our abilities and start questioning if some apparent simplicity is based on our false assumptions or harbors devastating vulnerabilities. I encourage you to go beyond the obvious, question the familiar, and always strive for the highest standard you can deliver. After all, the integrity of your data, and perhaps even the fate of a company, could very well depend on it.