CWE-326: Inadequate Encryption Strength - Go
Overview
Inadequate encryption strength vulnerabilities in Go applications occur when weak cryptographic algorithms, insufficient key sizes, or insecure encryption modes are used, allowing attackers to decrypt sensitive data. Modern cryptography requires strong algorithms (AES, ChaCha20), key lengths adequate for their own algorithm type (AES-256, RSA-3072), and secure modes of operation (GCM, not ECB). Go's crypto package provides sound implementations, but choosing the algorithm, key size and mode is still the developer's job.
Common mistakes include using deprecated algorithms (DES, RC4, MD5 for security), inadequate key sizes for the algorithm in use (RSA-1024, which is roughly 80-bit strength; note that AES-128 is a full 128-bit strength and is not one of these), insecure modes (ECB which doesn't provide semantic security, CBC without proper IV management), weak key derivation (simple hashing instead of PBKDF2/Argon2), and predictable initialization vectors. The crypto/des, crypto/md5, and crypto/rc4 packages still exist in Go for backwards compatibility, but should never be used for new security-sensitive code.
These vulnerabilities matter most in applications handling sensitive data - user PII, financial data, health records, authentication tokens, API keys - and in database encryption. Weak encryption is often worse than no encryption: it looks like protection while remaining breakable with current hardware, so an attacker who reaches the database decrypts everything. PCI DSS is direct about this for cardholder data; HIPAA treats encryption as addressable and GDPR Article 32 is risk-based, so there weak crypto is a failure to apply a control your own risk assessment called for rather than a breach of a flat mandate.
Primary Defence: Use AES-256-GCM for symmetric encryption with crypto/rand-generated keys and nonces. Use RSA-OAEP with 3072-bit keys for asymmetric encryption, or X25519 where what you need is key agreement - X25519 establishes a shared secret between two parties and does not encrypt a message, so it replaces the key-transport role of RSA rather than RSA itself. Never use DES, 3DES, RC4, ECB mode, or MD5/SHA1 for security. Always use authenticated encryption (GCM, ChaCha20-Poly1305) to prevent tampering.
Common Vulnerable Patterns
DES Encryption (Deprecated Algorithm)
// VULNERABLE - Using obsolete DES encryption
package main
import (
"crypto/cipher"
"crypto/des"
"fmt"
)
func encryptData(key, plaintext []byte) ([]byte, error) {
// DANGEROUS: DES has 56-bit effective key size
block, err := des.NewCipher(key) // Key must be 8 bytes
if err != nil {
return nil, err
}
// VULNERABLE - ECB mode provides no semantic security
ciphertext := make([]byte, len(plaintext))
for i := 0; i < len(plaintext); i += des.BlockSize {
block.Encrypt(ciphertext[i:], plaintext[i:])
}
return ciphertext, nil
}
// VULNERABILITIES:
// 1. DES 56-bit key is brute-forceable in hours with modern hardware
// 2. ECB mode reveals patterns in plaintext (identical blocks = identical ciphertext)
// 3. No authentication - attacker can modify ciphertext undetected
Why this is vulnerable: DES uses a 56-bit key (8 bytes with parity bits), which can be brute-forced in hours with specialized hardware or cloud computing. ECB (Electronic Codebook) mode encrypts each block independently, revealing patterns - identical plaintext blocks produce identical ciphertext blocks, leaking information about the data structure. Without authentication, attackers can flip bits in the ciphertext to modify the decrypted plaintext. DES was retired as a federal standard in 2005 and should never be used.
AES with ECB Mode
// VULNERABLE - AES with insecure ECB mode
import (
"crypto/aes"
"crypto/cipher"
)
func encryptAESECB(key, plaintext []byte) ([]byte, error) {
// Using AES-256 (good key size)
block, err := aes.NewCipher(key) // 32 bytes = AES-256
if err != nil {
return nil, err
}
// VULNERABLE - ECB mode is fundamentally insecure
ciphertext := make([]byte, len(plaintext))
for i := 0; i < len(plaintext); i += block.BlockSize() {
block.Encrypt(ciphertext[i:], plaintext[i:])
}
return ciphertext, nil
}
// ATTACK:
// Identical plaintext blocks → identical ciphertext blocks
// Famous "ECB Penguin" demonstrates pattern leakage
// Example: {"amount": 100} appears identical everywhere in encrypted data
// No protection against block reordering or replay
Why this is vulnerable: ECB mode lacks semantic security - encrypting the same plaintext block always produces the same ciphertext block. This leaks patterns: repeated data (like JSON field names, database NULL values, common words) produces recognizable patterns in ciphertext. Attackers can identify structure, swap blocks, or replay portions of ciphertext. The classic demonstration is encrypting images - encrypted images in ECB mode still show visible outlines. ECB also provides no integrity protection, allowing undetected tampering. Even with AES-256 keys, ECB is fundamentally broken.
CBC Mode with Static IV
// VULNERABLE - CBC with predictable initialization vector
import (
"crypto/aes"
"crypto/cipher"
)
var staticIV = []byte("1234567890123456") // DANGEROUS: Hardcoded IV
func encryptAESCBC(key, plaintext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
// VULNERABLE - Reusing the same IV for multiple messages
mode := cipher.NewCBCEncrypter(block, staticIV)
ciphertext := make([]byte, len(plaintext))
mode.CryptBlocks(ciphertext, plaintext)
return ciphertext, nil
}
// VULNERABILITY:
// A CBC IV must be unpredictable before the plaintext is chosen.
// A hardcoded IV is known in advance, so an attacker who can get
// chosen data encrypted confirms guesses about earlier plaintext.
// It also makes encryption deterministic: two messages with the same
// prefix produce byte-identical leading ciphertext blocks.
Why this is vulnerable: CBC requires an IV that an attacker cannot predict before the plaintext of that message is chosen, and staticIV is in the source. An attacker who can have chosen data encrypted under the same key submits a block built from the known IV and a guess at an earlier plaintext block; if the resulting ciphertext block matches one they captured, the guess was right. That recovers a secret a guess at a time without touching the key - the BEAST attack on TLS 1.0 is this exact shape. The static IV also makes encryption deterministic, so two messages sharing a prefix produce byte-identical leading ciphertext blocks and an observer learns which requests are the same. Note what this is not: XORing two CBC ciphertexts under the same key and IV does not give the XOR of the plaintexts, because each block passes through the block cipher - that property belongs to CTR, ChaCha20 and GCM, where a repeated nonce is catastrophic on its own. The IV need not be secret, but it must come from crypto/rand per message. Separately, CBC provides no authentication, so this construction is also open to padding oracle attacks.
Weak Key Derivation
// VULNERABLE - Weak password-to-key derivation
import (
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"crypto/sha256"
)
func encryptWithPassword(password, plaintext []byte) ([]byte, error) {
// VULNERABLE - Simple hash for key derivation
hash := md5.Sum(password) // One fast hash pass - no salt, no work factor
key := hash[:]
// Alternative vulnerable pattern:
// hash := sha256.Sum256(password)
// key := hash[:32] // Still vulnerable - no salt, no iterations
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
// ... encryption code ...
return nil, nil
}
// VULNERABILITIES:
// 1. MD5 is cryptographically broken (collisions, speed)
// 2. No salt - same password always produces same key (rainbow tables)
// 3. No iterations - susceptible to brute force (GPUs can test billions/second)
// 4. Fast hash functions designed for speed, not password security
Why this is vulnerable: Hashing passwords directly with MD5 or SHA-256 to derive encryption keys is insecure. MD5 is completely broken for security purposes. Even with SHA-256, the lack of a salt means identical passwords produce identical keys, enabling rainbow table attacks and revealing users with the same password. No iteration count means attackers can test billions of passwords per second using GPUs. Password-based key derivation must use specialized algorithms (PBKDF2, bcrypt, scrypt, Argon2) that are intentionally slow and use unique salts, making brute force attacks computationally expensive.
Small RSA Key Size
// VULNERABLE - RSA with inadequate key size
import (
"crypto/rand"
"crypto/rsa"
)
func generateWeakKeys() (*rsa.PrivateKey, error) {
// VULNERABLE - 1024-bit RSA is considered broken
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
return nil, err
}
return privateKey, nil
}
// VULNERABILITY:
// 1024-bit RSA has been factored with significant resources
// NIST deprecated 1024-bit RSA in 2013
// Use 3072-bit for new keys - NIST rates 2048-bit at 112-bit strength and accepts it only through 2030; 3072-bit gives 128-bit and stays valid beyond
Why this is vulnerable: RSA security depends on the difficulty of factoring large numbers, not on searching the key space, which is why an RSA key has to be an order of magnitude longer than a symmetric one to deliver the same strength. RSA-1024 buys roughly 80-bit strength and NIST has disallowed it for applying protection since 2013; academic demonstrations have already factored RSA-768. RSA-2048 is rated at 112-bit and SP 800-57 Part 1 Table 4 accepts it for applying protection only through 2030, so 3072 bits is the size to generate today - it gives 128-bit strength and no expiry date. Go past 3072 only where a policy or a very long retention period calls for it; 4096-bit keys cost noticeably more to generate and to use, and buy about 150-bit strength rather than the doubling the number suggests.
Secure Patterns
AES-256-GCM Authenticated Encryption
// SECURE - AES-256-GCM with proper key and nonce handling
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"fmt"
"io"
)
func encryptAESGCM(key, plaintext []byte) ([]byte, error) {
// SECURE - AES-256 requires 32-byte key
if len(key) != 32 {
return nil, fmt.Errorf("key must be 32 bytes for AES-256")
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
// SECURE - GCM provides authenticated encryption
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
// SECURE - Generate random nonce for this message
nonce := make([]byte, gcm.NonceSize()) // 12 bytes for GCM
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
// Encrypt and authenticate
// Format: nonce || ciphertext || tag (tag appended by Seal)
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
return ciphertext, nil
}
func decryptAESGCM(key, ciphertext []byte) ([]byte, error) {
if len(key) != 32 {
return nil, fmt.Errorf("key must be 32 bytes")
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonceSize := gcm.NonceSize()
if len(ciphertext) < nonceSize {
return nil, fmt.Errorf("ciphertext too short")
}
// Extract nonce and ciphertext
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
// SECURE - Open verifies authentication tag before decrypting
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, err // Authentication failed - data tampered with
}
return plaintext, nil
}
func generateAESKey() ([]byte, error) {
// SECURE - Generate cryptographically random key
key := make([]byte, 32) // AES-256
if _, err := io.ReadFull(rand.Reader, key); err != nil {
return nil, err
}
return key, nil
}
Why this works: AES-256 uses 256-bit keys, providing strong security against brute force attacks. GCM (Galois/Counter Mode) provides authenticated encryption - it both encrypts the data and generates an authentication tag that detects tampering. The nonce (number used once) is generated with crypto/rand (cryptographically secure), ensuring uniqueness for each encryption with the same key. Prepending the nonce to the ciphertext allows the recipient to decrypt without separate nonce transmission. gcm.Open() verifies the authentication tag before decrypting, preventing padding oracle attacks and detecting any modification to the ciphertext.
ChaCha20-Poly1305 for High Performance
// SECURE - ChaCha20-Poly1305 authenticated encryption
import (
"crypto/rand"
"fmt"
"io"
"golang.org/x/crypto/chacha20poly1305"
)
func encryptChaCha20Poly1305(key, plaintext []byte) ([]byte, error) {
// SECURE - ChaCha20-Poly1305 requires 32-byte key
if len(key) != chacha20poly1305.KeySize {
return nil, fmt.Errorf("key must be 32 bytes")
}
aead, err := chacha20poly1305.New(key)
if err != nil {
return nil, err
}
// SECURE - Generate random nonce
nonce := make([]byte, aead.NonceSize()) // 12 bytes for standard variant
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
// Encrypt and authenticate
ciphertext := aead.Seal(nonce, nonce, plaintext, nil)
return ciphertext, nil
}
func decryptChaCha20Poly1305(key, ciphertext []byte) ([]byte, error) {
if len(key) != chacha20poly1305.KeySize {
return nil, fmt.Errorf("key must be 32 bytes")
}
aead, err := chacha20poly1305.New(key)
if err != nil {
return nil, err
}
nonceSize := aead.NonceSize()
if len(ciphertext) < nonceSize {
return nil, fmt.Errorf("ciphertext too short")
}
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
// Decrypt and verify
plaintext, err := aead.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, err
}
return plaintext, nil
}
Why this works: ChaCha20-Poly1305 is a modern authenticated encryption algorithm providing security equivalent to AES-256-GCM with better performance on systems without AES hardware acceleration (mobile devices, older servers). ChaCha20 is the stream cipher, Poly1305 provides authentication. Like GCM, it requires a 256-bit key and unique nonce per message. The algorithm is resistant to timing attacks. It's specified in RFC 8439 and widely used (TLS 1.3, SSH, VPNs). Either AES-GCM or ChaCha20-Poly1305 is a sound default for authenticated encryption.
Secure Key Derivation with Argon2
// SECURE - Password-based key derivation with Argon2
import (
"crypto/rand"
"encoding/base64"
"fmt"
"io"
"golang.org/x/crypto/argon2"
)
type DerivedKey struct {
Key []byte
Salt []byte
}
func deriveKeyFromPassword(password string) (*DerivedKey, error) {
// SECURE - Generate random salt
salt := make([]byte, 16)
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
return nil, err
}
// SECURE - Argon2id with recommended parameters
// time=1, memory=64MB, threads=4, keyLen=32
key := argon2.IDKey(
[]byte(password),
salt,
1, // iterations
64*1024, // memory in KiB (64 MB)
4, // parallelism
32, // key length (AES-256)
)
return &DerivedKey{
Key: key,
Salt: salt,
}, nil
}
func deriveKeyWithSalt(password string, salt []byte) []byte {
// Use same parameters for verification
return argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, 32)
}
// Usage example
func encryptWithPassword(password string, plaintext []byte) (string, error) {
// Derive encryption key from password
derived, err := deriveKeyFromPassword(password)
if err != nil {
return "", err
}
// Encrypt with AES-256-GCM
ciphertext, err := encryptAESGCM(derived.Key, plaintext)
if err != nil {
return "", err
}
// Return salt + ciphertext encoded
result := append(derived.Salt, ciphertext...)
return base64.StdEncoding.EncodeToString(result), nil
}
func decryptWithPassword(password, encoded string) ([]byte, error) {
// Decode
data, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return nil, err
}
if len(data) < 16 {
return nil, fmt.Errorf("invalid ciphertext")
}
// Extract salt and ciphertext
salt := data[:16]
ciphertext := data[16:]
// Derive key with same salt
key := deriveKeyWithSalt(password, salt)
// Decrypt
return decryptAESGCM(key, ciphertext)
}
Why this works: Argon2id is based on the Password Hashing Competition winner and is recommended for password-based key derivation when available. It uses memory-hard operations that make GPU/ASIC attacks more expensive. The salt ensures identical passwords produce different keys, preventing reusable precomputed tables. The parameters (time/memory/parallelism) tune computational cost and must be tested on production-class hardware. The 32-byte output is suitable for AES-256. Salt must be stored alongside the ciphertext for decryption. PBKDF2, bcrypt, and scrypt remain acceptable in environments where Argon2id is unavailable or not permitted, when configured with current work factors.
RSA-OAEP with Strong Keys
// SECURE - RSA-OAEP with 3072-bit keys
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
)
func generateSecureRSAKeys() (*rsa.PrivateKey, error) {
// SECURE - 3072-bit - NIST rates 2048-bit at 112-bit strength and accepts it only through 2030; 3072-bit gives 128-bit and stays valid beyond
privateKey, err := rsa.GenerateKey(rand.Reader, 3072)
if err != nil {
return nil, err
}
return privateKey, nil
}
func encryptRSAOAEP(publicKey *rsa.PublicKey, plaintext []byte) ([]byte, error) {
// SECURE - OAEP padding with SHA-256
hash := sha256.New()
ciphertext, err := rsa.EncryptOAEP(
hash,
rand.Reader,
publicKey,
plaintext,
nil, // label (optional additional data)
)
if err != nil {
return nil, err
}
return ciphertext, nil
}
func decryptRSAOAEP(privateKey *rsa.PrivateKey, ciphertext []byte) ([]byte, error) {
hash := sha256.New()
plaintext, err := rsa.DecryptOAEP(
hash,
rand.Reader,
privateKey,
ciphertext,
nil,
)
if err != nil {
return nil, err
}
return plaintext, nil
}
// Hybrid encryption: RSA for key, AES for data
func hybridEncrypt(publicKey *rsa.PublicKey, plaintext []byte) ([]byte, []byte, error) {
// SECURE - Generate random AES key
aesKey, err := generateAESKey()
if err != nil {
return nil, nil, err
}
// Encrypt data with AES-GCM (fast)
ciphertext, err := encryptAESGCM(aesKey, plaintext)
if err != nil {
return nil, nil, err
}
// Encrypt AES key with RSA-OAEP (small payload)
encryptedKey, err := encryptRSAOAEP(publicKey, aesKey)
if err != nil {
return nil, nil, err
}
return encryptedKey, ciphertext, nil
}
Why this works: 3072-bit RSA keys provide 128-bit security strength. NIST rates 2048-bit at 112-bit and accepts it only through 2030, so a key generated today at 2048 has a known expiry; 3072-bit remains valid past that date, which matters for anything with a long lifetime such as a signing key or archived ciphertext. NIST SP 800-57 Part 1 Table 4 is the specific rule: 112-bit strength is Acceptable for applying protection through 2030 and Disallowed from 2031, while processing data already protected at that strength remains permitted as legacy use - so this governs new key material, not re-encryption of existing data. OAEP (Optimal Asymmetric Encryption Padding) is the secure RSA padding scheme, preventing padding oracle attacks that break PKCS#1 v1.5. SHA-256 is used within OAEP for hashing. Hybrid encryption (RSA for key exchange, AES for data) is the standard pattern - RSA is slow and has message size limits (~318 bytes for a 3072-bit key with SHA-256 OAEP), while AES is fast and handles arbitrary sizes. The random AES key is encrypted with RSA and transmitted alongside the AES-encrypted data.
X25519 for Key Exchange
// SECURE - Modern X25519 key exchange using the standard library
import (
"crypto/ecdh"
"crypto/rand"
"crypto/subtle"
"fmt"
)
func generateX25519KeyPair() (*ecdh.PrivateKey, error) {
// SECURE - GenerateKey draws from crypto/rand and clamps the scalar for you
return ecdh.X25519().GenerateKey(rand.Reader)
}
func deriveSharedSecret(myPrivateKey *ecdh.PrivateKey, theirPublicKeyBytes []byte) ([]byte, error) {
// SECURE - NewPublicKey rejects a wrong-length encoding, and ECDH rejects a
// low-order peer key - so the returned error is the peer-key check. Do not
// hand the caller a secret you then have to screen yourself.
theirPublicKey, err := ecdh.X25519().NewPublicKey(theirPublicKeyBytes)
if err != nil {
return nil, fmt.Errorf("peer key: %w", err)
}
sharedSecret, err := myPrivateKey.ECDH(theirPublicKey)
if err != nil {
return nil, fmt.Errorf("x25519: %w", err)
}
return sharedSecret, nil
}
// Complete example: Ephemeral Diffie-Hellman
func performKeyExchange() ([]byte, error) {
// Alice generates key pair
alice, err := generateX25519KeyPair()
if err != nil {
return nil, err
}
// Bob generates key pair
bob, err := generateX25519KeyPair()
if err != nil {
return nil, err
}
// Alice computes shared secret from Bob's public key
aliceShared, err := deriveSharedSecret(alice, bob.PublicKey().Bytes())
if err != nil {
return nil, err
}
// Bob computes shared secret (should match)
bobShared, err := deriveSharedSecret(bob, alice.PublicKey().Bytes())
if err != nil {
return nil, err
}
// Both parties must arrive at the same secret
if subtle.ConstantTimeCompare(aliceShared, bobShared) != 1 {
return nil, fmt.Errorf("key exchange failed: shared secrets differ")
}
// Pass the shared secret through a KDF (HKDF) before using it as a key
return aliceShared, nil
}
Why this works: X25519 is a modern elliptic curve Diffie-Hellman (ECDH) function providing roughly 128-bit security from a 256-bit key - comparable to AES-128 in NIST's strength tables, not to AES-256. An elliptic-curve key is about twice the length of the symmetric strength it delivers, which is why 256-bit here and 256-bit in AES are different amounts of security. It's significantly faster than RSA and has smaller key sizes. The curve (Curve25519) is designed to resist side-channel attacks and has built-in protections against common implementation mistakes. X25519 is used in TLS 1.3, SSH, Signal Protocol, and WireGuard. The shared secret should be passed through a KDF (like HKDF) to derive actual encryption keys. This is the modern alternative to RSA for key exchange and agreement.
Use crypto/ecdh rather than golang.org/x/crypto/curve25519. The
curve25519 package has been a thin wrapper over crypto/ecdh since Go 1.20
and its own documentation now describes it as frozen and not accepting new
features. It also works at a lower level: it hands you raw byte slices, so
nothing stops a caller passing a private key where a public key belongs, while
ecdh.PublicKey and ecdh.PrivateKey are distinct types the compiler checks.
Testing
A scanner confirms the weak algorithm is gone. It cannot confirm the replacement works, and a key-strength change is unusually good at passing review while breaking data that already exists. Assert each of these:
- Ciphertext round-trips. Encrypt, then decrypt through a separately
constructed AEAD, and compare against the original plaintext.
gcm.Sealappends the tag for you, but the nonce is yours to carry; a test that keeps the nonce in a local variable will pass while the stored ciphertext is undecryptable. - Tampering is rejected. Flip one byte of the ciphertext, one byte of the
trailing tag, and one byte of the prepended nonce, and confirm
gcm.Openreturns a non-nil error in each case rather than plaintext. - The key size actually applied. Assert
key.N.BitLen() == 3072after generation rather than trusting the argument torsa.GenerateKey, and assertlen(aesKey)for symmetric keys. - Nonces do not repeat. Generate several thousand and assert they are distinct. Nonce reuse under one key breaks GCM's confidentiality and its authentication, and a counter accidentally reset per process shows up here and almost nowhere else.
- Data encrypted before the change still decrypts. Keep a fixture encrypted under the old algorithm and assert the dual-read path returns the original plaintext. This is the test that fails in production if it is missing.
- Old password hashes still verify, and are upgraded on use. Assert that a
correct password checked against a stored legacy hash succeeds, that the
stored hash is then rewritten with the current cost, and that a wrong
password still fails at both stages.
bcrypt.Coston the stored hash tells you whether the rewrite happened.
Additional Resources
- Argon2 RFC 9106
- ChaCha20-Poly1305 RFC 8439
- CWE-326: Inadequate Encryption Strength
- Go crypto Package
- golang.org/x/crypto (extended crypto library)
- Latacora: Cryptographic Right Answers
- NIST SP 800-175B: Cryptographic Algorithm Validation Program
- NIST SP 800-57 Part 1 Rev 5: Recommendation for Key Management - Table 4 is the source for the 2030 date: 112-bit strength is Acceptable for applying protection (generating keys, encrypting, signing) through 2030 and Disallowed from 2031, while processing already-protected data stays permitted as legacy use
- OWASP Cryptographic Storage Cheat Sheet