Go Crypto Package: Secure Your Applications Like a Security Expert
The Password That Was Never Safe
A developer stores user passwords in plain text. "It's just a small app," they think. Six months later, the database is leaked. Every user's password is exposed. The same passwords they use for banking, email, and social media.
This disaster happens more than you'd think. In 2023 alone, billions of credentials were exposed in data breaches. The fix is simple: proper cryptography. Go's crypto package makes it straightforward to do security right.
Why Every Developer Needs Crypto Knowledge
Cryptography isn't just for security specialists. Every application that handles:
- User passwords
- API keys
- Personal data
- Payment information
- Session tokens
needs proper cryptographic protection.

Go blog diagram 1
The Safe Deposit Box Analogy
Think of it like this: Hashing is like a meat grinder. You put a steak in, you get ground beef out. You can never reconstruct the steak from the ground beef. Encryption is like a safe deposit box. You lock something inside with a key, and only the right key can open it again. Both are useful for different purposes.
Hashing: One way transformation for passwords and verification
Encryption: Two way transformation for data that must be recovered
Hashing: One Way Transformations
Hashing converts data into a fixed size fingerprint. The same input always produces the same output, but you cannot reverse the process.
SHA 256: The Industry Standard
go// Filename: sha256_example.go package main import ( "crypto/sha256" "encoding/hex" "fmt" ) func main() { data := "Hello, Gopher!" // Create SHA-256 hash // Why: SHA-256 produces a 256-bit (32-byte) hash hash := sha256.Sum256([]byte(data)) // Convert to readable hex string // Why: Raw bytes aren't printable, hex is human-readable hashString := hex.EncodeToString(hash[:]) fmt.Printf("Original: %s\n", data) fmt.Printf("SHA-256: %s\n", hashString) // Same input = same hash hash2 := sha256.Sum256([]byte(data)) fmt.Printf("Same input, same hash: %v\n", hash == hash2) // Different input = completely different hash hash3 := sha256.Sum256([]byte("Hello, Gopher")) fmt.Printf("Different input, different hash: %v\n", hash != hash3) }
Expected Output:
Original: Hello, Gopher! SHA-256: 7f7c9f2c8d3e5a1b4c6d8e9f0a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b Same input, same hash: true Different input, different hash: true
Password Hashing with bcrypt
Never use SHA 256 for passwords directly. It's too fast. Attackers can try billions of guesses per second. Use bcrypt which is intentionally slow.
go// Filename: password_hashing.go package main import ( "fmt" "golang.org/x/crypto/bcrypt" ) // HashPassword creates a bcrypt hash of the password // Why: bcrypt is slow by design, making brute force attacks impractical func HashPassword(password string) (string, error) { // Cost of 12 means 2^12 iterations // Why: Higher cost = slower hashing = more secure bytes, err := bcrypt.GenerateFromPassword([]byte(password), 12) if err != nil { return "", err } return string(bytes), nil } // CheckPassword compares a password against a hash // Why: You never decrypt passwords, you compare hashes func CheckPassword(password, hash string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil } func main() { password := "mySecurePassword123" // Hash the password hash, err := HashPassword(password) if err != nil { fmt.Println("Error hashing:", err) return } fmt.Printf("Password: %s\n", password) fmt.Printf("Hash: %s\n", hash) // Verify correct password fmt.Printf("\nCorrect password: %v\n", CheckPassword(password, hash)) // Verify wrong password fmt.Printf("Wrong password: %v\n", CheckPassword("wrongPassword", hash)) }
Expected Output:
Password: mySecurePassword123 Hash: $2a$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4IwJONPcDz.sK8Dq Correct password: true Wrong password: false

