Skip to content

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), adequate key lengths (AES-256, RSA-2048/4096), and secure modes of operation (GCM, not ECB). Go's crypto package provides robust implementations, but developers must make correct choices about which algorithms and configurations to use.

Common mistakes include using deprecated algorithms (DES, RC4, MD5 for security), inadequate key sizes (AES-128 when AES-256 is appropriate, RSA-1024), 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 are critical in applications handling sensitive data: user PII, financial data, health records, authentication tokens, API keys, and database encryption. Weak encryption is often worse than no encryption - it provides a false sense of security while being trivially breakable with modern computing power. Even if data appears encrypted in the database, weak algorithms allow attackers who gain database access to decrypt everything. Compliance frameworks (PCI DSS, HIPAA, GDPR) mandate strong encryption, making weak crypto a regulatory violation.

Primary Defence: Use AES-256-GCM for symmetric encryption with crypto/rand-generated keys and nonces. Use RSA-OAEP with 2048+ bit keys or modern alternatives like X25519 for asymmetric encryption. 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. Furthermore, ECB 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:
// IV must be unique for each encryption with the same key
// Reusing IV allows attacker to XOR ciphertexts to reveal plaintext XOR
// If plaintext format is known, can decrypt messages

Why this is vulnerable: CBC mode requires a unique, unpredictable Initialization Vector (IV) for each message encrypted with the same key. Using a static IV completely breaks CBC's security - given two ciphertexts encrypted with the same key and IV, an attacker can XOR them together to get the XOR of the plaintexts. If one plaintext is known or has predictable structure (JSON, XML headers), the other can be partially or fully recovered. The IV doesn't need to be secret, but must be unique and random for each encryption. Additionally, CBC provides no authentication, allowing 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)  // Only 128 bits, vulnerable to collisions
    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
// Minimum should be 2048-bit, 4096-bit preferred for long-term secrets

Why this is vulnerable: RSA security depends on the difficulty of factoring large numbers. 1024-bit RSA keys can be factored by well-funded attackers (nation-states, large corporations) with significant computing resources. Academic demonstrations have shown RSA-768 factorization, making 1024-bit insecure. NIST deprecated 1024-bit RSA after 2013 and requires 2048-bit minimum. For data that must remain secure long-term (10+ years), 4096-bit keys are recommended. Using weak key sizes for sensitive data (financial transactions, health records, legal documents) is a serious vulnerability.

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 and side-channel attacks. It's specified in RFC 8439 and widely used (TLS 1.3, SSH, VPNs). Both AES-GCM and ChaCha20-Poly1305 are excellent choices 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 the winner of the Password Hashing Competition and the recommended algorithm for password-based key derivation. It uses memory-hard operations making GPU/ASIC attacks expensive. The salt (16 random bytes) ensures identical passwords produce different keys, preventing rainbow tables. The parameters (time/memory/parallelism) tune computational cost - these settings make each derivation take ~100ms, slowing brute force to thousands of attempts per second instead of billions. The 32-byte output is suitable for AES-256. Salt must be stored alongside the ciphertext for decryption. Argon2id is more secure than PBKDF2, bcrypt, or scrypt.

RSA-OAEP with Strong Keys

// SECURE - RSA-OAEP with 2048+ bit keys
import (
    "crypto/rand"
    "crypto/rsa"
    "crypto/sha256"
)

func generateSecureRSAKeys() (*rsa.PrivateKey, error) {
    // SECURE: 2048-bit minimum, 4096-bit for long-term security
    privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
    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: 2048-bit RSA keys provide strong security (current NIST standard), with 4096-bit for high-value long-term secrets. 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 (~256 bytes for 2048-bit keys), 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
import (
    "crypto/rand"
    "fmt"
    "io"

    "golang.org/x/crypto/curve25519"
)

func generateX25519KeyPair() (privateKey, publicKey []byte, error error) {
    // SECURE: Generate random private key
    privateKey = make([]byte, 32)
    if _, err := io.ReadFull(rand.Reader, privateKey); err != nil {
        return nil, nil, err
    }

    // Derive public key
    publicKey, err := curve25519.X25519(privateKey, curve25519.Basepoint)
    if err != nil {
        return nil, nil, err
    }

    return privateKey, publicKey, nil
}

func deriveSharedSecret(myPrivateKey, theirPublicKey []byte) ([]byte, error) {
    // SECURE: ECDH shared secret derivation
    sharedSecret, err := curve25519.X25519(myPrivateKey, theirPublicKey)
    if err != nil {
        return nil, err
    }

    // Check for low-order points
    var zero [32]byte
    if subtle.ConstantTimeCompare(sharedSecret, zero[:]) == 1 {
        return nil, fmt.Errorf("shared secret is zero")
    }

    return sharedSecret, nil
}

// Complete example: Ephemeral Diffie-Hellman
func performKeyExchange() ([]byte, error) {
    // Alice generates key pair
    alicePriv, alicePub, err := generateX25519KeyPair()
    if err != nil {
        return nil, err
    }

    // Bob generates key pair
    bobPriv, bobPub, err := generateX25519KeyPair()
    if err != nil {
        return nil, err
    }

    // Alice computes shared secret
    aliceShared, err := deriveSharedSecret(alicePriv, bobPub)
    if err != nil {
        return nil, err
    }

    // Bob computes shared secret (should match)
    bobShared, err := deriveSharedSecret(bobPriv, alicePub)
    if err != nil {
        return nil, err
    }

    // Both parties now have same shared secret
    // Use KDF to derive encryption keys
    return aliceShared, nil
}

Why this works: X25519 is a modern elliptic curve Diffie-Hellman (ECDH) function providing ~128-bit security level (equivalent to AES-256) with 256-bit keys. 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.

Additional Resources