使用JavaScript创建安全随机密码的完整指南

本文详细介绍了如何使用JavaScript生成安全的随机密码,包括常见错误示例分析、密码学安全随机数生成器的选择、浮点数偏差和模偏差的避免方法,并提供了完整的代码实现。

如何使用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;
}

总结

要编写安全的随机密码生成函数,我们应该考虑三个要点:

  1. 使用安全的随机数生成函数
  2. 避免使用浮点数
  3. 避免模偏差

该代码和相关演示可在https://password.hboeck.de/找到,所有代码都在GitHub上可用,采用非常宽松的0BSD许可证。

图片来源:SVG Repo,CC0

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计