Math.random() 是 JavaScript 中的一个内置函数,用于生成一个介于 0(包含)和 1(不包含)之间的伪随机浮点数。也就是说,你可以期望得到任何大于等于 0 且小于 1 的数。
这是一个例子:
console.log(Math.random());
// 输出:0.12345678901234567 (示例,实际输出会是一个随机数)
由于 Math.random() 生成的是 [0, 1) 范围内的数,所以如果你想要在不同的范围内生成随机数,你需要进行一些额外的操作。例如,如果你想生成一个介于 1 和 10 之间的随机整数,你可以这样做:
var randomInt = Math.floor(Math.random() * 10) + 1;
console.log(randomInt);
// 输出:5 (示例,实际输出会是一个 1 到 10 之间的随机整数)
在这个例子中,Math.random() * 10 会生成一个 [0, 10) 范围内的随机浮点数,然后 Math.floor() 函数将其向下取整,最后加 1 将范围移动到 [1, 10]。
需要注意的是,Math.random() 生成的是伪随机数,因为它们是由算法生成的,如果知道算法和初始种子,就可以预测到随机数序列。因此,对于需要高度安全性的场景(如密码生成、加密等),不应该使用 Math.random() ,而应该使用能够生成真随机数的方法或库。
原理
Math.random() 函数的工作原理基于所谓的伪随机数生成器(PRNG,Pseudorandom Number Generator)。伪随机数生成器是一种算法,它使用数学公式或预定义的值(称为种子)来生成看起来像随机的数字序列。
虽然这些数字看起来是随机的,但实际上它们是由完全确定的过程生成的,因此被称为“伪随机”。如果你知道生成器的种子和算法,你就可以预测它将生成的所有数字。
JavaScript 的标准由 ECMA International 的 TC39 委员会制定,这个标准被称为 ECMAScript。然而,ECMAScript 标准并没有规定 Math.random() 的具体实现。它只要求 Math.random() 必须返回一个新的、伪随机的、在 [0, 1) 范围内的浮点数。
JavaScript 中的 Math.random() 函数并没有公开它使用的具体 PRNG 算法,这取决于浏览器或 JavaScript 引擎的实现。一些常见的 PRNG 算法包括线性同余生成器(LCG) 、梅森旋转(Mersenne Twister) 等。
需要注意的是,由于 PRNG 的可预测性,Math.random() 不应该用于需要高度随机性的场景,如加密或密码生成。在这些情况下,应该使用密码学安全的随机数生成器(CSPRNG)。
线性同余生成器(LCG)
线性同余生成器(LCG)是一种简单的伪随机数生成器。它的工作原理是使用一个线性方程来生成新的随机数。下面是一个在 JavaScript 中实现 LCG 的简单示例:
// 线性同余生成器的参数
var m = Math.pow(2, 32); // 模数
var a = 1103515245; // 乘数
var c = 12345; // 增量
// 种子值
var seed = 1;
// 自定义的随机数生成函数
function customRandom() {
// 线性同余生成器的公式
seed = (a * seed + c) % m;
return seed / m;
}
console.log(customRandom());
// 输出:0.25693503906950355 (示例,实际输出会是一个随机数)
在这个示例中,customRandom 函数使用线性同余生成器的公式 (a * seed + c) % m 来生成新的随机数。这个公式中的 a、c 和 m 是常数,seed 是当前的随机数。每次调用 customRandom 函数时,它都会更新 seed 的值,并返回一个新的随机数。
需要注意的是,虽然 LCG 可以生成看起来是随机的数字序列,但它生成的随机数实际上是可预测的。因此,LCG 不适合用于需要高度安全的场景,如密码生成、加密等。在这些场景中,应该使用密码学安全的随机数生成器。
梅森旋转(Mersenne Twister)
梅森旋转(Mersenne Twister)是一种伪随机数生成器,它以其高质量的随机数和高效的性能而闻名。以下是一个简单的在JavaScript中实现梅森旋转的示例:
let N = 624;
let M = 397;
let MATRIX_A = 0x9908b0df;
let UPPER_MASK = 0x80000000;
let LOWER_MASK = 0x7fffffff;
let mt = new Array(N);
let mti = N + 1;
function init(seed = Date.now()) {
mt[0] = seed >>> 0;
for (mti = 1; mti < N; mti++) {
const s = mt[mti - 1] ^ (mt[mti - 1] >>> 30);
mt[mti] = (((((s & 0xffff0000) >>> 16) * 1812433253) << 16) + (s & 0x0000ffff) * 1812433253) + mti;
mt[mti] >>>= 0;
}
}
function generate() {
let y;
const mag01 = new Uint32Array([0x0, MATRIX_A]);
if (mti >= N) {
let kk;
for (kk = 0; kk < N - M; kk++) {
y = (mt[kk] & UPPER_MASK) | (mt[kk + 1] & LOWER_MASK);
mt[kk] = mt[kk + M] ^ (y >>> 1) ^ mag01[y & 0x1];
}
for (; kk < N - 1; kk++) {
y = (mt[kk] & UPPER_MASK) | (mt[kk + 1] & LOWER_MASK);
mt[kk] = mt[kk + (M - N)] ^ (y >>> 1) ^ mag01[y & 0x1];
}
y = (mt[N - 1] & UPPER_MASK) | (mt[0] & LOWER_MASK);
mt[N - 1] = mt[M - 1] ^ (y >>> 1) ^ mag01[y & 0x1];
mti = 0;
}
y = mt[mti++];
y ^= (y >>> 11);
y ^= (y << 7) & 0x9d2c5680;
y ^= (y << 15) & 0xefc60000;
y ^= (y >>> 18);
return y >>> 0;
}
init();
console.log(generate());
这段代码实现了一个名为梅森旋转(Mersenne Twister)的伪随机数生成器。这是一种常用的随机数生成算法,因为它能够生成具有很长周期(通常为2^19937-1)和高度均匀分布的随机数序列。
>>> 是 JavaScript 中的无符号右移操作符。它将第一个操作数的二进制表示向右移动指定的位数,由第二个操作数给出。向右移动后,左侧产生的空位用零填充,而右侧超出的位被丢弃。这种操作符与其他大多数语言中的右移操作符不同,因为它不考虑符号位,总是在左侧插入零,而不是复制最左边的位。
在这段代码中,>>> 操作符主要用于确保生成的随机数是非负的。例如,seed >>> 0 将 seed 变量转换为非负整数。如果 seed 已经是非负整数,那么 >>> 0 不会有任何效果。但是,如果 seed 是负数,那么 >>> 0 会将其转换为对应的无符号整数。这是通过将 seed 的二进制表示视为无符号整数来实现的。
这段代码的主要部分是 generate 函数,它实现了梅森旋转算法的核心部分。这个函数首先检查是否需要重新生成内部状态数组 mt。如果需要,它会使用一系列的位操作和模运算来生成新的状态。然后,函数会选择一个新的随机数,再次使用一系列的位操作来改变这个数,最后返回这个数作为生成的随机数。
需要注意的是,虽然梅森旋转算法可以生成高质量的随机数,但它生成的随机数仍然是可预测的,因此不适合用于需要高度安全的场景,如密码生成、加密等。在这些场景中,应该使用密码学安全的随机数生成器。
优化种子值
在实际应用中,种子值通常会被设置为一个变化的值,比如当前的时间戳,以确保每次运行时都能生成不同的随机数序列。这里是一个改进的版本,使用当前时间作为种子:
// 线性同余生成器的参数
var m = Math.pow(2, 32); // 模数
var a = 1103515245; // 乘数
var c = 12345; // 增量
// 使用当前时间作为种子值
var seed = Date.now();
// 自定义的随机数生成函数
function customRandom() {
// 线性同余生成器的公式
seed = (a * seed + c) % m;
return seed / m;
}
console.log(customRandom());
// 输出:0.6573488399624825 (示例,实际输出会是一个随机数)
使用时间戳作为种子值确实增加了随机性,因为时间戳在每一毫秒都在变化,但它仍然是可预测的。如果攻击者知道你的算法和你生成随机数的精确时间,他们理论上可以重现你的随机数序列。
在实践中,为了生成真正的随机数,你需要从一个真正随机的源头获取数据,比如物理现象(如大气噪声、放射性衰变等)或者硬件设备(如鼠标移动、键盘敲击等) 。然而,在许多情况下,这是不可能的,因此我们通常使用伪随机数生成器,并尽可能地使种子值难以预测。
在需要高度安全的场景(如密码生成、加密等),应该使用密码学安全的随机数生成器(CSPRNG)。CSPRNGs被设计成即使知道了算法,也无法预测输出的序列,除非你知道了内部的状态。在JavaScript中,可以使用 crypto.getRandomValues() 函数来生成密码学安全的随机数。
crypto.getRandomValues()
crypto.getRandomValues() 是 Web Cryptography API 的一部分,它在浏览器环境中提供了一种生成密码学安全的随机数的方法。
这个函数的工作原理基于密码学安全的伪随机数生成器(CSPRNG) 。CSPRNG 是一种特殊类型的伪随机数生成器,它的设计目标是使得即使知道了生成器的所有历史输出,也无法预测未来的输出,除非你知道生成器的内部状态。
具体的 CSPRNG 算法有很多种,如 Fortuna、Yarrow、CryptGenRandom 等。这些算法通常会使用一些难以预测的源(如硬件噪声、用户输入等)来初始化和(或)定期重新设置生成器的内部状态,以确保生成的随机数序列的安全性。
然而,crypto.getRandomValues() 函数的具体实现(包括它使用的 CSPRNG 算法)是由浏览器决定的,不同的浏览器可能会有不同的实现。此外,由于这个函数的实现通常会涉及到底层的硬件和操作系统接口,因此它的具体工作原理可能会相当复杂,并且超出了 JavaScript 本身的范围。
crypto.getRandomValues() 底层原理
crypto.getRandomValues() 是 Web Cryptography API 的一部分,它用于生成密码学安全的随机数。这个函数在浏览器环境中可用,包括 Chrome 浏览器。
Chrome 浏览器的源代码是开源的,但是它的实现可能依赖于底层操作系统的功能,因此可能在不同的平台上有所不同。具体来说,crypto.getRandomValues() 的实现可能会调用操作系统提供的随机数生成函数,如 Linux 的 /dev/urandom 或 Windows 的 CryptGenRandom。
在 Chrome 的源代码中,crypto.getRandomValues() 的实现位于 third_party/blink/renderer/modules/crypto/crypto.cc 文件中。这个函数的实现首先检查输入的数组是否为空,然后调用底层的 Platform::current()->getCrypto()->getRandomValues() 函数来填充数组。
Platform::current()->getCrypto()->getRandomValues() 的实现位于 third_party/blink/renderer/platform/crypto.cc 文件中。这个函数使用 base::RandBytes 函数来生成随机数。
base::RandBytes 的实现位于 base/rand_util.cc 文件中。这个函数使用 base::RandomBitGenerator 类来生成随机数,这个类的实现会根据平台的不同而不同。在 Linux 平台上,它可能会使用 /dev/urandom,在 Windows 平台上,它可能会使用 CryptGenRandom。
需要注意的是,虽然 Chrome 的源代码是开源的,但是 crypto.getRandomValues() 的具体实现可能会依赖于不开源的操作系统功能。此外,这个函数的实现可能会随着 Chrome 的版本和平台的不同而有所不同。
Math.random() 与底层交互
当你在 JavaScript 中调用 Math.random() 时,实际上是在调用 V8 引擎内部的这个 RandomNumberGenerator 类。
RandomNumberGenerator::RandomNumberGenerator(): 这是RandomNumberGenerator类的构造函数。它首先检查是否有一个熵源(entropy source)可用。如果有,它会使用这个熵源来生成一个种子值(seed)。如果没有,它会使用不同的方法来生成一个种子值,具体的方法取决于操作系统。RandomNumberGenerator::NextInt(int max): 这个函数生成一个在 [0, max) 范围内的随机整数。RandomNumberGenerator::NextDouble(): 这个函数生成一个在 [0, 1) 范围内的随机浮点数。RandomNumberGenerator::NextInt64(): 这个函数生成一个随机的 64 位整数。RandomNumberGenerator::NextBytes(void* buffer, size_t buflen): 这个函数生成一串随机字节,并将它们存储在给定的缓冲区中。RandomNumberGenerator::SetSeed(int64_t seed): 这个函数设置随机数生成器的种子值。RandomNumberGenerator::MurmurHash3(uint64_t h): 这个函数实现了 MurmurHash3 哈希函数,用于生成种子值。
在 JavaScript 中调用 Math.random() 时,V8 引擎会创建一个 RandomNumberGenerator 实例(如果还没有的话),然后调用它的 NextDouble 方法来生成一个随机浮点数。这就是 Math.random() 如何调用这段代码的。
v8 种子值
在V8引擎中,Math.random() 函数的种子值是通过 RandomNumberGenerator 类的构造函数生成的。具体的生成方式取决于系统环境和是否提供了熵源(entropy source)。
以下是 RandomNumberGenerator 构造函数中种子生成的相关代码:
RandomNumberGenerator::RandomNumberGenerator() {
// Check if embedder supplied an entropy source.
{
MutexGuard lock_guard(entropy_mutex.Pointer());
if (entropy_source != nullptr) {
int64_t seed;
if (entropy_source(reinterpret_cast<unsigned char*>(&seed),
sizeof(seed))) {
SetSeed(seed);
return;
}
}
}
#if V8_OS_CYGWIN || V8_OS_WIN
// Use rand_s() to gather entropy on Windows. See:
// <https://code.google.com/p/v8/issues/detail?id=2905>
unsigned first_half, second_half;
errno_t result = rand_s(&first_half);
DCHECK_EQ(0, result);
result = rand_s(&second_half);
DCHECK_EQ(0, result);
USE(result);
SetSeed((static_cast<int64_t>(first_half) << 32) + second_half);
#elif V8_OS_DARWIN || V8_OS_FREEBSD || V8_OS_OPENBSD
// Despite its prefix suggests it is not RC4 algorithm anymore.
// It always succeeds while having decent performance and
// no file descriptor involved.
int64_t seed;
arc4random_buf(&seed, sizeof(seed));
SetSeed(seed);
#elif V8_OS_STARBOARD
SetSeed(SbSystemGetRandomUInt64());
#else
// Gather entropy from /dev/urandom if available.
FILE* fp = base::Fopen("/dev/urandom", "rb");
if (fp != nullptr) {
int64_t seed;
size_t n = fread(&seed, sizeof(seed), 1, fp);
base::Fclose(fp);
if (n == 1) {
SetSeed(seed);
return;
}
}
// We cannot assume that random() or rand() were seeded
// properly, so instead of relying on random() or rand(),
// we just seed our PRNG using timing data as fallback.
// This is weak entropy, but it's sufficient, because
// it is the responsibility of the embedder to install
// an entropy source using v8::V8::SetEntropySource(),
// which provides reasonable entropy, see:
// <https://code.google.com/p/v8/issues/detail?id=2905>
int64_t seed = Time::NowFromSystemTime().ToInternalValue() << 24;
seed ^= TimeTicks::Now().ToInternalValue();
SetSeed(seed);
#endif // V8_OS_CYGWIN || V8_OS_WIN
}
首先,如果提供了熵源,那么就直接使用熵源生成种子值。如果没有提供熵源,那么就根据不同的操作系统使用不同的方法来生成种子值。例如,在Windows系统上,使用rand_s()函数来生成种子值;在Darwin、FreeBSD和OpenBSD系统上,使用arc4random_buf()函数来生成种子值;在其他系统上,首先尝试从/dev/urandom获取熵,如果失败,则使用当前系统时间作为种子值。
密码学安全
密码学安全,也被称为加密安全,是指在密码学中使用的各种技术和方法,以保护信息和数据不被未经授权的个人、组织或设备访问或篡改。密码学安全涵盖了一系列的安全性质,包括但不限于以下几点:
- 机密性(Confidentiality) :只有被授权的用户才能访问信息。
- 完整性(Integrity) :信息在传输或存储过程中不被篡改或破坏。
- 认证(Authentication) :验证消息或用户的身份。
- 不可否认性(Non-repudiation) :涉及到交易双方不能否认已经发生的交易。
- 可用性(Availability) :确保信息和系统对授权用户始终可用。
在密码学中,安全性通常依赖于算法的强度、密钥的长度和保密性、以及系统整体的设计和实现。例如,使用强加密算法和足够长的密钥可以提高机密性和完整性,而使用数字签名和证书可以提高认证和不可否认性。
密码学安全的一个重要概念是“密码学安全性”,它是指一个密码系统能抵抗各种已知攻击的能力。如果一个密码系统可以抵抗所有已知的有效攻击,那么我们就可以说它是密码学安全的。然而,这并不意味着这个系统是绝对安全的,因为总是有可能存在未知的攻击方法。因此,密码学安全性是一个动态的概念,需要随着新攻击方法的出现而不断更新和改进。
密码学安全的随机数生成器(CSPRNG)
密码学安全的随机数生成器(Cryptographically Secure Pseudo-Random Number Generator,CSPRNG)是一种特殊的随机数生成器,它的输出难以预测,即使知道了所有的先前生成的数字。这是因为它们被设计成抵抗所有已知的密码攻击。CSPRNG有以下几个主要特点:
- 下一个数字不可预测:即使你知道了生成器产生的所有先前的数字,你也无法预测出下一个数字。这是因为CSPRNG使用的算法在每次生成新的数字时都会改变其内部状态。
- 无法重现序列:如果你不知道生成器的内部状态(例如,它的种子),你就无法重现它产生的数字序列。这意味着即使攻击者能够观察到一些输出,他们也无法确定生成器的内部状态,从而无法预测未来的输出。
- 具有前向安全性:即使在某一时刻的内部状态被泄露,也不应能够重构出之前的随机数。
- 满足统计随机性:输出的数字序列在统计上应该看起来是随机的,即所有的数字在输出中出现的概率都是均匀的,且数字之间没有可观察到的相关性。
- 具有高熵:熵是一个衡量随机性的度量,CSPRNG生成的随机数应具有高熵,即每个生成的位都应该尽可能接近完全随机。
这些特性使得CSPRNG在密码学和数据安全领域中非常重要,因为它们提供了一种生成难以预测和重现的随机数的方法,这对于许多密码学应用(如生成密钥、初始化向量等)来说是必不可少的。
crypto.getRandomValues() 与密码学安全
crypto.getRandomValues()是Web Cryptography API的一部分,它能够生成密码学安全的伪随机数。这些伪随机数基于密码学级别的随机数生成器(Cryptographically Secure Pseudorandom Number Generator,CSPRNG)生成,这就保证了其高度的随机性和安全性。具体来说,密码学级别的随机数生成器需满足两个主要的属性:
- 下一个比特是不可预测的:给出前N个比特,没有任何算法能以超过50%的概率预测出第N+1个比特。
- 后续的比特是不可改变的:即使你看到了前N个比特,你也不能影响第N+1个比特的值。
具体的实现方式会依赖于浏览器的实现和操作系统。在很多系统中,这种随机数生成器会使用硬件设备(如鼠标移动,键盘按键,磁盘驱动器的活动等)或者系统事件(如CPU使用率)作为随机的“种子” 。这些事件本质上是不可预测的,因此,它们可以提供强大的随机性。然后,这些种子数据会通过密码学安全的算法,如Fortuna或者Yarrow,进一步提升其安全性。
需要注意的是,即使是使用 crypto.getRandomValues() ,也必须遵循正确的密码学实践,例如,不要重复使用密钥,保持密钥的安全,及时销毁不再使用的密钥等。
再强调一次,crypto.getRandomValues() 函数的具体实现是取决于浏览器和操作系统的,因此,具体的代码实现可能会有所不同。
Web Cryptography API
Web Cryptography API的草案最早在2012年发布,随后在几年内经过了多次修订和改进。直到2017年,它最终被W3C推荐为正式的推荐标准。
大部分现代浏览器都支持Web Cryptography API,包括但不限于Chrome,Firefox,Safari,Edge等。但是具体的支持程度和API的实现可能会因浏览器和版本不同而不同。你可以在 "Can I use" 这个网站上查看最新的浏览器兼容性信息。
总的来说,Web Cryptography API并不是JavaScript语言的标准的一部分,而是W3C定义的一套用于浏览器中进行密码学操作的API,被大部分现代浏览器所支持。
下面是一些 Web Cryptography API 的主要特性:
- 随机数生成:如我们之前提到的
crypto.getRandomValues(),这个方法可以生成密码学级别的随机数。 - 加密和解密:该 API 提供了加密和解密数据的方法,如
window.crypto.subtle.encrypt()和window.crypto.subtle.decrypt()。 - 散列函数:如 SHA-256 或 SHA-512,这些函数可以用来生成数据的散列值。
- 消息认证码(MAC):这些函数可以用来验证数据的完整性和真实性。
- 签名和验证:这些方法可以用来验证数据是由特定的实体发送的。
- 密钥生成和管理:API提供了方法来生成、导出和导入各种不同类型的密钥。
- 密钥派生:这些函数可以用来从一些初始的密钥材料生成新的密钥。
这个 API 主要设计用于处理敏感数据,如密码和个人信息,因此它提供了一种在客户端进行安全操作的方式,减少了将数据发送到服务器的需求,从而降低了数据被拦截的风险。
需要注意的是,尽管 Web Cryptography API 提供了一些基本的密码学功能,但使用它并不能保证应用程序的安全。开发者仍然需要理解和遵守基本的密码学和安全实践,如使用安全的连接,不在不安全的环境中存储敏感数据,以及正确地管理和销毁密钥等。
总结
在这篇文章中,我们深入研究了JavaScript的随机数生成方法,包括Math.random()以及更安全的crypto.getRandomValues() 。我们首先探讨了常见的伪随机数生成算法,如线性同余生成器(LCG)和梅森旋转(Mersenne Twister),解释了它们的工作原理以及如何优化种子值。然后,我们讨论了密码学安全性,阐述了密码学安全的随机数生成器(CSPRNG)的重要性。最后,我们深入了解了Web Cryptography API,特别是crypto.getRandomValues()的使用,以及它如何保证密码学安全。无论你是对密码学感兴趣,还是只是想了解JavaScript的随机数生成,这篇文章都会给你带来新的见解。