🔐 Security✍️ Khoa📅 20/04/2026☕ 11 phút đọc

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

  1. Authentication: Message came from specific sender
  2. Integrity: Message not modified
  3. 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:

  1. ✅ Don't roll your own crypto
  2. ✅ Use standard libraries (Go crypto, OpenSSL, libsodium)
  3. ✅ Use authenticated encryption (AES-GCM, not AES-CBC alone)
  4. ✅ Never reuse nonces
  5. ✅ Use crypto/rand, not math/rand
  6. ✅ Rotate keys regularly
  7. ✅ Use KMS in production

Bước tiếp theo

  • auth/oauth2-and-oidc.md — How JWT signatures work
  • distributed-systems-security.md — mTLS certificates & PKI
  • api-and-web-security.md — HMAC for API authentication