使用JavaScript生成安全随机密码的终极指南

本文深入探讨如何使用JavaScript生成真正安全的随机密码,分析常见方法的缺陷,并给出基于Web Crypto API和拒绝采样技术的无偏差实现方案,涵盖密码学安全随机数、浮点数陷阱和模偏差等关键技术要点。

如何使用JavaScript创建安全的随机密码

最近我在编写JavaScript代码时需要生成随机密码,但惊讶地发现很难找到正确的实现方法。Google、StackOverflow甚至ChatGPT提供的大多数方案都存在各种缺陷。

常见错误方案分析

错误示例1:使用Math.random()

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);
}

MDN文档明确指出:Math.random()不提供密码学安全的随机数,任何安全相关场景都应使用Web Crypto API的window.crypto.getRandomValues()方法。

错误示例2:浮点数转换偏差

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/* 存在浮点数舍入偏差的示例,切勿使用 */
function generatePassword(length = 16) {
    let generatedPassword = "";
    const validChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,.-{}+!\"#$%/()=?";
    
    for (let i = 0; i < length; i++) {
        let randomNumber = crypto.getRandomValues(new Uint32Array(1))[0];
        randomNumber = randomNumber / 0x100000000;
        randomNumber = Math.floor(randomNumber * validChars.length);
        generatedPassword += validChars[randomNumber];
    }
    return generatedPassword;
}

此方案虽然使用了安全的crypto.getRandomValues(),但通过浮点数转换会引入不均匀分布问题。

错误示例3:模偏差问题

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
 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. 使用密码学安全的crypto.getRandomValues()
  2. 避免浮点数运算
  3. 通过拒绝采样消除模偏差

技术原理详解

  1. 密码学安全随机数:现代操作系统都内置了密码学安全随机数生成器,浏览器通过Web Crypto API暴露该功能。

  2. 模偏差问题:当随机数范围(256)不是字符集大小(62)的整数倍时,直接取模会导致某些字符出现概率更高。例如:

    • 小写字母"a"对应5个值(0,62,124,186,248)
    • 大写字母"A"只对应4个值(26,88,150,212)
  3. 拒绝采样:通过丢弃会导致偏差的随机值(≥limit),确保剩余值能均匀分布。虽然理论上可能导致延迟,但实际影响可忽略不计。

实际应用

本文提供的代码已发布在0BSD许可下,包含:

  • 基础版:固定15字符长度,包含字母和数字
  • 可配置版:支持自定义长度和字符集

在线演示地址:https://password.hboeck.de/ GitHub仓库:包含完整实现代码

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