Cryptography Basics cho Developers
Cryptography khΓ΄ng phαΊ£i lΓ magic β ΔΓ³ lΓ toΓ‘n hα»c. NhΖ°ng bαΊ‘n khΓ΄ng cαΊ§n lΓ mathematician Δα» dΓΉng crypto ΔΓΊng cΓ‘ch. Section nΓ y giαΊ£i thΓch cΓ‘c concepts cΖ‘ bαΊ£n vΓ cΓ‘ch apply an toΓ n trong code.
π Golden rule: "Don't roll your own crypto." DΓΉng battle-tested libraries, khΓ΄ng tα»± implement algorithms.
Encoding vs Hashing vs Encryption
Ba khΓ‘i niα»m nΓ y thΖ°α»ng bα» nhαΊ§m lαΊ«n. HΓ£y clear ngay tα»« ΔαΊ§u:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ENCODING: Reversible, NO security β
β Base64, URL encoding, hex β
β Purpose: Data representation, not security β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β HASHING: One-way, NO key needed β
β SHA-256, bcrypt, argon2 β
β Purpose: Integrity check, password storage β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ENCRYPTION: Reversible, KEY required β
β AES, RSA β
β Purpose: Confidentiality (hide data) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Encoding Example
import "encoding/base64"
// Encode (not secure!)
plaintext := "password123"
encoded := base64.StdEncoding.EncodeToString([]byte(plaintext))
// β "cGFzc3dvcmQxMjM="
// Decode (anyone can do this)
decoded, _ := base64.StdEncoding.DecodeString(encoded)
// β "password123"
Use case: Truyα»n binary data qua text protocols (email attachments, JSON).
NOT for: Hiding sensitive data!
Hashing
Hash function: Input β Fixed-size output (digest/hash)
Properties:
- Deterministic: Same input β same output
- One-way: Cannot reverse (hash β original)
- Collision-resistant: Hard to find two inputs with same hash
- Avalanche effect: Small change in input β completely different hash
Common Hash Functions
import "crypto/sha256"
text := "Hello, World!"
hash := sha256.Sum256([]byte(text))
fmt.Printf("%x\n", hash)
// β dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f
// Change one character
text2 := "Hello, world!" // lowercase 'w'
hash2 := sha256.Sum256([]byte(text2))
fmt.Printf("%x\n", hash2)
// β 315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3
// Completely different!
| Algorithm | Output Size | Status | Use Case |
|---|---|---|---|
| MD5 | 128 bits | β Broken | NEVER use |
| SHA-1 | 160 bits | β Deprecated | Legacy systems only |
| SHA-256 | 256 bits | β Good | General-purpose hashing |
| SHA-512 | 512 bits | β Good | When need larger hash |
| SHA-3 | Variable | β Good | Latest standard |
Password Hashing (Special Case)
Problem: SHA-256 is too FAST β attackers can brute-force billions of hashes/sec.
Solution: Use password hashing functions designed to be slow:
- bcrypt
- scrypt
- argon2 (winner of password hashing competition)
import "golang.org/x/crypto/bcrypt"
// Hash password (with salt automatically included)
func hashPassword(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
// Cost = 10 means 2^10 iterations (adjustable)
return string(hash), err
}
// Verify password
func checkPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// Example
hashedPassword, _ := hashPassword("mypassword")
// β $2a$10$N9qo8uLOickgx2ZMRZoMye.IjkkXyZvF0b0HW.5gYsVgU6TQ5VlGm
// Verify
isValid := checkPassword("mypassword", hashedPassword) // true
isValid = checkPassword("wrongpassword", hashedPassword) // false
Key features:
- β Salt automatically included in output
- β Work factor (cost) tunable β increase as hardware improves
- β Slow by design β resistant to brute-force
argon2 (recommended for new systems):
import "golang.org/x/crypto/argon2"
func hashPasswordArgon2(password string) string {
salt := make([]byte, 16)
rand.Read(salt)
hash := argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, 32)
// time=1, memory=64MB, threads=4, keyLen=32
// Encode salt + hash for storage
return fmt.Sprintf("%x:%x", salt, hash)
}
Symmetric Encryption
Symmetric: Same key for encrypt & decrypt.
βββββββββββ Key βββββββββββ Key βββββββββββ
β Plain ββββββββββββΊβ Cipher ββββββββββββΊβ Plain β
β text β Encrypt β text β Decrypt β text β
βββββββββββ βββββββββββ βββββββββββ
AES-GCM (Recommended)
AES-GCM = AES encryption + authentication (ensures integrity).
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
)
// Encrypt
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
}
// Generate random nonce (number used once)
nonce := make([]byte, gcm.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return nil, err
}
// Encrypt and authenticate
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
// Prepend nonce to ciphertext (nonce is public, not secret)
return ciphertext, nil
}
// Decrypt
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, errors.New("ciphertext too short")
}
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, err // Authentication failed or tampered
}
return plaintext, nil
}
// Usage
key := []byte("32-byte-long-key-for-aes-256!!!")
plaintext := []byte("Secret message")
encrypted, _ := encrypt(plaintext, key)
decrypted, _ := decrypt(encrypted, key)
fmt.Println(string(decrypted)) // "Secret message"
Key points:
- β Use GCM mode (not ECB or CBC alone)
- β Nonce must be unique for each encryption (never reuse!)
- β Key size: 16 bytes (AES-128), 24 (AES-192), or 32 (AES-256)
- β GCM provides authentication β detects tampering
Common Mistakes
β Reusing nonces:
// π₯ NEVER do this
nonce := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
β Using ECB mode:
// π₯ ECB leaks patterns (never use for real data)
β No integrity check:
// π₯ Using AES-CBC without HMAC β vulnerable to padding oracle
Asymmetric Encryption (Public Key Cryptography)
Asymmetric: Different keys for encrypt & decrypt.
ββββββββββββββ Public Key ββββββββββββ Private Key ββββββββββββββ
β Plaintext βββββββββββββββββΊβ Cipher ββββββββββββββββββΊβ Plaintext β
ββββββββββββββ (Encrypt) β text β (Decrypt) ββββββββββββββ
ββββββββββββ
Use cases:
- Encrypt data for someone (use their public key)
- Establish shared secret (Diffie-Hellman)
- Digital signatures
RSA Example
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
)
// Generate key pair
func generateKeyPair() (*rsa.PrivateKey, *rsa.PublicKey, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, err
}
return privateKey, &privateKey.PublicKey, nil
}
// Encrypt with public key
func encryptRSA(plaintext []byte, publicKey *rsa.PublicKey) ([]byte, error) {
return rsa.EncryptOAEP(sha256.New(), rand.Reader, publicKey, plaintext, nil)
}
// Decrypt with private key
func decryptRSA(ciphertext []byte, privateKey *rsa.PrivateKey) ([]byte, error) {
return privateKey.Decrypt(rand.Reader, ciphertext, &rsa.OAEPOptions{Hash: crypto.SHA256})
}
// Usage
privateKey, publicKey, _ := generateKeyPair()
plaintext := []byte("Secret message")
ciphertext, _ := encryptRSA(plaintext, publicKey)
decrypted, _ := decryptRSA(ciphertext, privateKey)
fmt.Println(string(decrypted)) // "Secret message"
Limitations:
- β Slow compared to symmetric encryption
- β Can only encrypt small data (max = key size - padding)
Solution: Hybrid encryption (RSA + AES):
// 1. Generate random AES key
aesKey := make([]byte, 32)
rand.Read(aesKey)
// 2. Encrypt data with AES
ciphertext, _ := encrypt(largeData, aesKey)
// 3. Encrypt AES key with RSA
encryptedKey, _ := encryptRSA(aesKey, recipientPublicKey)
// Send both encryptedKey + ciphertext
This is how TLS works!
Digital Signatures
Signatures prove:
- Authentication: Message came from specific sender
- Integrity: Message not modified
- Non-repudiation: Sender cannot deny sending
ββββββββββββ Private Key βββββββββββββ
β Message ββββββββββββββββββΊβ Signature β
ββββββββββββ (Sign) βββββββββββββ
ββββββββββββ Public Key ββββββββββββββ
β Message ββββββββββββββββββΊβ Valid? β
β + Sig β (Verify) β True/False β
ββββββββββββ ββββββββββββββ
RSA Signature
import "crypto"
// Sign message
func signMessage(message []byte, privateKey *rsa.PrivateKey) ([]byte, error) {
hashed := sha256.Sum256(message)
return rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed[:])
}
// Verify signature
func verifySignature(message, signature []byte, publicKey *rsa.PublicKey) error {
hashed := sha256.Sum256(message)
return rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, hashed[:], signature)
}
// Usage
privateKey, publicKey, _ := generateKeyPair()
message := []byte("Important contract")
signature, _ := signMessage(message, privateKey)
// Verify
err := verifySignature(message, signature, publicKey)
if err == nil {
fmt.Println("Signature valid!")
} else {
fmt.Println("Signature INVALID!")
}
// Tamper with message
tamperedMessage := []byte("Tampered contract")
err = verifySignature(tamperedMessage, signature, publicKey)
// β Error: verification failed
HMAC (Symmetric Signature)
When both parties share a secret key:
import "crypto/hmac"
// Generate HMAC
func generateHMAC(message, key []byte) []byte {
mac := hmac.New(sha256.New, key)
mac.Write(message)
return mac.Sum(nil)
}
// Verify HMAC
func verifyHMAC(message, receivedMAC, key []byte) bool {
expectedMAC := generateHMAC(message, key)
return hmac.Equal(receivedMAC, expectedMAC)
}
// Usage
key := []byte("shared-secret-key")
message := []byte("Important data")
mac := generateHMAC(message, key)
// Verify
isValid := verifyHMAC(message, mac, key) // true
// Tampered
tamperedMessage := []byte("Tampered data")
isValid = verifyHMAC(tamperedMessage, mac, key) // false
HMAC vs Digital Signature:
| HMAC | Digital Signature | |
|---|---|---|
| Keys | Symmetric (same key) | Asymmetric (public/private) |
| Speed | β Fast | β Slower |
| Non-repudiation | β No (both have key) | β Yes |
| Use case | API authentication | Documents, software signing |
Key Management
The hardest part of crypto is managing keys.
Don't Do This
// π₯ NEVER hardcode keys
var encryptionKey = []byte("my-secret-key-123")
// π₯ NEVER commit keys to git
// .env
ENCRYPTION_KEY=abc123xyz
Key Derivation (from password)
import "golang.org/x/crypto/scrypt"
// Derive encryption key from password
func deriveKey(password, salt []byte) ([]byte, error) {
return scrypt.Key(password, salt, 32768, 8, 1, 32)
// N=32768, r=8, p=1, keyLen=32
}
// Usage
password := []byte("user-password")
salt := make([]byte, 16)
rand.Read(salt)
key, _ := deriveKey(password, salt)
// Now use 'key' for AES encryption
// Store salt alongside encrypted data (salt is not secret)
Key Rotation
type EncryptedData struct {
KeyID int // Which key was used
Ciphertext []byte
CreatedAt time.Time
}
var keys = map[int][]byte{
1: []byte("old-key-aaaaaaaaaaaaaaaaaaaaaaa!"),
2: []byte("new-key-bbbbbbbbbbbbbbbbbbbbbbb!"),
}
var currentKeyID = 2
func encryptWithRotation(plaintext []byte) (*EncryptedData, error) {
key := keys[currentKeyID]
ciphertext, err := encrypt(plaintext, key)
if err != nil {
return nil, err
}
return &EncryptedData{
KeyID: currentKeyID,
Ciphertext: ciphertext,
CreatedAt: time.Now(),
}, nil
}
func decryptWithRotation(data *EncryptedData) ([]byte, error) {
key, exists := keys[data.KeyID]
if !exists {
return nil, errors.New("key not found")
}
return decrypt(data.Ciphertext, key)
}
// Background job: Re-encrypt old data with new key
func rotateKeys() {
oldDataList := getDataEncryptedWithKey(1)
for _, oldData := range oldDataList {
// Decrypt with old key
plaintext, _ := decrypt(oldData.Ciphertext, keys[1])
// Encrypt with new key
newData, _ := encryptWithRotation(plaintext)
// Update in DB
db.Update(oldData.ID, newData)
}
}
Use Key Management Services (Production)
AWS KMS:
import "github.com/aws/aws-sdk-go/service/kms"
func encryptWithKMS(plaintext []byte, keyID string) ([]byte, error) {
svc := kms.New(session.Must(session.NewSession()))
result, err := svc.Encrypt(&kms.EncryptInput{
KeyId: aws.String(keyID),
Plaintext: plaintext,
})
return result.CiphertextBlob, err
}
func decryptWithKMS(ciphertext []byte) ([]byte, error) {
svc := kms.New(session.Must(session.NewSession()))
result, err := svc.Decrypt(&kms.DecryptInput{
CiphertextBlob: ciphertext,
})
return result.Plaintext, err
}
TLS/SSL (Transport Layer Security)
TLS secures communication between client & server.
TLS Handshake (Simplified)
Client Server
β β
β β ClientHello β
β (supported ciphers, TLS version) β
ββββββββββββββββββββββββββββββββββββββΊβ
β β
β β‘ ServerHello β
β (chosen cipher, certificate) β
βββββββββββββββββββββββββββββββββββββββ€
β β
β β’ Verify certificate β
β (signed by trusted CA?) β
β β
β β£ Generate pre-master secret β
β Encrypt with server's public key β
ββββββββββββββββββββββββββββββββββββββΊβ
β β
β β€ Both derive β
β session keys β
β (symmetric) β
β β
β β₯ Encrypted communication β
ββββββββββββββββββββββββββββββββββββββΊβ
β (using AES with session key) β
Key points:
- Uses asymmetric crypto to exchange symmetric key
- Then uses symmetric crypto (AES) for actual data (faster)
- Certificate proves server identity
Implementing HTTPS in Go
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, HTTPS!")
})
// Generate self-signed cert for dev:
// openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
log.Fatal(http.ListenAndServeTLS(":443", "cert.pem", "key.pem", nil))
}
Production: Use Let's Encrypt (free, auto-renewing certificates)
Random Number Generation
Cryptographically secure random is critical for:
- Generating keys
- Nonces
- Tokens
- Salts
β Wrong
import "math/rand"
// π₯ NEVER use math/rand for security
token := rand.Int() // Predictable!
β Correct
import "crypto/rand"
// Generate random bytes
func generateRandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
return b, err
}
// Generate random token
func generateToken() (string, error) {
b, err := generateRandomBytes(32)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}
// Usage
token, _ := generateToken()
// β "a8f3jkd9f0aksdjf0a9sdjf0a9sdjf0as9df"
Common Pitfalls
1. Using ECB Mode
// π₯ NEVER use ECB (Electronic Codebook) mode
// Identical plaintext blocks β identical ciphertext blocks
// Leaks patterns (e.g., penguin image meme)
2. Not Using Authenticated Encryption
// π₯ AES-CBC without HMAC
// β Vulnerable to padding oracle attacks
// β
Use AES-GCM (authenticated encryption)
3. Reusing Nonces/IVs
// π₯ Same nonce for multiple encryptions
// β Breaks security guarantees
// β
Generate new random nonce for EACH encryption
4. Weak Random Number Generator
// π₯ math/rand for crypto
// β Predictable
// β
crypto/rand
5. Short Keys
// π₯ Short keys are brute-forceable
key := []byte("abc") // 3 bytes = 24 bits
// β
Use at least 128 bits (16 bytes) for symmetric
key := make([]byte, 32) // 256 bits for AES-256
rand.Read(key)
TΓ³m tαΊ―t
| Operation | Algorithm | Use Case |
|---|---|---|
| Password hashing | bcrypt, argon2 | Storing passwords |
| General hashing | SHA-256, SHA-3 | Checksums, signatures |
| Symmetric encryption | AES-GCM | Encrypting data at rest |
| Asymmetric encryption | RSA, ECC | Key exchange, signatures |
| Message authentication | HMAC | API authentication |
| Digital signatures | RSA, ECDSA | Document signing, JWT |
| Key derivation | PBKDF2, scrypt | Password β encryption key |
| Random generation | crypto/rand | Keys, nonces, tokens |
Golden rules:
- β Don't roll your own crypto
- β
Use standard libraries (Go
crypto, OpenSSL, libsodium) - β Use authenticated encryption (AES-GCM, not AES-CBC alone)
- β Never reuse nonces
- β Use crypto/rand, not math/rand
- β Rotate keys regularly
- β Use KMS in production
BΖ°α»c tiαΊΏp theo
auth/oauth2-and-oidc.mdβ How JWT signatures workdistributed-systems-security.mdβ mTLS certificates & PKIapi-and-web-security.mdβ HMAC for API authentication