偶尔,你会发现需要在浏览器上为你的用户生成一些秘密。它可能看起来很诱人,写一个像Math.random().toString(36).slice(2) 的单行字,然后就结束了。生成的字符串可能看起来是随机的,但在密码学上是不安全的。我相信你并不感到惊讶。
在大多数浏览器中,Math.random() 是使用伪随机数发生器(PRNG)实现的。这意味着,随机数来自于内部状态,由一个确定的算法对每一个新的随机数进行混杂。对用户来说,它似乎是随机的,因为算法是以这样的方式调整的,所以看起来是这样。然而,如果你知道发生器的内部状态,你就知道它所产生的所有未来数字。
关于Math.random
由于ECMAScript规范并没有定义Math.random 实现的算法,构建浏览器的开发者可以选择他们认为合适的实现。理想情况下,该算法应尽可能少地使用内存,并在具有显著的周期长度的情况下快速执行。
PRNG的周期长度是指算法开始重复后的输出比特数。
今天,大多数现代浏览器都使用xorshift128+ 。(阅读为什么Chrome V8改用这种算法)。它是最快的非加密安全的随机数生成器之一。你可以在这篇文章中阅读更多关于Math.random()的实现。
这些算法,包括xorshift ,可以被破解--这里有一篇关于它的文章。互联网上已经出现了很多旧的实现容易受到简单的预测攻击的例子,我在大学游戏时代知道的最流行的一个是来自CSGOJackpot。这里有更多关于这个的阅读。更多这样的故事,比如这个和这个告诉我们为什么使用Math.random()并不是一个好主意。虽然这些黑客今天可能无法利用,但它确实暴露了PRNGs在这种情况下使用的弱点。一个确定性的算法可以而且会被破解。
网络密码学API已经为你提供了保障
2017年,万维网联盟(W3C)提出了Web Cryptography API。它提供了一套加密基元,可以执行基本操作,如散列、签名生成和验证,以及加密和解密。此外,它还描述了一个API,供应用程序生成和管理执行这些操作所需的密钥材料。在MDN上阅读更多内容。
所有流行的浏览器都通过半全局的crypto 对象向JavaScript应用程序提供Web Crypto API的实现。对于生成秘密,getRandomValues 方法是我们需要的全部。该方法被广泛支持,并能生成加密安全的随机数。它接收一个typedArray 作为输入,并返回同一个数组,其内容被新生成的随机数所取代:
crypto.getRandomValues(new Uint8Array(10))
// Uint8Array(10) [137, 32, 62, 244, 157, 240, 103, 42, 47, 57]
实现并不使用真正的随机数生成器来保证足够的性能。相反,他们使用一个伪随机数发生器,用一个具有足够熵的值作为种子。这确保了性能和生成的随机性质量之间有一个良好的平衡。由于种子值是随机的,所以输出是加密安全的。
虽然这个API提供的方法和基元本质上是安全的,但很容易用错它们。在使用它的时候,要确保你的工作得到这方面知识的人的彻底审查。
使用API来生成秘密
第1步:生成种子
种子是一个Uint8数组,使用getRandomValues 。Uint8Array ,生成一个8位无符号整数的数组,填充随机数:
generateSeed() {
return window.crypto.getRandomValues(new Uint8Array(256));
}
第2步:在种子上循环操作
我们在生成的种子上进行循环,直到满足字符长度的要求。如果在这之前我们已经用完了种子,那么种子会再次生成。我们使用String.fromCharCode ,在循环中生成一个字符。如果它是有效的,它会被附加到函数结束时返回的秘密字符串中:
generate(length = 32): string {
let secret = "";
let randomSeed;
while (secret.length < length) {
randomSeed = generateSeed()
for (let ii = 0; ii < randomSeed.length; ii++) {
const char = String.fromCharCode(randomSeed[ii]);
// Append the character to secret if it is valid
if (validateCharacter(char)) {
secret += char;
}
if (secret.length === length) {
break;
}
}
}
return secret;
}
生成的字符将总是属于ASCII集。验证函数只是检查它是否属于ASCII可打印字符,即字母、数字、标点符号和一些杂项符号。
介绍一下Shifty
有时,浏览器可能不支持网络加密API。在这种情况下,开发人员没有其他选择,只能依靠Math.random 。最近,我们发布了一个库,用一个很小的包来解决这个问题。它被称为Shifty。
Shifty是一个使用TypeScript为网络构建的微小的零依赖性秘密发生器。Shifty是为浏览器制作的,不会与Node一起工作。你可以使用内置的密码模块来代替。
它提供了一个直接的API来生成任何长度的秘密。要开始使用shifty,只需使用你选择的软件包管理器安装它:
yarn add @deepsource/shifty
使用Shifty是非常简单的。只要用默认值初始化Shifty 类,并使用generate 方法来生成任何长度的秘密。如果crypto 对象不在window 上,Shifty 会退回到使用Math.random 。当使用回退方法时,它会在开发者控制台显示一个警告:
import Shifty from '@deepsource/shifty'
const shifty = new Shifty((harden = true), (defaultLength = 16))
shifty.generate((length = 12)) // G8qZt7PEha^s
Shifty只是一个单一的TypeScript文件。你可以在这里偷看源代码。如果你喜欢它,请给它一颗星!