Go’s crypto package provides battle-tested implementations of modern cryptographic primitives. The right primitives for most applications: AES-GCM for symmetric encryption, bcrypt for passwords, HMAC-SHA256 for message authentication, SHA-256 for content hashing, and ECDSA or RSA for digital signatures.
The most important rule: always use crypto/rand for randomness — never math/rand. And always use authenticated encryption (AES-GCM, not AES-CBC) — unauthenticated ciphers are vulnerable to tampering.
For TLS configuration see Go HTTPS and TLS. For authentication patterns see Go authentication and authorization.
Secure Randomness
The foundation of all cryptography is unpredictable randomness. crypto/rand reads from the OS’s CSPRNG:
import "crypto/rand"
// Generate a random 32-byte key
key := make([]byte, 32)
if _, err := rand.Read(key); err != nil {
log.Fatal("failed to generate random key:", err)
}
// Generate a secure token (hex-encoded for readability)
tokenBytes := make([]byte, 32)
rand.Read(tokenBytes)
token := hex.EncodeToString(tokenBytes) // 64-char hex string
// ❌ Never use math/rand for security purposes
import mathrand "math/rand"
mathrand.Read(key) // predictable with known seed
Hashing: SHA-256 and SHA-3
Hash functions produce a fixed-size fingerprint of data. Use them for content integrity checks, checksums, and as building blocks for signatures and MACs:
import (
"crypto/sha256"
"encoding/hex"
"io"
"os"
)
// Hash a string
data := []byte("important data")
hash := sha256.Sum256(data)
fmt.Printf("SHA-256: %x\n", hash) // [32]byte as hex
// Hash a large file without loading it all into memory
func hashFile(path string) (string, error) {
f, err := os.Open(path)
if err != nil { return "", err }
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
SHA-256 is safe for integrity checks. For password hashing, SHA-256 is not appropriate — use bcrypt (see below). For newer systems, SHA-3/SHAKE256 from golang.org/x/crypto/sha3 is available.
HMAC: Message Authentication
An HMAC (Hash-based Message Authentication Code) proves both the integrity and the origin of a message. Use it to authenticate webhook payloads, API request signatures, and session tokens:
import (
"crypto/hmac"
"crypto/sha256"
)
var hmacKey = []byte(os.Getenv("HMAC_SECRET")) // 32+ random bytes
func sign(message []byte) []byte {
mac := hmac.New(sha256.New, hmacKey)
mac.Write(message)
return mac.Sum(nil)
}
func verify(message, sig []byte) bool {
expected := sign(message)
// Use hmac.Equal — constant-time comparison prevents timing attacks
return hmac.Equal(sig, expected)
}
// Verify a GitHub webhook
func verifyGitHubWebhook(body []byte, signature string) bool {
sig := sign(body)
expected := "sha256=" + hex.EncodeToString(sig)
return hmac.Equal([]byte(signature), []byte(expected))
}
hmac.Equal is critical — never compare MACs with == or bytes.Equal. Regular comparison short-circuits on the first mismatch, leaking timing information that can be exploited to forge valid MACs.
AES-GCM: Authenticated Symmetric Encryption
AES-GCM is the correct mode for symmetric encryption. It provides confidentiality (no one can read the data) and authenticity (tampering is detected). Never use AES-CBC or AES-ECB alone — they don’t provide authentication:
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
)
func encrypt(plaintext, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key) // key must be 16, 24, or 32 bytes
if err != nil { return nil, err }
gcm, err := cipher.NewGCM(block)
if err != nil { return nil, err }
// Nonce must be unique per (key, message) pair — crypto/rand guarantees this
nonce := make([]byte, gcm.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return nil, err
}
// Seal appends ciphertext to nonce: [nonce | ciphertext+tag]
return gcm.Seal(nonce, nonce, plaintext, nil), nil
}
func decrypt(ciphertext, key []byte) ([]byte, error) {
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")
}
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
return gcm.Open(nil, nonce, ciphertext, nil)
// Open returns an error if authentication fails — data was tampered
}
// Usage
key := make([]byte, 32) // AES-256
rand.Read(key)
encrypted, err := encrypt([]byte("secret data"), key)
decrypted, err := decrypt(encrypted, key)
The nonce is prepended to the ciphertext so the receiver has everything needed to decrypt. Generate a fresh random nonce for each encryption — reusing a nonce with the same key is catastrophic for GCM security.
Password Hashing with bcrypt
Never hash passwords with SHA-256 or similar fast hash functions — they can be brute-forced at billions of hashes per second. bcrypt is deliberately slow and includes a salt automatically:
import "golang.org/x/crypto/bcrypt"
func hashPassword(password string) (string, error) {
// Cost 12 ≈ 250ms per hash on modern hardware — acceptable for login
// Increase as hardware gets faster
hash, err := bcrypt.GenerateFromPassword([]byte(password), 12)
return string(hash), err
}
func checkPassword(hash, password string) error {
// Returns nil if match, bcrypt.ErrMismatchedHashAndPassword otherwise
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
}
// Login handler
func handleLogin(w http.ResponseWriter, r *http.Request) {
email, password := r.FormValue("email"), r.FormValue("password")
user, err := db.GetUserByEmail(r.Context(), email)
if err != nil || bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)) != nil {
// Same error for "user not found" and "wrong password" — prevents enumeration
http.Error(w, "invalid credentials", http.StatusUnauthorized)
return
}
// issue session...
}
For new systems, consider Argon2id from golang.org/x/crypto/argon2 — it’s the modern recommendation, memory-hard and resistant to GPU attacks. bcrypt is still secure and widely supported.
RSA Signatures
RSA signatures prove the message was created by someone with the private key. The verifier only needs the public key:
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
)
func generateRSAKey() (*rsa.PrivateKey, error) {
return rsa.GenerateKey(rand.Reader, 2048) // 2048 minimum; prefer 4096 for long-lived keys
}
func sign(data []byte, priv *rsa.PrivateKey) ([]byte, error) {
hash := sha256.Sum256(data)
return rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA256, hash[:])
}
func verify(data, sig []byte, pub *rsa.PublicKey) error {
hash := sha256.Sum256(data)
return rsa.VerifyPKCS1v15(pub, crypto.SHA256, hash[:], sig)
}
For new systems, prefer ECDSA (smaller keys, same security level) or Ed25519 (crypto/ed25519, fastest, safest API):
import "crypto/ed25519"
pubKey, privKey, _ := ed25519.GenerateKey(rand.Reader)
sig := ed25519.Sign(privKey, message)
valid := ed25519.Verify(pubKey, message, sig) // true or false
Ed25519 has a simpler API — no hash to pre-compute, no digest type to specify, constant-time by design.
Key Management
Store encryption keys in environment variables or a secrets manager, never in code or config files:
// Load key from environment
keyHex := os.Getenv("ENCRYPTION_KEY")
if keyHex == "" {
log.Fatal("ENCRYPTION_KEY not set")
}
key, err := hex.DecodeString(keyHex)
if err != nil || len(key) != 32 {
log.Fatal("ENCRYPTION_KEY must be 32 bytes hex-encoded (64 hex chars)")
}
// Generate a new key for first-time setup
key := make([]byte, 32)
rand.Read(key)
fmt.Println("Set ENCRYPTION_KEY=" + hex.EncodeToString(key))
For production, use a KMS (AWS KMS, GCP Cloud KMS, HashiCorp Vault) — envelope encryption wraps your data key with a master key managed by the service, enabling key rotation without re-encrypting all data.
Summary
- Always use
crypto/randfor randomness —math/randis predictable - AES-GCM for symmetric encryption — authenticated (detects tampering), uses a unique random nonce per message
hmac.Equalfor MAC comparison — prevents timing attacks; never use==orbytes.Equal- bcrypt at cost 12+ for passwords — slow by design; Argon2id is the modern alternative
- Ed25519 for digital signatures in new code — simpler, faster, safer API than RSA
- Keys belong in environment variables or a KMS, never hardcoded
Comments