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

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

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

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 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

PracticeReason
Use bcrypt for passwordsSlow by design, resists brute force
Never store plain text secretsEncrypted or hashed only
Use AES-GCM over AES-CBCProvides authentication + encryption
Rotate keys regularlyLimits damage from compromise
Use crypto/randPredictable random = broken security
Constant time comparisonsPrevents 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.
All Blogs
Tags:golangcryptographysecurityencryptionhashing