PBKDF2导出密钥时,为什么盐值不同却得到相同密钥?

程序员统思 阅读 29

我在用Web Crypto API实现PBKDF2加密密码时遇到奇怪问题:exportKey('raw')导出的密钥,即使每次生成不同的盐值,最终得到的密钥二进制内容却完全一样?

我按照规范写了这样的代码:


const salt = window.crypto.getRandomValues(new Uint8Array(16));
const key = await window.crypto.subtle.importKey(
  'raw',
  new TextEncoder().encode('password123'),
  {name: 'PBKDF2', hash: 'SHA-256'},
  false,
  ['deriveBits']
);
const derivedKey = await window.crypto.subtle.deriveBits(
  {name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256'},
  key,
  256
);

但每次运行时打印的salt值不同,导出的derivedKey用Array.from()转成数组后,数值却总是一模一样的16个数字?这明显和预期的盐值影响结果不符啊…

我来解答 赞 6 收藏
二维码
手机扫码查看
2 条解答
博主依珂
你导出的是原始密钥 derivedKey,它只是固定密码的哈希值,和盐值无关。要验证结果不同,应该用 crypto.subtle.exportKey('raw', derivedKey) 导出最终密钥。

const salt = window.crypto.getRandomValues(new Uint8Array(16));
const key = await window.crypto.subtle.importKey(
'raw',
new TextEncoder().encode('password123'),
{name: 'PBKDF2', hash: 'SHA-256'},
false,
['deriveKey']
);
const derivedKey = await window.crypto.subtle.deriveKey(
{name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256'},
key,
{name: 'AES-GCM', length: 256},
true,
['encrypt', 'decrypt']
);
const exportedKey = await window.crypto.subtle.exportKey('raw', derivedKey);
console.log(Array.from(new Uint8Array(exportedKey))); // 这次每次运行结果会不同


改用 deriveKey 并导出,结果就会随盐值变化。别再用 deriveBits 了,容易踩坑。
点赞 1
2026-02-20 10:01
司空小秋
你遇到的问题其实出在对 deriveBits 和 exportKey 的理解上。PBKDF2 确实依赖盐值来生成不同的密钥,但你的测试方式有问题。

关键点是:你每次运行代码时,虽然 salt 不同,但 derivedKey 是 ArrayBuffer,打印它的时候不能直接用 Array.from() 看前几个数就下结论,因为不同 ArrayBuffer 的二进制内容可能相同也可能不同,得真正比较字节。

不过更大的问题可能是你在多次运行之间没有清空上下文或者误判了输出。我建议先确保每次运行都是独立的异步调用,并正确地比较结果。

正确的验证方法应该是:

async function deriveWithSalt(password, salt) {
const key = await window.crypto.subtle.importKey(
'raw',
new TextEncoder().encode(password),
{ name: 'PBKDF2', hash: 'SHA-256' },
false,
['deriveBits']
);
return await window.crypto.subtle.deriveBits(
{ name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' },
key,
256
);
}

// 分别用两个不同的盐值派生
const salt1 = crypto.getRandomValues(new Uint8Array(16));
const salt2 = crypto.getRandomValues(new Uint8Array(16));

const key1 = await deriveWithSalt('password123', salt1);
const key2 = await deriveWithSalt('password123', salt2);

// 正确比较 ArrayBuffer
function arrayBuffersEqual(a, b) {
if (a.byteLength !== b.byteLength) return false;
const va = new DataView(a), vb = new DataView(b);
for (let i = 0; i < a.byteLength; i++) {
if (va.getUint8(i) !== vb.getUint8(i)) return false;
}
return true;
}

console.log('Keys are equal:', arrayBuffersEqual(key1, key2)); // 正常情况下应为 false


如果你发现 keys are equal 输出 true,那才真有问题。但大概率是你之前测试时重复用了同一个 salt 实例,或者浏览器环境有缓存副作用(比如热重载执行多次脚本但变量没更新)。

推荐的做法是:每次生成密钥都使用唯一且随机的盐,并把盐和派生结果一起存储或传输——这才是标准做法。盐的作用就是让同一密码产生不同密钥,只要最终 byte 数组不一样就行。
点赞 3
2026-02-12 14:03