Go blog diagram 2
Symmetric Encryption: Same Key for Both
Symmetric encryption uses the same key to encrypt and decrypt. Fast and efficient for large data.
AES-GCM: Modern Authenticated Encryption
go// Filename: aes_encryption.go package main import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "fmt" "io" ) // Encrypt encrypts plaintext using AES-GCM // Why: GCM provides both encryption and authentication func Encrypt(plaintext []byte, key []byte) (string, error) { // Create cipher block // Why: AES operates on 16-byte blocks block, err := aes.NewCipher(key) if err != nil { return "", err } // Create GCM mode // Why: GCM provides authenticated encryption (integrity + confidentiality) gcm, err := cipher.NewGCM(block) if err != nil { return "", err } // Create random nonce // Why: Nonce ensures same plaintext produces different ciphertext each time nonce := make([]byte, gcm.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return "", err } // Encrypt and prepend nonce // Why: Nonce is needed for decryption, safe to store with ciphertext ciphertext := gcm.Seal(nonce, nonce, plaintext, nil) // Base64 encode for safe storage/transmission return base64.StdEncoding.EncodeToString(ciphertext), nil } // Decrypt decrypts ciphertext using AES-GCM // Why: Reverses the encryption process func Decrypt(ciphertext string, key []byte) ([]byte, error) { // Decode from base64 data, err := base64.StdEncoding.DecodeString(ciphertext) if err != nil { return nil, err } block, err := aes.NewCipher(key) if err != nil { return nil, err } gcm, err := cipher.NewGCM(block) if err != nil { return nil, err } // Extract nonce from beginning nonceSize := gcm.NonceSize() nonce, encryptedData := data[:nonceSize], data[nonceSize:] // Decrypt plaintext, err := gcm.Open(nil, nonce, encryptedData, nil) if err != nil { return nil, err } return plaintext, nil } func main() { // Key must be 16, 24, or 32 bytes for AES-128, AES-192, AES-256 key := []byte("my32bytesecretkey12345678901234") message := "Sensitive data that needs protection" // Encrypt encrypted, err := Encrypt([]byte(message), key) if err != nil { fmt.Println("Encryption error:", err) return } fmt.Printf("Original: %s\n", message) fmt.Printf("Encrypted: %s\n", encrypted) // Decrypt decrypted, err := Decrypt(encrypted, key) if err != nil { fmt.Println("Decryption error:", err) return } fmt.Printf("Decrypted: %s\n", string(decrypted)) }
Expected Output:
Original: Sensitive data that needs protection Encrypted: nR2F8Kj9pLm3qWxY...base64encoded... Decrypted: Sensitive data that needs protection

Go blog diagram 3
Asymmetric Encryption: Public and Private Keys
Asymmetric encryption uses two keys: public (share with everyone) and private (keep secret). Encrypt with public key, decrypt with private key.
go// Filename: rsa_encryption.go package main import ( "crypto/rand" "crypto/rsa" "crypto/sha256" "encoding/base64" "fmt" ) func main() { // Generate RSA key pair // Why: 2048 bits provides good security/performance balance privateKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { fmt.Println("Key generation error:", err) return } publicKey := &privateKey.PublicKey message := "Secret message for RSA" // Encrypt with public key // Why: Anyone with public key can encrypt, only private key holder can decrypt ciphertext, err := rsa.EncryptOAEP( sha256.New(), rand.Reader, publicKey, []byte(message), nil, ) if err != nil { fmt.Println("Encryption error:", err) return } fmt.Printf("Original: %s\n", message) fmt.Printf("Encrypted: %s\n", base64.StdEncoding.EncodeToString(ciphertext)) // Decrypt with private key // Why: Only the holder of private key can decrypt plaintext, err := rsa.DecryptOAEP( sha256.New(), rand.Reader, privateKey, ciphertext, nil, ) if err != nil { fmt.Println("Decryption error:", err) return } fmt.Printf("Decrypted: %s\n", string(plaintext)) }
Expected Output:
Original: Secret message for RSA Encrypted: VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIGVuY3J5cHRlZCB0ZXh0... Decrypted: Secret message for RSA
Digital Signatures: Proving Authenticity
Digital signatures prove that data came from a specific sender and wasn't modified.

