對稱加密在真實系統中的用法
Published: Sat Feb 01 2025 | Modified: Sat Feb 07 2026 , 9 minutes reading.
1. 為什麼要關心這個問題?
你已經知道 AES-GCM 是好的選擇,ECB 是災難。但當你面對真實的工程問題時:
- 「我應該把 IV 存在哪裡?」
- 「金鑰怎麼產生和儲存?」
- 「我需要加密多少次?」
- 「效能會是問題嗎?」
這些問題在教科書裡很少提到,但在生產環境中會決定你的系統是安全的還是脆弱的。
2. HTTPS/TLS 中的對稱加密
為什麼 HTTPS 使用對稱加密
TLS 握手使用非對稱加密來交換金鑰。但一旦握手完成,所有資料傳輸都用對稱加密。為什麼?
非對稱加密(RSA-2048):~1 MB/s
對稱加密(AES-256-GCM):~1 GB/s
相差 1000 倍!TLS 1.3 的對稱加密階段
┌─────────────────────────────────────────────────────────────┐
│ TLS 1.3 握手後 │
├─────────────────────────────────────────────────────────────┤
│ 用戶端 → 伺服器: │
│ Application Data │
│ encrypted with client_application_traffic_secret │
│ using AES-256-GCM or ChaCha20-Poly1305 │
├─────────────────────────────────────────────────────────────┤
│ 伺服器 → 用戶端: │
│ Application Data │
│ encrypted with server_application_traffic_secret │
│ using AES-256-GCM or ChaCha20-Poly1305 │
└─────────────────────────────────────────────────────────────┘TLS 的金鑰派生
TLS 不直接使用交換的金鑰。它使用 HKDF(HMAC-based Key Derivation Function)派生多個金鑰:
Master Secret
│
├──► client_handshake_traffic_secret
├──► server_handshake_traffic_secret
├──► client_application_traffic_secret
└──► server_application_traffic_secret
每個方向有獨立的金鑰
防止反射攻擊TLS 的 Nonce 管理
TLS 1.3 使用隱式 nonce:
nonce = static_IV XOR record_sequence_number
record_sequence_number 從 0 開始遞增
每個連線的 static_IV 不同
保證 nonce 不重複3. 檔案加密的最佳實踐
基本架構
原始檔案
│
▼
┌─────────────────────────────────────────────┐
│ 1. 產生隨機 DEK(Data Encryption Key) │
├─────────────────────────────────────────────┤
│ 2. 用 DEK 加密檔案(AES-GCM) │
├─────────────────────────────────────────────┤
│ 3. 用 KEK(Key Encryption Key)加密 DEK │
├─────────────────────────────────────────────┤
│ 4. 儲存:加密的 DEK + IV + 加密的檔案 │
└─────────────────────────────────────────────┘為什麼需要兩層金鑰
只用一個金鑰:
- 換金鑰需要重新加密所有檔案
- 金鑰洩漏 = 所有資料洩漏
兩層金鑰(DEK + KEK):
- 每個檔案有自己的 DEK
- 只需要重新加密 DEK(很小)
- 可以實現金鑰輪替而不動資料實作範例
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
import os
import json
import base64
class FileEncryptor:
def __init__(self, password: str):
"""從密碼派生 KEK"""
self.salt = os.urandom(16)
self.kek = self._derive_kek(password, self.salt)
def _derive_kek(self, password: str, salt: bytes) -> bytes:
"""使用 scrypt 從密碼派生金鑰"""
kdf = Scrypt(
salt=salt,
length=32,
n=2**20, # CPU/記憶體成本
r=8,
p=1,
)
return kdf.derive(password.encode())
def encrypt_file(self, plaintext: bytes) -> dict:
"""加密檔案"""
# 1. 產生隨機 DEK
dek = AESGCM.generate_key(bit_length=256)
# 2. 用 DEK 加密資料
data_nonce = os.urandom(12)
data_cipher = AESGCM(dek)
encrypted_data = data_cipher.encrypt(data_nonce, plaintext, None)
# 3. 用 KEK 加密 DEK
key_nonce = os.urandom(12)
key_cipher = AESGCM(self.kek)
encrypted_dek = key_cipher.encrypt(key_nonce, dek, None)
# 4. 打包結果
return {
'version': 1,
'salt': base64.b64encode(self.salt).decode(),
'key_nonce': base64.b64encode(key_nonce).decode(),
'encrypted_dek': base64.b64encode(encrypted_dek).decode(),
'data_nonce': base64.b64encode(data_nonce).decode(),
'encrypted_data': base64.b64encode(encrypted_data).decode(),
}
def decrypt_file(self, encrypted: dict) -> bytes:
"""解密檔案"""
# 解碼
key_nonce = base64.b64decode(encrypted['key_nonce'])
encrypted_dek = base64.b64decode(encrypted['encrypted_dek'])
data_nonce = base64.b64decode(encrypted['data_nonce'])
encrypted_data = base64.b64decode(encrypted['encrypted_data'])
# 1. 解密 DEK
key_cipher = AESGCM(self.kek)
dek = key_cipher.decrypt(key_nonce, encrypted_dek, None)
# 2. 解密資料
data_cipher = AESGCM(dek)
plaintext = data_cipher.decrypt(data_nonce, encrypted_data, None)
return plaintext大檔案處理
對於 GB 級的大檔案,你不能把整個檔案讀入記憶體:
def encrypt_large_file(input_path: str, output_path: str, key: bytes):
"""串流加密大檔案"""
CHUNK_SIZE = 64 * 1024 # 64KB chunks
# 使用 AES-GCM-SIV 或 AES-CTR + HMAC
# 注意:標準 AES-GCM 不適合串流加密,因為需要完整資料計算 tag
# 更好的選擇:使用專門設計的檔案加密格式
# 如 age (https://age-encryption.org/)
pass建議:對於大檔案,使用成熟的工具如 age 或 gpg,而不是自己實作。
4. 資料庫加密
加密層級
┌─────────────────────────────────────────────────────────────┐
│ 1. 傳輸加密(TLS) │
│ - 加密用戶端和資料庫之間的通訊 │
│ - 防止網路竊聽 │
├─────────────────────────────────────────────────────────────┤
│ 2. 透明資料加密(TDE) │
│ - 資料庫檔案層級加密 │
│ - 防止磁碟被盜 │
│ - 對應用程式透明 │
├─────────────────────────────────────────────────────────────┤
│ 3. 欄位層級加密 │
│ - 應用程式層級加密 │
│ - 只加密敏感欄位 │
│ - 資料庫管理員也看不到明文 │
└─────────────────────────────────────────────────────────────┘欄位層級加密範例
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
import base64
class EncryptedField:
def __init__(self, key: bytes):
self.cipher = AESGCM(key)
def encrypt(self, value: str) -> str:
"""加密欄位值"""
nonce = os.urandom(12)
ciphertext = self.cipher.encrypt(nonce, value.encode(), None)
# 格式:nonce + ciphertext,base64 編碼
return base64.b64encode(nonce + ciphertext).decode()
def decrypt(self, encrypted_value: str) -> str:
"""解密欄位值"""
data = base64.b64decode(encrypted_value)
nonce = data[:12]
ciphertext = data[12:]
plaintext = self.cipher.decrypt(nonce, ciphertext, None)
return plaintext.decode()
# 使用
field_key = os.urandom(32)
encrypted_field = EncryptedField(field_key)
# 儲存到資料庫
ssn = "123-45-6789"
encrypted_ssn = encrypted_field.encrypt(ssn)
# INSERT INTO users (encrypted_ssn) VALUES ('...')
# 從資料庫讀取
decrypted_ssn = encrypted_field.decrypt(encrypted_ssn)加密欄位的查詢問題
-- 這不能工作!
SELECT * FROM users WHERE encrypted_ssn = ?
-- 因為相同的明文會產生不同的密文(不同的 nonce)解決方案:
1. 盲索引(Blind Index)
- 對明文計算 HMAC
- 儲存 HMAC 作為可搜尋的索引
- 查詢時計算 HMAC 進行比對
2. 確定性加密(Deterministic Encryption)
- 固定 nonce 或使用 SIV 模式
- 相同明文產生相同密文
- 可以精確匹配
- 會洩漏相等性資訊
3. 同態加密(Homomorphic Encryption)
- 可以在密文上進行運算
- 效能開銷很大
- 仍在研究階段盲索引實作
import hmac
import hashlib
def create_blind_index(value: str, key: bytes) -> str:
"""建立可搜尋的盲索引"""
h = hmac.new(key, value.encode(), hashlib.sha256)
# 只取前 16 位元組,減少儲存空間,增加一點模糊性
return base64.b64encode(h.digest()[:16]).decode()
# 使用
index_key = os.urandom(32) # 與加密金鑰不同!
ssn = "123-45-6789"
ssn_index = create_blind_index(ssn, index_key)
# 儲存
# INSERT INTO users (encrypted_ssn, ssn_index) VALUES (?, ?)
# 查詢
search_index = create_blind_index("123-45-6789", index_key)
# SELECT * FROM users WHERE ssn_index = ?5. 金鑰管理
金鑰的生命週期
生成 → 分發 → 使用 → 輪替 → 撤銷 → 銷毀
每個階段都有安全考量:
- 生成:必須使用 CSPRNG
- 分發:必須安全傳輸
- 使用:必須限制存取
- 輪替:必須支援多版本
- 撤銷:必須快速生效
- 銷毀:必須不可恢復金鑰儲存選項
┌─────────────────────────────────────────────────────────────┐
│ 開發環境 │
│ - 環境變數 │
│ - 設定檔(不要提交到 Git!) │
├─────────────────────────────────────────────────────────────┤
│ 生產環境 │
│ - 雲端金鑰管理服務(AWS KMS、GCP KMS、Azure Key Vault) │
│ - HashiCorp Vault │
│ - 硬體安全模組(HSM) │
└─────────────────────────────────────────────────────────────┘使用雲端 KMS 的範例
import boto3
class KMSKeyManager:
def __init__(self, key_id: str):
self.kms = boto3.client('kms')
self.key_id = key_id
def generate_data_key(self) -> tuple:
"""產生資料金鑰"""
response = self.kms.generate_data_key(
KeyId=self.key_id,
KeySpec='AES_256'
)
return (
response['Plaintext'], # 用於加密
response['CiphertextBlob'] # 儲存這個
)
def decrypt_data_key(self, encrypted_key: bytes) -> bytes:
"""解密資料金鑰"""
response = self.kms.decrypt(
KeyId=self.key_id,
CiphertextBlob=encrypted_key
)
return response['Plaintext']
# 使用
km = KMSKeyManager('alias/my-key')
# 加密時
plaintext_key, encrypted_key = km.generate_data_key()
# 用 plaintext_key 加密資料
# 儲存 encrypted_key 和加密的資料
# 解密時
plaintext_key = km.decrypt_data_key(encrypted_key)
# 用 plaintext_key 解密資料6. 效能考量
硬體加速
# 檢查 CPU 是否支援 AES-NI
import subprocess
result = subprocess.run(['grep', 'aes', '/proc/cpuinfo'], capture_output=True)
has_aesni = b'aes' in result.stdout
# 現代 CPU 幾乎都支援
# AES-NI 可以讓 AES 加密快 10 倍以上加密對效能的影響
操作 | 沒有加密 | AES-256-GCM
-------------------------------------------
檔案讀寫 | 1.0x | ~1.1x
網路傳輸 | 1.0x | ~1.05x
資料庫查詢 | 1.0x | 1.0x(TDE)
欄位加密/解密 | 1.0x | ~1.5-2x
結論:對於大多數應用,加密的效能開銷可以忽略何時效能會是問題
1. 大量小檔案
- 每次加密都需要初始化
- 考慮批次處理
2. 即時資料串流
- 延遲敏感
- 考慮 ChaCha20-Poly1305(沒有 AES-NI 時更快)
3. 資料庫欄位加密 + 大量查詢
- 每次存取都需要加解密
- 考慮快取解密後的值7. 常見錯誤總結
| 錯誤 | 後果 | 正確做法 |
|---|---|---|
| 把金鑰硬編碼在程式碼中 | 金鑰隨程式碼外洩 | 使用環境變數或金鑰管理服務 |
| 使用密碼直接當金鑰 | 金鑰空間太小 | 使用 KDF(PBKDF2、scrypt、Argon2) |
| 不儲存 IV/nonce | 無法解密 | IV 可以和密文一起儲存 |
| 把 IV 存成祕密 | 沒必要,反而增加複雜性 | IV 不需要保密,只需要唯一 |
| 用同一個金鑰加密太多資料 | GCM 有資料量限制 | 定期輪替金鑰 |
| 自己實作加密邏輯 | 幾乎肯定有漏洞 | 使用成熟的函式庫 |
8. 本章小結
三點要記住:
HTTPS 展示了對稱加密的最佳實踐。 金鑰派生、nonce 管理、認證加密——TLS 的設計值得學習。
檔案加密用雙層金鑰(DEK + KEK)。 這讓金鑰輪替變得簡單,也提供了更好的安全隔離。
資料庫加密有多個層級。 傳輸加密、透明資料加密、欄位加密各有用途。欄位加密要考慮查詢的問題。
9. 下一步
我們已經完成了對稱加密的深度探索。但對稱加密有一個根本問題:雙方需要預先共享金鑰。
在下一部分,我們將進入非對稱加密的世界:RSA 的核心思想——為什麼「分解大數」這麼難,以及公鑰和私鑰是怎麼來的。
