Crypto #
Cryptography is the foundation of modern application security — data integrity verification, password hashing, unpredictable tokens, and digital signatures all depend on correctly implemented cryptographic primitives. Go provides a complete crypto ecosystem in the standard library: crypto/sha256 and crypto/md5 for hashing, crypto/rand for cryptographically secure random numbers, crypto/hmac for message authentication, and crypto/subtle for operations safe against timing attacks. What makes Go’s crypto packages special is a design that encourages correct usage — for example crypto/rand is explicitly distinct from math/rand, which isn’t secure for cryptography. This article covers everything you need to know to implement correct cryptography in production Go applications.
An Overview of the crypto Packages #
flowchart TD
Crypto["package crypto\n(and sub-packages)"] --> Hash["Hashing"]
Crypto --> Rand["Random"]
Crypto --> Auth["Authentication"]
Crypto --> Sym["Symmetric Encryption"]
Crypto --> Asym["Asymmetric Encryption"]
Hash --> SHA256["crypto/sha256\nSHA-256, SHA-224"]
Hash --> SHA512["crypto/sha512\nSHA-512, SHA-384"]
Hash --> MD5["crypto/md5\nMD5 (DON'T for security)"]
Hash --> SHA1["crypto/sha1\nSHA-1 (deprecated for security)"]
Rand --> CR["crypto/rand\nCryptographically secure random\nuse this, not math/rand"]
Auth --> HMAC["crypto/hmac\nHMAC for integrity verification"]
Auth --> BCrypt["golang.org/x/crypto/bcrypt\npassword hashing"]
Auth --> Subtle["crypto/subtle\nconstant-time comparison"]
Sym --> AES["crypto/aes\nAES cipher"]
Sym --> Cipher["crypto/cipher\nGCM, CBC, CTR modes"]
Asym --> RSA["crypto/rsa\nRSA encryption & signatures"]
Asym --> ECDSA["crypto/ecdsa\nElliptic Curve DSA"]
Asym --> ED["crypto/ed25519\nEdDSA (modern, fast)"]
style Crypto fill:#4f86c6,color:#fff
style Hash fill:#e8f5e9
style Rand fill:#e3f2fd
style Auth fill:#fff3e0
style Sym fill:#f3e5f5
style Asym fill:#fce4eccrypto/sha256 — Secure Hashing #
SHA-256 is the most commonly used cryptographic hash function today — it produces a deterministic, irreversible 32-byte (256-bit) digest. It’s used for file integrity verification, data fingerprints, and as a component in larger cryptographic systems.
package main
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
)
func main() {
// Way 1: hash a short string all at once
data := []byte("Hello, World!")
hash := sha256.Sum256(data)
// hash is a [32]byte — an array, not a slice
fmt.Printf("SHA-256: %x\n", hash)
// SHA-256: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
// Convert to a hex string
hashHex := hex.EncodeToString(hash[:])
fmt.Println(hashHex)
// Way 2: hash large data streaming (memory-efficient)
f, err := os.Open("large-file.bin")
if err == nil {
defer f.Close()
h := sha256.New() // create a new hasher
if _, err := io.Copy(h, f); err != nil {
fmt.Println("error hashing file:", err)
}
hashFile := h.Sum(nil) // Sum(nil) returns the hash as []byte
fmt.Printf("File hash: %x\n", hashFile)
}
// Way 3: incremental hashing — add data piece by piece
h := sha256.New()
h.Write([]byte("first part"))
h.Write([]byte(" "))
h.Write([]byte("second part"))
result := h.Sum(nil)
fmt.Printf("Incremental hash: %x\n", result)
// The result is the same as:
combined := sha256.Sum256([]byte("first part second part"))
fmt.Printf("Combined hash: %x\n", combined)
fmt.Println("Same?", result != nil &&
hex.EncodeToString(result) == hex.EncodeToString(combined[:]))
}
SHA-224, SHA-384, SHA-512 #
import (
"crypto/sha256"
"crypto/sha512"
)
// SHA-224 (28-byte output)
hash224 := sha256.New224()
hash224.Write([]byte("data"))
fmt.Printf("SHA-224: %x\n", hash224.Sum(nil))
// SHA-384 (48-byte output)
hash384 := sha512.New384()
hash384.Write([]byte("data"))
fmt.Printf("SHA-384: %x\n", hash384.Sum(nil))
// SHA-512 (64-byte output)
hash512 := sha512.New()
hash512.Write([]byte("data"))
fmt.Printf("SHA-512: %x\n", hash512.Sum(nil))
// SHA-512/256 — SHA-512 truncated to 256 bits (faster on 64-bit CPUs)
hash512_256 := sha512.New512_256()
hash512_256.Write([]byte("data"))
fmt.Printf("SHA-512/256: %x\n", hash512_256.Sum(nil))
crypto/md5 — For Non-Security Use Only #
MD5 produces a 16-byte (128-bit) hash. It’s not secure for cryptography because it’s vulnerable to collision attacks — two different inputs can produce the same hash. Use MD5 only for non-security checksums like change detection or cache keys:
flowchart LR
subgraph Safe["✓ Use SHA-256"]
S1["Integrity verification of\nimportant data"]
S2["Fingerprints for\nsecurity"]
S3["Components in\ncryptographic systems"]
S4["Tokens and IDs\nthat must be unique and secure"]
end
subgraph NotSafe["⚠ MD5 — only for this"]
T1["Cache keys\n(not for security)"]
T2["HTTP ETags\n(resource identification)"]
T3["Simple checksums\n(not for security)"]
T4["Deduplication IDs\n(not for security)"]
end
subgraph Never["✗ NEVER use MD5 for"]
J1["Password hashing"]
J2["Identity verification"]
J3["Digital signatures"]
J4["Authentication tokens"]
end
style Safe fill:#e8f5e9
style NotSafe fill:#fff3e0
style Never fill:#fce4ecimport "crypto/md5"
// MD5 for cache keys — not for security!
func makeCacheKey(data []byte) string {
hash := md5.Sum(data)
return hex.EncodeToString(hash[:])
}
// MD5 for HTTP ETags
func computeETag(content []byte) string {
hash := md5.Sum(content)
return fmt.Sprintf(`"%x"`, hash)
}
// Streaming MD5 for large files
func md5File(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
h := md5.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
crypto/rand — Secure Randomness #
crypto/rand generates cryptographically secure random data — using the operating system’s entropy source (/dev/urandom on Linux, CryptGenRandom on Windows). This is different from math/rand, which uses a PRNG (Pseudo-Random Number Generator) whose output can be predicted:
flowchart LR
subgraph MathRand["math/rand — DON'T use for crypto"]
M1["Seeded from a fixed value\nor time.Now()"]
M2["Output is predictable\nif the seed is known"]
M3["Fast — suitable for\nsimulation, games, tests"]
M4["NOT SECURE for\ntokens, passwords, keys"]
end
subgraph CryptoRand["crypto/rand — USE this"]
C1["From OS entropy\n(/dev/urandom, CryptGenRandom)"]
C2["Output can't be predicted\neven with access to previous state"]
C3["Slightly slower\nbut secure"]
C4["SECURE for tokens,\npasswords, cryptographic keys"]
end
style MathRand fill:#fce4ec
style CryptoRand fill:#e8f5e9import (
"crypto/rand"
"math/big"
)
// Read random bytes directly
func randomBytes(n int) ([]byte, error) {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
return nil, fmt.Errorf("randomBytes: %w", err)
}
return b, nil
}
// Generate a hex token — for session IDs, API keys, password resets
func generateToken(length int) (string, error) {
b, err := randomBytes(length)
if err != nil {
return "", err
}
return hex.EncodeToString(b), nil // string length = length*2
}
// Generate a URL-safe base64 token
func generateTokenBase64(length int) (string, error) {
b, err := randomBytes(length)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}
// Random integer in the range [0, max)
func randomInt(max int64) (int64, error) {
n, err := rand.Int(rand.Reader, big.NewInt(max))
if err != nil {
return 0, fmt.Errorf("randomInt: %w", err)
}
return n.Int64(), nil
}
// UUID v4 — a secure unique identifier
func generateUUID() (string, error) {
b, err := randomBytes(16)
if err != nil {
return "", err
}
// Set version 4
b[6] = (b[6] & 0x0f) | 0x40
// Set variant bits
b[8] = (b[8] & 0x3f) | 0x80
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
b[0:4], b[4:6], b[6:8], b[8:10], b[10:]), nil
}
// Usage examples
func main() {
// A 32-byte token = 64 hex characters
token, _ := generateToken(32)
fmt.Println("Token:", token)
// Token: a3f2c1d8e9b7a6f5...
// A random number for games or sampling
n, _ := randomInt(100)
fmt.Println("Random 0-99:", n)
// UUID
uuid, _ := generateUUID()
fmt.Println("UUID:", uuid)
// UUID: 550e8400-e29b-41d4-a716-446655440000
}
A Secure Random Password #
const (
charLetters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
charDigits = "0123456789"
charSymbols = "!@#$%^&*()_+-=[]{}|;':\",./<>?"
charAll = charLetters + charDigits + charSymbols
)
func generatePassword(length int, useSymbols bool) (string, error) {
charset := charLetters + charDigits
if useSymbols {
charset = charAll
}
password := make([]byte, length)
for i := range password {
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
if err != nil {
return "", fmt.Errorf("generatePassword: %w", err)
}
password[i] = charset[n.Int64()]
}
return string(password), nil
}
// A 6-digit OTP (One-Time Password)
func generateOTP() (string, error) {
n, err := rand.Int(rand.Reader, big.NewInt(1000000))
if err != nil {
return "", err
}
return fmt.Sprintf("%06d", n.Int64()), nil
}
crypto/hmac — Integrity Verification #
HMAC (Hash-based Message Authentication Code) uses a secret key to produce an authentication tag — verifying that the message wasn’t modified AND comes from a party knowing the secret key:
sequenceDiagram
participant Sender as Sender (knows the key)
participant Msg as Message
participant Receiver as Receiver (knows the key)
Sender->>Msg: HMAC(key, data) → tag
Sender->>Receiver: send (data + tag)
Receiver->>Receiver: recompute HMAC(key, data) → tag2
Receiver->>Receiver: compare tag vs tag2\n(constant-time!)
alt tag == tag2
Receiver-->>Receiver: ✓ data valid & authentic
else tag != tag2
Receiver-->>Receiver: ✗ data modified or\nnot from the legitimate sender
endimport (
"crypto/hmac"
"crypto/sha256"
)
// Create an HMAC tag
func makeHMAC(key, data []byte) []byte {
mac := hmac.New(sha256.New, key)
mac.Write(data)
return mac.Sum(nil)
}
// Verify an HMAC — MUST use hmac.Equal, not bytes.Equal!
func verifyHMAC(key, data, tag []byte) bool {
mac := hmac.New(sha256.New, key)
mac.Write(data)
expectedTag := mac.Sum(nil)
// hmac.Equal uses constant-time comparison
// to prevent timing attacks
return hmac.Equal(expectedTag, tag)
}
// Pattern: a signed webhook token
func makeWebhookToken(payload []byte, secret string) string {
tag := makeHMAC([]byte(secret), payload)
return "sha256=" + hex.EncodeToString(tag)
}
func verifyWebhook(payload []byte, secret, signature string) bool {
expected := makeWebhookToken(payload, secret)
// constant-time comparison for the whole string
return hmac.Equal([]byte(expected), []byte(signature))
}
// Usage in an HTTP handler for GitHub/Stripe webhooks
func webhookHandler(w http.ResponseWriter, r *http.Request) {
payload, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) // max 1MB
if err != nil {
http.Error(w, "failed to read body", 400)
return
}
signature := r.Header.Get("X-Hub-Signature-256")
webhookSecret := os.Getenv("WEBHOOK_SECRET")
if !verifyWebhook(payload, webhookSecret, signature) {
http.Error(w, "invalid signature", 401)
return
}
// Process the payload...
w.WriteHeader(http.StatusOK)
}
bcrypt — Password Hashing #
Passwords must not be stored as plain SHA-256 hashes — this is vulnerable to rainbow table attacks and fast brute force. Use bcrypt, which is designed specifically for password hashing: deliberately slow and containing automatic salt.
import "golang.org/x/crypto/bcrypt"
// Hash a password at registration
func hashPassword(password string) (string, error) {
// Cost factor: 10 is the minimum recommendation (2^10 = 1024 iterations)
// Higher = more secure but slower
// Use 12-14 for production if the hardware allows it
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 12)
if err != nil {
return "", fmt.Errorf("hashPassword: %w", err)
}
return string(bytes), nil
}
// Verify a password at login
func verifyPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// Check whether a hash needs updating (cost factor lower than the current standard)
func needsRehash(hash string, minCost int) bool {
cost, err := bcrypt.Cost([]byte(hash))
if err != nil {
return true // assume a rehash is needed on error
}
return cost < minCost
}
// A full flow example
func registerUser(db *sql.DB, email, password string) error {
// Validate the password
if len(password) < 8 {
return fmt.Errorf("password must be at least 8 characters")
}
// Hash the password
hash, err := hashPassword(password)
if err != nil {
return fmt.Errorf("failed to hash password: %w", err)
}
// Store in the database — store the HASH, not the real password!
_, err = db.Exec(
"INSERT INTO users (email, password_hash) VALUES ($1, $2)",
email, hash)
return err
}
func loginUser(db *sql.DB, email, password string) (*User, error) {
var user User
var hash string
err := db.QueryRow(
"SELECT id, email, name, password_hash FROM users WHERE email = $1",
email,
).Scan(&user.ID, &user.Email, &user.Name, &hash)
if err == sql.ErrNoRows {
// Don't reveal whether the email is registered or not
// Still run the verification to prevent timing attacks
bcrypt.CompareHashAndPassword([]byte("$2a$12$dummy"), []byte(password))
return nil, fmt.Errorf("wrong email or password")
}
if err != nil {
return nil, fmt.Errorf("user query: %w", err)
}
if !verifyPassword(password, hash) {
return nil, fmt.Errorf("wrong email or password")
}
// Rehash if the cost factor is too low
if needsRehash(hash, 12) {
newHash, _ := hashPassword(password)
db.Exec("UPDATE users SET password_hash=$1 WHERE id=$2",
newHash, user.ID)
}
return &user, nil
}
crypto/subtle — Constant-Time Comparison #
Ordinary string comparison (== or bytes.Equal) is vulnerable to timing attacks — an attacker can measure the time needed to get hints about the string’s content. crypto/subtle provides constant-time operations that take the same time regardless of the input:
import "crypto/subtle"
// ANTI-PATTERN: ordinary comparison — vulnerable to timing attacks
func verifyTokenBad(receivedToken, validToken string) bool {
return receivedToken == validToken
// The time varies based on the length of the matching prefix
// An attacker can guess the token character by character!
}
// CORRECT: constant-time comparison
func verifyToken(receivedToken, validToken string) bool {
// ConstantTimeCompare returns 1 if equal, 0 if not
return subtle.ConstantTimeCompare(
[]byte(receivedToken),
[]byte(validToken),
) == 1
}
// XOR — a constant-time operation on byte slices
func xorSlices(a, b []byte) []byte {
if len(a) != len(b) {
panic("slices must have the same length")
}
result := make([]byte, len(a))
subtle.XORBytes(result, a, b) // Go 1.20+
return result
}
// ConstantTimeByteEq — compare two bytes in constant time
fmt.Println(subtle.ConstantTimeByteEq(0x41, 0x41)) // 1 (equal)
fmt.Println(subtle.ConstantTimeByteEq(0x41, 0x42)) // 0 (different)
Production Security Patterns #
Signed URLs with HMAC #
// Create a signature-protected URL for temporary downloads
type SignedURL struct {
URL string
ExpiresAt time.Time
Signature string
}
func makeSignedURL(baseURL, path, secret string, duration time.Duration) SignedURL {
expiresAt := time.Now().Add(duration)
payload := fmt.Sprintf("%s|%d", path, expiresAt.Unix())
sig := makeHMAC([]byte(secret), []byte(payload))
return SignedURL{
URL: baseURL + path,
ExpiresAt: expiresAt,
Signature: hex.EncodeToString(sig),
}
}
func verifySignedURL(path, signatureHex, secret string, expiresAt time.Time) bool {
// Check the expiry
if time.Now().After(expiresAt) {
return false
}
payload := fmt.Sprintf("%s|%d", path, expiresAt.Unix())
sigBytes, err := hex.DecodeString(signatureHex)
if err != nil {
return false
}
expected := makeHMAC([]byte(secret), []byte(payload))
return hmac.Equal(expected, sigBytes)
}
File Fingerprints for Change Detection #
// Calculate and store a file fingerprint for change detection
type Fingerprint struct {
Path string
SHA256 string
Size int64
ModTime time.Time
}
func computeFingerprint(path string) (*Fingerprint, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("open file: %w", err)
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return nil, fmt.Errorf("stat file: %w", err)
}
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return nil, fmt.Errorf("hashing: %w", err)
}
return &Fingerprint{
Path: path,
SHA256: hex.EncodeToString(h.Sum(nil)),
Size: info.Size(),
ModTime: info.ModTime(),
}, nil
}
func verifyIntegrity(path, expectedHash string) (bool, error) {
fp, err := computeFingerprint(path)
if err != nil {
return false, err
}
return subtle.ConstantTimeCompare(
[]byte(fp.SHA256),
[]byte(expectedHash),
) == 1, nil
}
Password Reset Tokens #
type PasswordResetToken struct {
Token string
UserID int
ExpiresAt time.Time
}
// Create a secure password reset token
func createResetToken(db *sql.DB, email string) (*PasswordResetToken, error) {
// Find the user
var userID int
err := db.QueryRow(
"SELECT id FROM users WHERE email = $1", email).Scan(&userID)
if err != nil {
// Don't reveal whether the email is registered
return nil, fmt.Errorf("if the email is registered, a reset link was sent")
}
// Generate a secure random token (32 bytes = 64 hex chars)
tokenBytes, err := randomBytes(32)
if err != nil {
return nil, fmt.Errorf("generate token: %w", err)
}
token := hex.EncodeToString(tokenBytes)
// Hash the token before storing it in the database
// So if the database leaks, the tokens can't be used
tokenHash := sha256.Sum256([]byte(token))
tokenHashHex := hex.EncodeToString(tokenHash[:])
expiresAt := time.Now().Add(1 * time.Hour)
// Store the token hash, not the actual token
_, err = db.Exec(
"INSERT INTO reset_tokens (user_id, token_hash, expires_at) VALUES ($1, $2, $3) "+
"ON CONFLICT (user_id) DO UPDATE SET token_hash=$2, expires_at=$3",
userID, tokenHashHex, expiresAt)
if err != nil {
return nil, fmt.Errorf("store token: %w", err)
}
return &PasswordResetToken{
Token: token, // send the actual token via email
UserID: userID,
ExpiresAt: expiresAt,
}, nil
}
// Verify a reset token
func verifyResetToken(db *sql.DB, token string) (int, error) {
// Hash the received token
tokenHash := sha256.Sum256([]byte(token))
tokenHashHex := hex.EncodeToString(tokenHash[:])
var userID int
var expiresAt time.Time
err := db.QueryRow(
"SELECT user_id, expires_at FROM reset_tokens WHERE token_hash = $1",
tokenHashHex,
).Scan(&userID, &expiresAt)
if err == sql.ErrNoRows {
return 0, fmt.Errorf("invalid or already used token")
}
if err != nil {
return 0, fmt.Errorf("token verification: %w", err)
}
if time.Now().After(expiresAt) {
// Delete the expired token
db.Exec("DELETE FROM reset_tokens WHERE token_hash = $1", tokenHashHex)
return 0, fmt.Errorf("token expired")
}
// Delete the token after use (one-time use)
db.Exec("DELETE FROM reset_tokens WHERE token_hash = $1", tokenHashHex)
return userID, nil
}
Cryptographic Mistakes to Avoid #
flowchart TD
subgraph Wrong["✗ Common Mistakes"]
E1["Use math/rand\nfor tokens/keys"]
E2["Store passwords\nas plain SHA-256"]
E3["Compare tokens\nwith == or bytes.Equal"]
E4["Use MD5\nfor security"]
E5["Store the actual token\nin the database"]
E6["Reveal whether the\nemail is registered\nat login/reset"]
end
subgraph Right["✓ What You Should Do"]
C1["Use crypto/rand\nfor all cryptographic randomness"]
C2["Use bcrypt\nfor password hashing"]
C3["Use subtle.ConstantTimeCompare\nor hmac.Equal"]
C4["Use SHA-256 or SHA-512\nfor security"]
C5["Store the token hash\nin the database"]
C6["Return the same response\nfor valid/invalid emails"]
end
E1 -.->|"fix"| C1
E2 -.->|"fix"| C2
E3 -.->|"fix"| C3
E4 -.->|"fix"| C4
E5 -.->|"fix"| C5
E6 -.->|"fix"| C6
style Wrong fill:#fce4ec
style Right fill:#e8f5e9// ✗ ANTI-PATTERN: math/rand for tokens
import "math/rand"
token := fmt.Sprintf("%d", rand.Int63()) // NOT SECURE!
// ✓ CORRECT: crypto/rand
tokenBytes, _ := randomBytes(32)
token := hex.EncodeToString(tokenBytes)
// ✗ ANTI-PATTERN: SHA-256 for passwords
hash := sha256.Sum256([]byte(password)) // easy to brute force!
// ✓ CORRECT: bcrypt
hash, _ := bcrypt.GenerateFromPassword([]byte(password), 12)
// ✗ ANTI-PATTERN: ordinary comparison for tokens
if receivedToken == storedToken { // timing attack!
// ✓ CORRECT: constant-time
if subtle.ConstantTimeCompare([]byte(receivedToken), []byte(storedToken)) == 1 {
// ✗ ANTI-PATTERN: store the actual token in the DB
db.Exec("INSERT INTO tokens (token) VALUES ($1)", token)
// If the DB leaks, all tokens can be used!
// ✓ CORRECT: store the token hash
h := sha256.Sum256([]byte(token))
db.Exec("INSERT INTO tokens (token_hash) VALUES ($1)", hex.EncodeToString(h[:]))
When to Switch to Alternatives #
Keep using the crypto standard library if:
✓ SHA-256 / SHA-512 for data hashing and integrity
✓ crypto/rand for all cryptographic randomness needs
✓ crypto/hmac for keyed integrity verification
✓ crypto/subtle for constant-time comparison
Use golang.org/x/crypto (the extended library) for:
✓ bcrypt — password hashing
✓ argon2 — modern password hashing (stronger than bcrypt)
✓ scrypt — password hashing with a memory-hard function
✓ chacha20poly1305 — modern symmetric encryption
✓ ssh — SSH clients and servers
✓ tls — more advanced TLS configuration
Consider external libraries if:
✗ JWT (JSON Web Token) → golang-jwt/jwt
✗ OAuth 2.0 → golang.org/x/oauth2
✗ OpenPGP → golang.org/x/crypto/openpgp
✗ Hardware Security Modules (HSM) → vendor-specific libraries
NEVER implement your own cryptographic algorithms —
always use battle-tested implementations from the standard library
or trusted libraries.
Summary #
crypto/rand, notmath/randfor all cryptographic needs —math/randis a PRNG whose output can be predicted;crypto/randuses operating system entropy that can’t be predicted.- SHA-256 for data hashing, not SHA-1 or MD5 — SHA-1 and MD5 have proven cryptographic weaknesses and must not be used for security.
- bcrypt (or argon2) for password hashing — SHA-256 isn’t suitable for passwords because it’s too fast, making brute force easy; bcrypt is deliberately slow and contains automatic salt.
hmac.Equalorsubtle.ConstantTimeComparefor comparing cryptographic values — ordinary comparison (==) is vulnerable to timing attacks that allow an attacker to guess values character by character.- Store token hashes, not the actual tokens in the database — if the database leaks, an attacker can’t use the tokens directly because they’d need to know the original values.
io.Copyinto a hasher for hashing large files — avoid reading the entire file into memory; use streaming for efficiency.- HMAC for integrity verification with a secret key — proving that the message wasn’t modified AND comes from the party holding the key.
- Don’t reveal excessive information on login/reset endpoints — return the same response for valid and invalid emails to prevent user enumeration attacks.
- Never implement your own cryptography — always use battle-tested implementations from the standard library or golang.org/x/crypto.