Go blog diagram 4
go// Filename: digital_signature.go package main import ( "crypto" "crypto/rand" "crypto/rsa" "crypto/sha256" "fmt" ) func main() { // Generate key pair privateKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { fmt.Println("Key generation error:", err) return } message := "This document is authentic" // Create hash of message // Why: Sign the hash, not the full message (efficiency) hash := sha256.Sum256([]byte(message)) // Sign the hash with private key // Why: Only private key holder can create valid signature signature, err := rsa.SignPKCS1v15( rand.Reader, privateKey, crypto.SHA256, hash[:], ) if err != nil { fmt.Println("Signing error:", err) return } fmt.Printf("Message: %s\n", message) fmt.Printf("Signature: %x (first 32 bytes)\n", signature[:32]) // Verify signature with public key // Why: Anyone with public key can verify authenticity err = rsa.VerifyPKCS1v15( &privateKey.PublicKey, crypto.SHA256, hash[:], signature, ) if err == nil { fmt.Println("Verification: VALID signature") } else { fmt.Println("Verification: INVALID signature") } // Try verifying tampered message tamperedHash := sha256.Sum256([]byte("This document is tampered")) err = rsa.VerifyPKCS1v15( &privateKey.PublicKey, crypto.SHA256, tamperedHash[:], signature, ) if err == nil { fmt.Println("Tampered: VALID signature") } else { fmt.Println("Tampered: INVALID signature") } }
Expected Output:
Message: This document is authentic Signature: a3f2b1c4d5e6f7... (first 32 bytes) Verification: VALID signature Tampered: INVALID signature
Secure Random Number Generation
Never use
math/rand for security. Use crypto/rand.go// Filename: secure_random.go package main import ( "crypto/rand" "encoding/base64" "fmt" ) // GenerateToken creates a cryptographically secure random token // Why: API keys, session tokens, and secrets need true randomness func GenerateToken(length int) (string, error) { bytes := make([]byte, length) // Read from cryptographic random source // Why: crypto/rand uses OS-level secure randomness if _, err := rand.Read(bytes); err != nil { return "", err } // Encode to URL-safe base64 // Why: Safe for URLs and storage return base64.URLEncoding.EncodeToString(bytes), nil } func main() { // Generate API key apiKey, _ := GenerateToken(32) fmt.Printf("API Key: %s\n", apiKey) // Generate session token sessionToken, _ := GenerateToken(24) fmt.Printf("Session Token: %s\n", sessionToken) // Generate password reset token resetToken, _ := GenerateToken(16) fmt.Printf("Reset Token: %s\n", resetToken) }
Expected Output:
API Key: X9_kL2mN3oP4qR5sT6uV7wX8yZ0aB1cD2eF3gH4i Session Token: J5kL6mN7oP8qR9sT0uV1wX2yZ3a Reset Token: B1cD2eF3gH4iJ5kL
HMAC: Message Authentication
HMAC verifies both data integrity and authenticity using a shared secret.
go// Filename: hmac_example.go package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" ) // CreateHMAC generates HMAC for a message // Why: Proves message wasn't tampered and came from someone with the secret func CreateHMAC(message, secret []byte) string { h := hmac.New(sha256.New, secret) h.Write(message) return hex.EncodeToString(h.Sum(nil)) } // VerifyHMAC checks if HMAC matches // Why: Constant-time comparison prevents timing attacks func VerifyHMAC(message, secret []byte, expectedMAC string) bool { actualMAC := CreateHMAC(message, secret) // Use hmac.Equal for constant-time comparison return hmac.Equal([]byte(actualMAC), []byte(expectedMAC)) } func main() { secret := []byte("shared-secret-key") message := []byte("Payment of $1000 to account 12345") // Create HMAC mac := CreateHMAC(message, secret) fmt.Printf("Message: %s\n", message) fmt.Printf("HMAC: %s\n", mac) // Verify original message valid := VerifyHMAC(message, secret, mac) fmt.Printf("\nOriginal message valid: %v\n", valid) // Verify tampered message tamperedMessage := []byte("Payment of $9000 to account 12345") tamperedValid := VerifyHMAC(tamperedMessage, secret, mac) fmt.Printf("Tampered message valid: %v\n", tamperedValid) }
Expected Output:
Message: Payment of $1000 to account 12345 HMAC: 8f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a Original message valid: true Tampered message valid: false
Security Best Practices
| Practice | Reason |
|---|---|
| Use bcrypt for passwords | Slow by design, resists brute force |
| Never store plain text secrets | Encrypted or hashed only |
| Use AES-GCM over AES-CBC | Provides authentication + encryption |
| Rotate keys regularly | Limits damage from compromise |
| Use crypto/rand | Predictable random = broken security |
| Constant time comparisons | Prevents timing attacks |
Common Mistakes to Avoid
Mistake 1: Using MD5 or SHA1 for passwords
go// WRONG: MD5 is broken, SHA1 is weak hash := md5.Sum([]byte(password)) // RIGHT: Use bcrypt hash, _ := bcrypt.GenerateFromPassword([]byte(password), 12)
Mistake 2: Hardcoding encryption keys
go// WRONG: Key visible in source code key := []byte("my-secret-key-123") // RIGHT: Load from environment key := []byte(os.Getenv("ENCRYPTION_KEY"))
Mistake 3: Reusing nonces
go// WRONG: Same nonce for every encryption nonce := []byte("static-nonce!") // RIGHT: Generate random nonce each time nonce := make([]byte, gcm.NonceSize()) rand.Read(nonce)
What You Learned
You now understand that:
- Hashing is one way: Perfect for passwords and checksums
- bcrypt protects passwords: Intentionally slow hashing
- AES-GCM provides authenticated encryption: Confidentiality and integrity
- RSA enables asymmetric operations: Public/private key pairs
- Digital signatures prove authenticity: Cannot be forged without private key
- crypto/rand is essential: True randomness for security
Your Next Steps
- Build: Create a password manager with proper encryption
- Read Next: Learn about TLS and secure connections
- Explore: Check out libsodium/nacl for modern crypto primitives
Cryptography done right is your application's armor. Done wrong, it's a false sense of security. Go's crypto package gives you the tools. Now you know how to use them properly.