概念概述
密钥派生函数(KDF)(Key Derivation Functions)指的是:KDF 把低熵口令或共享密钥转换为一个或多个强密钥,口令场景刻意缓慢且耗内存,密钥扩展场景则快速并强调域分离。
从更严格的定义看,A key derivation function deterministically derives cryptographic key material from an input secret, with variants tuned either to resist brute force on passwords or to expand existing high-entropy keys. 该概念在工程实践中的价值,在于它能帮助团队用统一的语言描述一个具体的质量属性或设计约束,从而在评审与协作中减少歧义。
为什么需要它
It is used when storing passwords, deriving per-context keys from a master secret, and turning a shared secret into session keys. 它与 Public Key Infrastructure、XML External Entity Injection、NoSQL Injection 等概念经常一起出现,用于解决相近的工程问题。理解它的适用场景,有助于在多种备选方案之间做出有依据的取舍,而不是凭直觉选择技术方案。
常见误解
- a password hash and a general-purpose KDF are interchangeable
- adding more iterations helps only for password inputs
需要避免的反模式
- using a fast key expansion function to hash passwords
- deriving many keys by a trivial transformation of one secret
实践要点
- Password-oriented functions such as Argon2, scrypt, and bcrypt are deliberately slow and often memory-hard, making large-scale guessing expensive.
- Key expansion functions such as HKDF assume input with real entropy and focus on domain separation, letting one master key safely produce distinct keys for different purposes.
- Always separate keys by context using an informational string, so a key derived for one protocol cannot be silently reused in another.
参考来源
https://datatracker.ietf.org/doc/html/rfc5869
代码示例
function assertSecret(secret: string): void {
if (typeof secret !== "string" || secret.length === 0) {
throw new TypeError("secret must be a non-empty string");
}
}
export async function deriveAesKey(
secret: string,
salt: Uint8Array,
info: string,
): Promise<CryptoKey> {
assertSecret(secret);
if (!(salt instanceof Uint8Array)) throw new TypeError("salt must be a Uint8Array");
if (typeof info !== "string") throw new TypeError("info must be a string");
const material = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(secret),
"HKDF",
false,
["deriveKey"],
);
return crypto.subtle.deriveKey(
{ name: "HKDF", hash: "SHA-256", salt, info: new TextEncoder().encode(info) },
material,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"],
);
}
export async function deriveBytes(
secret: string,
salt: Uint8Array,
info: string,
length = 32,
): Promise<Uint8Array> {
assertSecret(secret);
if (!(salt instanceof Uint8Array)) throw new TypeError("salt must be a Uint8Array");
if (typeof info !== "string") throw new TypeError("info must be a string");
if (!Number.isInteger(length) || length < 1 || length > 255) {
throw new RangeError("length must be an integer between 1 and 255");
}
const material = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(secret),
"HKDF",
false,
["deriveBits"],
);
const bits = await crypto.subtle.deriveBits(
{ name: "HKDF", hash: "SHA-256", salt, info: new TextEncoder().encode(info) },
material,
length * 8,
);
return new Uint8Array(bits);
}