如何使用JavaScript创建安全随机密码
我最近需要在JavaScript代码中生成随机密码。令人惊讶的是,找到相关的指导和优秀示例非常困难。无论是Google、StackOverflow还是ChatGPT返回的结果,几乎都存在各种缺陷。
让我们看几个示例,学习如何创建真正安全的密码生成函数。我们的目标是从定义的字符集中生成固定长度的密码。密码应该来自安全的随机源,并且应该具有均匀分布,即每个字符出现的概率相同。虽然示例是JavaScript代码,但这些原则可以用于任何编程语言。
常见错误示例
Google上最先出现的示例之一是dev.to网站上的一篇博客文章。以下是相关代码:
1
2
3
4
5
6
7
8
|
/* 使用弱随机数生成器的示例,请勿使用 */
var chars = "0123456789abcdefghijklmnopqrstuvwxyz!@#$%^&*()ABCDEFGHIJKLMNOPQRSTUVWXYZ";
var passwordLength = 12;
var password = "";
for (var i = 0; i <= passwordLength; i++) {
var randomNumber = Math.floor(Math.random() * chars.length);
password += chars.substring(randomNumber, randomNumber +1);
}
|
在这个示例中,随机源是Math.random()函数。MDN中Math.random()的文档明确指出:
Math.random()不提供密码学安全的随机数。不要将其用于任何与安全相关的用途。请改用Web Crypto API,更具体地说,使用window.crypto.getRandomValues()方法。
使用加密安全随机数生成器
我们可以做一个更一般的陈述:每当我们需要用于安全目的的随机性时,应该使用密码学安全的随机数生成器。即使在非安全环境中,使用安全随机源通常也没有缺点。
所有现代操作系统都有内置的功能。不幸的是,由于历史原因,在许多编程语言中,存在简单且更广泛使用的随机数生成函数,而安全随机数的API通常带有额外的障碍,并且可能并不总是可用。然而,在JavaScript的情况下,crypto.getRandomValues()在所有主流浏览器中已经可用超过十年了。
浮点数偏差问题
另一个常见错误是使用浮点数进行转换:
1
2
3
4
|
/* 存在浮点数舍入偏差的示例,请勿使用 */
function generatePassword(length = 16) {
// ... 使用浮点数转换的代码
}
|
这种方法的问题是使用了浮点数。在安全和特别是密码学应用中,尽可能避免使用浮点数是一个好主意。浮点数会引入舍入误差,并且由于它们的存储方式,几乎不可能生成均匀分布。
模偏差问题
另一种常见方法是使用模运算:
1
2
3
4
5
6
7
8
|
/* 存在模偏差的示例,请勿使用 */
var generatePassword = (
length = 20,
characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~!@-#$'
) =>
Array.from(crypto.getRandomValues(new Uint32Array(length)))
.map((x) => characters[x % characters.length])
.join('')
|
这种方法引入了模偏差。当随机值的范围不是字符集大小的倍数时,某些字符出现的概率会高于其他字符。
解决方案:拒绝采样
避免模偏差的一种方法是使用拒绝采样。基本思想是丢弃导致较高概率的值:
1
2
3
4
5
|
const pwchars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const limit = 256 - (256 % pwchars.length);
do {
randval = window.crypto.getRandomValues(new Uint8Array(1))[0];
} while (randval >= limit);
|
等于或高于限制的值被丢弃。限制设置为字节的可能值数量模我们想要使用的不同字符数量。
完整实现代码
综合以上所有考虑,以下是一个生成15字符密码的JavaScript函数,由ASCII字母和数字组成:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
function simplesecpw() {
const pwlen = 15;
const pwchars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const limit = 256 - (256 % pwchars.length);
let passwd = "";
let randval;
for (let i = 0; i < pwlen; i++) {
do {
randval = window.crypto.getRandomValues(new Uint8Array(1))[0];
} while (randval >= limit);
passwd += pwchars[randval % pwchars.length];
}
return passwd;
}
|
总结
要编写安全的随机密码生成函数,我们应该考虑三个要点:
- 使用安全的随机数生成函数
- 避免使用浮点数
- 避免模偏差
该代码和相关演示可在https://password.hboeck.de/找到,所有代码都在GitHub上可用,采用非常宽松的0BSD许可证。
图片来源:SVG Repo,CC0