Javascript: 使用crypto.getRandomValues生成指定范围内的随机数

33
我了解到您可以使用以下函数在JavaScript中生成指定范围内的随机数:
function getRandomInt (min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

感谢 Ionuț G. Stan这里的帮助。

我想知道是否可以使用crypto.getRandomValues()在指定范围内生成一个更好的随机数,而不是使用Math.random()。我想要生成一个包括0到10在内的数字,或者0到1之间的数字,甚至是包括10到5000在内的数字。

你会注意到,Math.random()产生的数字是这样的: 0.8565239671015732

getRandomValues API可能会返回以下内容:

  • Uint8Array(1)生成231
  • Uint16Array(1)生成54328
  • Uint32Array(1)生成355282741

那么如何将其转换回十进制数,以便我可以使用上面相同的范围算法?还是说我需要一个新的算法?

这是我尝试过的代码,但效果不太好。

function getRandomInt(min, max) {       
    // Create byte array and fill with 1 random number
    var byteArray = new Uint8Array(1);
    window.crypto.getRandomValues(byteArray);

    // Convert to decimal
    var randomNum = '0.' + byteArray[0].toString();

    // Get number in range
    randomNum = Math.floor(randomNum * (max - min + 1)) + min;

    return randomNum;
}

在低端范围(范围为0-1)内,它返回的0比1更多。如何使用getRandomValues()来实现最佳效果?

非常感谢

7个回答

32

在我看来,使用 window.crypto.getRandomValues() 生成处于 [min..max] 范围内的随机数最简单的方法在此处描述:here

如果链接过长:

function getRandomIntInclusive(min, max) {
    const randomBuffer = new Uint32Array(1);

    window.crypto.getRandomValues(randomBuffer);

    let randomNumber = randomBuffer[0] / (0xffffffff + 1);

    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(randomNumber * (max - min + 1)) + min;
}

4
0xFFFFFFFF = uint32.MaxValue(+1是因为Math.random包括0,但不包括1) - Stefan Steiger

26

最简单的方法可能是通过拒绝抽样(参见 http://en.wikipedia.org/wiki/Rejection_sampling)。例如,假设max-min小于256:

function getRandomInt(min, max) {       
    // Create byte array and fill with 1 random number
    var byteArray = new Uint8Array(1);
    window.crypto.getRandomValues(byteArray);

    var range = max - min + 1;
    var max_range = 256;
    if (byteArray[0] >= Math.floor(max_range / range) * range)
        return getRandomInt(min, max);
    return min + (byteArray[0] % range);
}

2
在这里的讨论中 https://github.com/EFForg/OpenWireless/pull/195,提出了一个更通用的解决方案(int > 256)。我已经修改了OP并采纳了这个建议。 :) - Scott Arciszewski
2
请查看 https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js 中的 Diceware.prototype.random - caw
始终抛出错误 RangeError: Maximum call stack size exceeded - Bruno Costa

8

很多答案可能会产生偏见的结果,这里提供了一个没有偏见的解决方案。

function random(min, max) {
    const range = max - min + 1
    const bytes_needed = Math.ceil(Math.log2(range) / 8)
    const cutoff = Math.floor((256 ** bytes_needed) / range) * range
    const bytes = new Uint8Array(bytes_needed)
    let value
    do {
        crypto.getRandomValues(bytes)
        value = bytes.reduce((acc, x, n) => acc + x * 256 ** n, 0)
    } while (value >= cutoff)
    return min + value % range
}

这样做不会引入模数偏差吗? - Eric Elliott
1
@EricElliott,cutoff 指定的范围可以被 max-min+1 的范围均匀地整除,如果随机生成的 value 大于 cutoff,则继续执行 do ... while 直到获得一个不是这样的值。 - Chris_F

6
如果你在使用 Node.js,最好使用具有密码学安全性的伪随机数 crypto.randomInt。如果你不知道自己在做什么且没有经过同行评审,请不要编写此类敏感方法。

官方文档

crypto.randomInt([min,] max[, callback])

添加于:v14.10.0, v12.19.0

  • min <integer> 随机范围的起始值(包含)。默认值为 0。
  • max <integer> 随机范围的结束值(不包含)。
  • callback <Function> function(err, n) {}

返回一个随机整数 n,使得 min <= n < max。此实现避免了 取模偏差

范围(max-min)必须小于2^48。 minmax必须是安全整数
如果未提供回调函数,则随机整数将同步生成。
// Asynchronous
crypto.randomInt(3, (err, n) => {
  if (err) throw err;
  console.log(`Random number chosen from (0, 1, 2): ${n}`);
});

// Synchronous
const n = crypto.randomInt(3);
console.log(`Random number chosen from (0, 1, 2): ${n}`);
// With `min` argument
const n = crypto.randomInt(1, 7);
console.log(`The dice rolled: ${n}`);

5

召唤死灵。
嗯,这很容易解决。

考虑在没有加密随机数的情况下生成特定范围内的随机整数

// Returns a random number between min (inclusive) and max (exclusive)
function getRandomArbitrary(min, max) {
    return Math.random() * (max - min) + min;
}

/**
 * Returns a random integer between min (inclusive) and max (inclusive).
 * The value is no lower than min (or the next integer greater than min
 * if min isn't an integer) and no greater than max (or the next integer
 * lower than max if max isn't an integer).
 * Using Math.round() will give you a non-uniform distribution!
 */
function getRandomInt(min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

所以,您要做的就是用来自 crypt 的随机数替换 Math.random。

那么 Math.random 是什么?
根据MDN,Math.random() 函数返回一个浮点数伪随机数在范围内 0 至小于 1(包括 0,但不包括 1)。

因此,我们需要一个加密随机数 >= 0 and < 1 (不包括 <=)。

所以,我们需要一个从 getRandomValues 得到的非负(即 UNSIGNED)整数。
怎么做呢?

简单: 与其获取整数然后执行 Math.abs,我们直接获取一个 UInt:

var randomBuffer = new Int8Array(4); // Int8Array = byte, 1 int = 4 byte = 32 bit 
window.crypto.getRandomValues(randomBuffer);
var dataView = new DataView(array.buffer);
var uint = dataView.getUint32();

其缩写版本为:
var randomBuffer = new Uint32Array(1);
(window.crypto || window.msCrypto).getRandomValues(randomBuffer);
var uint = randomBuffer[0];

现在我们需要做的就是将uint除以uint32.MaxValue(又称为0xFFFFFFFF)以获得浮点数。由于我们不能在结果集中有1,所以我们需要除以(uint32.MaxValue + 1)来确保结果小于1。
通过除以(UInt32.MaxValue + 1)可以运行,因为JavaScript整数在内部是64位浮点数,因此它不会受到32位的限制。
function cryptoRand()
{
    var array = new Int8Array(4);
    (window.crypto || window.msCrypto).getRandomValues(array);
    var dataView = new DataView(array.buffer);

    var uint = dataView.getUint32();
    var f = uint / (0xffffffff + 1); // 0xFFFFFFFF = uint32.MaxValue (+1 because Math.random is inclusive of 0, but not 1) 

    return f;
}

这个的速记符号是:
function cryptoRand()
{
    const randomBuffer = new Uint32Array(1);
    (window.crypto || window.msCrypto).getRandomValues(randomBuffer);
    return ( randomBuffer[0] / (0xffffffff + 1) );
}

现在,您需要做的就是在上述函数中将 Math.random() 替换为 cryptoRand()。

请注意,如果 crypto.getRandomValues 在 Windows 上使用 Windows-CryptoAPI 获取随机字节,则不应将这些值视为真正的加密安全熵源。


2

Rando.js使用crypto.getRandomValues来为您完成这个操作。

console.log(rando(5, 10));
<script src="https://randojs.com/2.0.0.js"></script>

这是从源代码中提取出来的,如果你想看看幕后情况:

var cryptoRandom = () => {
  try {
    var cryptoRandoms, cryptoRandomSlices = [],
      cryptoRandom;
    while ((cryptoRandom = "." + cryptoRandomSlices.join("")).length < 30) {
      cryptoRandoms = (window.crypto || window.msCrypto).getRandomValues(new Uint32Array(5));
      for (var i = 0; i < cryptoRandoms.length; i++) {
        var cryptoRandomSlice = cryptoRandoms[i].toString().slice(1, -1);
        if (cryptoRandomSlice.length > 0) cryptoRandomSlices[cryptoRandomSlices.length] = cryptoRandomSlice;
      }
    }
    return Number(cryptoRandom);
  } catch (e) {
    return Math.random();
  }
};

var min = 5;
var max = 10;
if (min > max) var temp = max, max = min, min = temp;
min = Math.floor(min), max = Math.floor(max);
console.log( Math.floor(cryptoRandom() * (max - min + 1) + min) );


0

如果您关心数字的随机性,请阅读以下内容:

如果您使用一个六面骰子生成1到5之间的随机数,当您掷出6时该怎么办?有两种策略:

  1. 重新掷骰子直到您得到1到5之间的数字。这样可以保持随机性,但会增加额外的工作量。
  2. 将6映射到您想要的数字之一,比如5。这样做的工作量较小,但现在您的分布被扭曲了,会得到额外的5。

策略1是@arghbleargh提到的“拒绝抽样”,并在他们的答案和其他几个答案中使用。

策略2是@Chris_F所说的产生偏差结果。

因此,请理解所有原始帖子问题的解决方案都需要从一个伪随机数字分布映射到具有不同“桶”数量的另一个分布。

策略2可能是可以接受的,因为:

  • 采用第二种策略时,只要您取模,那么没有任何结果数字会比任何其他数字更有可能出现2倍。因此,它与第一种策略相比并不明显更容易猜测。
  • 只要您的源分布比目标分布要大得多,则在随机性上的偏斜将是可以忽略不计的,除非您正在运行蒙特卡罗模拟之类的东西(您可能首先不会在JavaScript中执行此操作,或者至少您将不会使用加密库来进行操作)。
  • Math.random()使用第二种策略,从一个约52位的数字(2^52个唯一数字)映射,尽管有些环境使用较低的精度(请参见此处)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接