前言
今天安全的演算法,不代表十年後仍然安全。
計算能力會增加、密碼分析會進步、實作漏洞會被發現,合規要求也會改變;有些變化甚至不是演算法本身失效,而是金鑰外洩、函式庫停止維護,或協定版本被淘汰。
資料庫裡的密文、簽發給客戶的 Token、裝置韌體中的公開金鑰、服務之間的協定,都可能存活多年,還要考慮到:
- 如何應對逐漸不安全的演算法?
- 如何平順替換上更安全的演算法?
- 如何兼容新舊演算法?
Crypto Agility(密碼學敏捷性) 就是預先回答這些問題的能力:系統能在可控、可觀測、可回復的情況下,替換演算法、參數、金鑰或協定,而不必大改整個產品。
為什麼需要密碼學敏捷性
安全演算法的安全有前提
密碼學安全不是永久成立的性質,它是一個在特定攻擊模型、可用計算資源與已知研究下的判斷。當其中因素變了,原本合理的選擇都可能需要重新評估:
- 新的攻擊方法:研究者找到碰撞、降級或更有效的破解方法
- 硬體進步:GPU、ASIC、雲端叢集讓暴力破解的成本快速下降
- 實作與供應鏈風險:函式庫有漏洞、供應商停止支援、硬體模組退役
- 政策與合規:瀏覽器、作業系統或政府標準不再接受舊的 TLS、雜湊或金鑰長度
- 威脅模型改變:例如攻擊者今天先收集加密流量,等未來取得量子電腦後再解密
加密的遷移成本會隨時間上升
寫死使用一個演算法或一把金鑰看似很快很方便,但隨著應用開發可能變成一場難以收拾的爛攤子⋯⋯寫死的演算法出現在不同快取、備份、服務、合作夥伴情境當中。
加密的遷移成本會隨時間上升,所以提倡密碼學敏捷的核心價值不是讓切換變得零成本,而是讓成本可預估:知道哪些資料受影響、哪些版本仍在使用、先部署什麼、何時能移除舊金鑰。
回顧過去案例:DES
DES 是很具體的例子。它在 1977 年成為美國聯邦標準;1998 年,EFF 以名為 Deep Crack 的專用硬體在約 56 小時內找出一把 DES 金鑰,證明 56-bit 金鑰已經不足以抵抗有資源的攻擊者。直到 2005 年,NIST 才正式撤回 DES 標準。
這條時間線有兩個值得注意的間隔:DES 在「被採用」約 21 年後,硬體成本終於讓其 56-bit 金鑰變得不夠安全;而從 1998 年的公開示警到 2005 年正式退役,又有約 7 年。這不是可以原地不動的緩衝期,而是系統必須完成升級的窗口。
因此,問題不只是「目前的演算法夠不夠強」,更是「當它不夠強時,我們能多快停止使用它」。當前的演算法也會經歷相同的生命週期,密碼敏捷性就是為這段必然存在的升級窗口預先準備。
面對未來量子威脅:對稱加密、非對稱加密、雜湊遭遇的不是同一種問題
非對稱加密:面臨結構性威脅
像是 RSA 安全性依賴大整數因數分解困難;Diffie–Hellman 與橢圓曲線密碼學(ECC)的安全性則依賴離散對數困難,但面對量子電腦可以用 Shor’s algorithm 大幅加速這兩類問題。一旦量子運算達到實用規模,這些傳統系統都必須更換。
對稱加密:安全強度下降,而非立刻失效
對稱加密通常直接保護大量資料,例如資料庫欄位、檔案與傳輸內容。對它而言,Grover’s algorithm 理論上可把暴力搜尋的工作量從約 2^n 降到約 2^(n/2)。以 AES 為例,可粗略理解為:AES-128 的 128-bit 暴力破解安全強度可能降至約 64-bit;AES-256 則仍約為 128-bit。
雜湊:不是加密,但安全強度也會下降
雜湊函式不用來保密而是把任意長度輸入映射成固定長度摘要。數位簽章、檔案完整性、HMAC 與金鑰衍生⋯⋯等機制都依賴它的不同性質:
- 原像抗性(preimage resistance):給定摘要,難以找出能產生它的輸入
- 第二原像抗性(second-preimage resistance):給定一份輸入,難以找到另一份不同輸入卻有相同摘要
- 碰撞抗性(collision resistance):難以找出任意兩份不同輸入,卻有相同摘要
同一個雜湊演算法用在不同地方,依賴的性質、面對的攻擊與真正的防線都不一樣。以 SHA-256 為例:
| 用途 | 依賴的性質 | 攻擊者要做的事 | 傳統安全強度 | 量子後 | 真正的防線 |
|---|---|---|---|---|---|
| 數位簽章 | 碰撞抗性 | 自己造兩份同摘要的文件,騙你簽無害那份,再換成惡意那份 | 約 128-bit(生日攻擊,n/2) | 理論約 85-bit(BHT,n/3),實務上難以加速 | 更長摘要:SHA-384/SHA-512 |
| 完整性驗證(下載檔案 checksum) | 第二原像抗性 | 對一個「不是他選的」既定摘要找出另一份輸入 | 約 256-bit | 約 128-bit | 摘要本身要從可信管道取得,否則攻擊者連檔案帶摘要一起換掉 |
| HMAC | 金鑰保密與 PRF 性質,不依賴碰撞抗性 | 猜出金鑰 | 取金鑰長度與輸出長度較小者 | 金鑰強度約減半 | 夠長的隨機金鑰 |
| 密碼儲存 | 以上性質都不是重點 | 對低熵密碼做離線字典攻擊 | 與摘要長度幾乎無關 | 同左 | 慢速、加鹽、耗記憶體的演算法:Argon2id、scrypt、bcrypt |
| KDF(如 HKDF) | 隨機性萃取/PRF | 攻擊輸入的金鑰材料 | 受限於輸入金鑰的熵 | 約減半 | 高熵的輸入金鑰 |
真實案例:SHA-1 在 2017 年被 SHAttered 做出實際碰撞,因此不再適用於簽章;但 HMAC-SHA1 不依賴碰撞抗性,並沒有因此被攻破(雖然仍建議淘汰)。MD5 也是同樣的情況。
和 AES 一樣,這不代表 SHA-256 已經不安全。重點是盤查時不能只記「用了 SHA-256」,而要記「演算法 × 用途」:簽章用途最依賴碰撞抗性,應優先升級,密碼儲存的解法則不是換更長的雜湊,而是改用 Argon2id,並把成本參數一起版本化。
Harvest now, decrypt later
有些資料的機密性比系統生命週期更長,例如醫療紀錄、政府文件、商業機密。攻擊者可以今天先錄下以傳統非對稱加密保護的流量或密文,未來再嘗試解密。這種「現在收集、未來解密」的風險,讓後量子密碼(PQC)的遷移不能只等到量子電腦出現才開始。
實踐密碼學敏捷性
設計一個單向不可回退的系統,永遠只能逐步替換上更新的演算法
- 識別:從資料、Token、憑證或協定交握中辨識版本、演算法與金鑰識別碼(Key ID /
kid) - 選擇:由集中且明確的政策決定新資料使用什麼方案,而不是讓呼叫端任意指定演算法
- 共存:在必要期間能解密/驗證受支援的舊版本,同時只產生新版本
- 輪替與退役:能加入新金鑰或新版本、觀測使用量,並在截止後移除舊方案
- 可觀測與可測試:知道線上還有多少舊資料、遷移是否失敗,以及不同實作是否真的互通
以實作一個加解密介面為例
一、定義密文元資料與其介面
// 1. 密文格式應包含其版本與使用的 KeyIDtype Envelope struct { Version string `json:"v"` // 例如 v2 KeyID string `json:"kid"` // 例如 customer-2026-01,金鑰 ID 本身不是秘密,意義在於方便將真實金鑰存放到更安全的地方與輪替 Payload string `json:"ciphertext"`}
// 2. 密文應抽象出「加密」、「解密」兩種方法type Cipher interface { Encrypt(plaintext, aad []byte) (Envelope, error) Decrypt(envelope Envelope, aad []byte) ([]byte, error)}只拿到這一串密文,解密端就知道該用哪一套規則與哪一把金鑰。實際格式可以是 JSON、二進位欄位或前綴字串。
二、新寫入永遠走目前版本,舊版本只讀
前面 Cipher 同時要求 Encrypt 與 Decrypt 意味著每個版本也被迫實作完整的 Cipher,它就必須提供 Encrypt 方法,等於留了一條還能寫出舊格式的路,任何人在新功能裡都可能不小心呼叫到它,所以改良的結構如下:
type Decrypter interface { Decrypt(envelope Envelope, aad []byte) ([]byte, error)}
type Encrypter interface { Encrypt(plaintext, aad []byte) (Envelope, error)}
// 只有目前版本需要同時加密與解密。type CurrentCipher interface { Encrypter Decrypter}
// v1 只實作 Decrypter;刻意沒有 Encrypt。type LegacyV1 struct{}
// v2 是目前版本,因此實作 CurrentCipher。type AESGCMV2 struct{}省略具體實作,重點是 LegacyV1 只符合 Decrypter,而 AESGCMV2 同時符合 Encrypter 和 Decrypter。
- 介面
- Decrypter:只能解密。
- Encrypter:只能加密。
- CurrentCipher:兩者組合,只給目前版本使用。
- 實作
- LegacyV1 只符合 Decrypter,刻意不寫 Encrypt。
- AESGCMV2 同時符合 Encrypter 與 Decrypter。
把「舊版只讀」做成程式結構,而不是俗成約定。例如舊模組只提供 Decrypt,根本沒辦法 Encrypt。如此就算有人在新功能裡想沿用舊方案,也會在編譯時被擋下。
實際使用上在程式啟動時,從其他地方取得金鑰,建立目前的 v2 加密器與仍支援讀取的 v1 解密器:
type CipherRef struct { Version string KeyID string}
type CryptoService struct { writer Encrypter decrypters map[CipherRef]Decrypter}currentV2 := AESGCMV2{ /* keyID: customer-2026-01, key: ... */ }legacyV1 := LegacyV1{ /* keyID: customer-2019, key: ... */ }
crypto := CryptoService{ writer: currentV2, decrypters: map[CipherRef]Decrypter{ {Version: "v2", KeyID: "customer-2026-01"}: currentV2, {Version: "v1", KeyID: "customer-2019"}: legacyV1, },}func (c CryptoService) Decrypt(env Envelope, aad []byte) ([]byte, error) { ref := CipherRef{Version: env.Version, KeyID: env.KeyID}
d, ok := c.decrypters[ref] if !ok { return nil, errors.New("unsupported version or key id") } return d.Decrypt(env, aad)}如下使用者輸入稅務 ID 後,應用程式以不可變的紀錄身分組成 AAD,然後只呼叫服務的加密入口:
plaintext := []byte("A123456789")aad := []byte("tenant/acme:customers:42:tax-id")
encrypted, err := crypto.Encrypt(plaintext, aad)// encrypted 約為:// {"v":"v2", "kid":"customer-2026-01", "ciphertext":"Base64..."}
db.SaveCustomerTaxID(42, encrypted)讀取時,服務從儲存的 Version 和 KeyID 找到對應的解密器,再以相同的 AAD 解密:
stored := db.LoadCustomerTaxID(42)
plaintext, err := crypto.Decrypt( stored, []byte("tenant/acme:customers:42:tax-id"),)// plaintext == []byte("A123456789")如果這筆資料是歷史資料,stored 會帶著 v1 與 customer-2019,讀取路徑就會交給 LegacyV1.Decrypt(...)。
延伸閱讀
- The Rise of Crypto Agility: How to Secure Systems for the Quantum Era - IBM Technology
- NIST:DES
- NIST:Hash Functions
- NIST FIPS 203:Module-Lattice-Based Key-Encapsulation Mechanism Standard(ML-KEM)
- NIST SP 800-57 Part 1 Rev. 5:Key Management 建議
- NIST SP 800-38D:GCM 與 GMAC