Securing Your Secrets in Go: From DIY Disaster to Production-Ready
You’re building an API that stores OAuth tokens, third-party API keys, or sensitive user data. You need to encrypt them before persisting…
Securing Your Secrets in Go: From DIY Disaster to Production-Ready
You’re building an API that stores OAuth tokens, third-party API keys, or sensitive user data. You need to encrypt them before persisting to your database. How do you do it securely?
The real-world scenario
Imagine a SaaS application that syncs data with your customers’ Stripe APIs. For each customer account, you store:
type Account struct {
UserID int
StripeAPIKey string // ⚠️ Sensitive!
WebhookSecret string // ⚠️ Sensitive!
}
The problem: If your database leaks (SQL dump, unencrypted backup, breach), every customer’s Stripe keys are exposed. Game over.
The solution: Encrypt these fields before insertion. But how?
First attempt: the “homemade” solution
A common quick‑start shows an AES‑GCM example
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"errors"
"io"
)
// Encrypt encrypts plaintext with AES-GCM using a passphrase
func Encrypt(plaintext, passphrase string) (string, error) {
// Derive key from passphrase with SHA256
key := sha256.Sum256([]byte(passphrase))
block, err := aes.NewCipher(key[:])
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// Decrypt decrypts encrypted text
func Decrypt(ciphertext, passphrase string) (string, error) {
key := sha256.Sum256([]byte(passphrase))
data, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", err
}
block, err := aes.NewCipher(key[:])
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonceSize := gcm.NonceSize()
if len(data) < nonceSize {
return "", errors.New("ciphertext too short")
}
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return "", err
}
return string(plaintext), nil
}
func main() {
secret := "sk_live_51ABCxyz..."
passphrase := "my-super-password"
encrypted, _ := Encrypt(secret, passphrase)
println("Encrypted:", encrypted)
decrypted, _ := Decrypt(encrypted, passphrase)
println("Decrypted:", decrypted)
}
This code works… but it’s dangerous ⚠️
Problem #1: SHA256 is NOT a KDF
key := sha256.Sum256([]byte(passphrase))
SHA256 is a fast hash function. That’s its job. But for deriving encryption keys, it’s catastrophic:
- An attacker can test billions of passphrases per second on a GPU.
- Even with a “correct” password (
P@ssw0rd123), it falls in hours. - Rainbow tables and dictionary attacks are trivial.
Problem #2: No salt
Two users with the same passphrase will produce the same AES key. An attacker can:
- Detect duplicates in your database.
- Crack one key and decrypt all identical records.
Problem #3: Memory management
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
return string(plaintext), nil
Passphrases and derived keys remain in memory. A RAM dump or crash dump can expose them.
Problem #4: Upgradeability
If you want to strengthen security in 6 months (use Argon2, increase iterations), all your old ciphertexts become incompatible. No versioning, no metadata.
The right approach: cryptio
Here’s the same code, rewritten with cryptio:
package main
import (
"fmt"
"github.com/azrod/cryptio"
)
func main() {
// Create a client with appropriate security level
client, err := cryptio.New(
"my-super-password",
cryptio.SecurityStandard, // OWASP-compliant
cryptio.ProfileBalanced, // RAM/CPU balance
)
if err != nil {
panic(err)
}
defer client.Wipe() // Clean passphrase from memory
// Encrypt
secret := "sk_live_51ABCxyz..."
encrypted, err := client.Encrypt(secret)
if err != nil {
panic(err)
}
fmt.Println("Encrypted:", encrypted)
// Decrypt
decrypted, err := client.Decrypt(encrypted)
if err != nil {
panic(err)
}
fmt.Println("Decrypted:", decrypted)
}
That’s it. 3 lines to encrypt, 3 lines to decrypt.
Why cryptio is secure
1. Argon2id: the modern KDF reference
Instead of SHA256, cryptio uses Argon2id (winner of the Password Hashing Competition):
- GPU/ASIC resistant: uses lots of memory (~64 MB in Standard mode).
- Slow by design: an attacker can only test ~10–100 passphrases/second (vs billions with SHA256).
- Recommended by OWASP and NIST.
// cryptio automatically generates:
salt := random(16 bytes) // Unique per message
key := argon2.IDKey(
passphrase,
salt,
time: 2, // Iterations
memory: 64*1024, // 64 MB
threads: 1,
)
2. Random salt for each encryption
Every call to Encrypt() generates a new cryptographically secure salt. Same message + same passphrase = **different*** ciphertext every time.
// Format: salt (16 bytes) || nonce (12 bytes) || ciphertext
encrypted1, _ := client.Encrypt("secret")
encrypted2, _ := client.Encrypt("secret")
// encrypted1 ≠ encrypted2 (but both decrypt to "secret")
3. Memory cleanup
defer client.Wipe()
Erases the passphrase from memory at the end. Derived keys are also cleaned after each operation (explicit zeroing).
4. Adaptive security levels
You can choose the level based on your context:
- SecurityUltraFast (Use Case: Tests/IoT, RAM: ~16 MB, Time: ~30 ms)
- SecurityStandard (Use Case: APIs/SaaS, RAM: ~64 MB, Time: ~80 ms)
- SecurityMedium (Use Case: Enterprise/compliance, RAM: ~128 MB, Time: ~140 ms)
- SecurityHigh (Use Case: Finance/healthcare, RAM: ~256 MB, Time: ~390 ms)
- SecurityExtreme (Use Case: Vault/ultra-sensitive, RAM: ~1 GB, Time: >1.2 s)
For a typical web API, SecurityStandard is perfect. For storing infrastructure secrets, use SecurityHigh.
5. Argon2 profiles to adjust RAM vs CPU
// Environment with lots of RAM?
client, _ := cryptio.New(pass, cryptio.SecurityStandard, cryptio.ProfileRAMHeavy)
// RAM-constrained environment?
client, _ := cryptio.New(pass, cryptio.SecurityStandard, cryptio.ProfileCPUHeavy)
Profiles let you trade memory for CPU without compromising security.
Conclusion
Encrypting secrets is not trivial. “Homemade” solutions are riddled with cryptographic traps that seem to work… until the day an attacker breaks everything in a few hours.
cryptio gives you:
- A simple API (3 lines of code).
- Modern, proven primitives (Argon2id + AES-GCM).
- Clear, documented security levels.
- Minimal dependencies (only
golang.org/x/crypto).
To install:
go get github.com/azrod/cryptio
Full documentation: github.com/azrod/cryptio
Security note: This article presents general best practices. Always have your cryptographic choices audited by a security team before production deployment.
메타데이터
- post_id
- 3804eaf5e35a
- slug
- securing-your-secrets-in-go-from-diy-disaster-to-production-ready-3804eaf5e35a
- url
- https://medium.com/@azrod/securing-your-secrets-in-go-from-diy-disaster-to-production-ready-3804eaf5e35a
- canonical_url
- https://medium.com/@azrod/securing-your-secrets-in-go-from-diy-disaster-to-production-ready-3804eaf5e35a
- author_url
- https://medium.com/@azrod
- status
- ok
- fetched_at
- 2026-07-13 13:01:55