Key Expansion and Stretching through Key Derivation Function

透過金鑰衍生函式(KDF)確保單一金鑰多處安全加密

前言

當處理多個加解密應用時我納悶:「多一份金鑰需要管理,意味著多一個需要防守的東西」、「如何避免用戶使用一串非常簡單的金鑰讓破解變得非常容易」。而正好有個專門的算法:金鑰衍生函式 Key Derivation Function 正是為此類問題而生。

金鑰衍生函式 Key Derivation Function

將一個秘密資料(例如用戶輸入的密碼、或是單一的主金鑰),轉換成一個或多個符合密碼學強度要求的高強度金鑰

低破解成本金鑰

金鑰衍生函式

A 高破解成本金鑰

B 高破解成本金鑰

C 高破解成本金鑰

金鑰擴張 Key Expansion

避免大量金鑰管理問題,透過金鑰擴張從一隻金鑰延伸出多支金鑰

利用主金鑰加上不同的「上下文標籤」,動態衍生出各自獨立的子金鑰,即使其中一把子金鑰洩漏,也不代表能反推出主金鑰或其他用途金鑰。意味著以下多筆金鑰造成的問題都能解決:

  • 複雜管理成本(金鑰輪替、權限、部署同步)
  • 洩漏風險增加(實踐一致性)
package main
import (
"crypto/sha256"
"fmt"
"io"
"golang.org/x/crypto/hkdf"
)
func deriveKey(masterKey []byte, info string) ([]byte, error) {
hkdfReader := hkdf.New(
sha256.New,
masterKey,
nil,
[]byte(info), // 建立用途隔離(domain separation),避免不同場景衍生出相同用途金鑰
)
key := make([]byte, 32)
_, err := io.ReadFull(hkdfReader, key)
if err != nil {
return nil, err
}
return key, nil
}
func main() {
masterKey := []byte("super-secret-master-key")
jwtKey, _ := deriveKey(masterKey, "jwt-sign")
aesKey, _ := deriveKey(masterKey, "aes-encryption")
fmt.Printf("JWT Key: %x\n", jwtKey)
fmt.Printf("AES Key: %x\n", aesKey)
}

金鑰拉伸 Key Stretching

避免過於簡單的金鑰(如用戶密碼)容易被破解,透過金鑰拉伸來提升破解成本

在推導過程中加入「鹽」,並故意將運算過程設計得非常耗時或極度消耗記憶體。即使駭客竊取了資料庫也無法輕易使用彩虹表或暴力破解。

package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
"log"
"golang.org/x/crypto/argon2"
)
// GenerateSalt 產生指定長度的隨機鹽值 (Salt)
// 鹽值不需要保密,但每個用戶或每次衍生都應該是唯一的
func GenerateSalt(size int) ([]byte, error) {
salt := make([]byte, size)
_, err := rand.Read(salt)
if err != nil {
return nil, err
}
return salt, nil
}
func main() {
// 情境:用戶設定了一個非常脆弱的密碼
userPassword := "iloveyou123"
fmt.Printf("原始輸入密碼: %s\n", userPassword)
// 1. 產生 16 bytes 的隨機 Salt
// 在實際應用中,這個 Salt 會跟隨加密後的資料或 Hash 一起存進資料庫
salt, err := GenerateSalt(16)
if err != nil {
log.Fatalf("產生 Salt 失敗: %v", err)
}
// 2. 設定 Argon2id 的參數 (決定破解難度)
// 這些參數會直接影響運算時間與記憶體消耗,可依據伺服器性能調整
time := uint32(1) // 迭代次數 (Time cost)
memory := uint32(64 * 1024) // 使用的記憶體大小 (Memory cost, 這裡設為 64MB)
threads := uint8(4) // 平行運算的執行緒數量 (Degree of parallelism)
keyLength := uint32(32) // 我們期望輸出的金鑰長度:32 bytes = 256 bits (適合 AES-256)
// 3. 執行金鑰衍生
derivedKey := argon2.IDKey([]byte(userPassword), salt, time, memory, threads, keyLength)
// 4. 輸出結果
fmt.Println("--------------------------------------------------")
fmt.Printf("生成的 Salt (Hex) : %s\n", hex.EncodeToString(salt))
fmt.Printf("衍生的 256-bit 金鑰: %s\n", hex.EncodeToString(derivedKey))
fmt.Println("--------------------------------------------------")
}

要留意提高破解成本不意味著密碼就變得絕對安全,弱密碼仍舊是弱密碼,只是嘗試猜測成本可以被控制抬高。

總結

金鑰衍生函式實際上主要都在緩解人類操作金鑰上的缺陷:「脆弱記憶」、「沒遵循最佳實踐」,而透過相關算法來緩解問題。

  • 金鑰衍生函式
    • 金鑰擴張:從「已經夠安全主金鑰」衍生更多「子金鑰」
    • 金鑰拉伸:把「容易暴力破解的弱密碼」變成大幅提高破解成本的「高成本金鑰」

實務上是可以也甚至有必要透過兩次的金鑰衍生函式來達成將弱密碼轉換為多個高成本金鑰。

使用者密碼

金鑰拉伸

主金鑰

金鑰擴張

A 金鑰

B 金鑰

C 金鑰