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)
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 密钥