Node.js - 如何使用crypto.randomBytes生成特定范围内的随机数

19

如何使用 crypto.randomBytes 生成特定范围内的随机数?

我希望能够生成这样的随机数:

console.log(random(55, 956)); // where 55 is minimum and 956 is maximum

我只能在random函数内使用crypto.randomBytes来生成指定范围内的随机数。

我知道如何将从randomBytes生成的字节转换为十六进制或十进制,但我无法通过数学计算从随机字节中获取指定范围内的随机数。


输出应该是整数还是浮点数? - CodesInChaos
@CodesInChaos 整数。 - Nicolo
相关:https://github.com/paragonie/random_compat 这是PHP编程,但由于PHP对整数和浮点数的处理方式,我们遇到了与您要正确实现此功能时相似的问题。 - CodesInChaos
5个回答

28

要在特定范围内生成随机数,可以使用以下公式

Math.random() * (high - low) + low

但您想使用crypto.randomBytes而不是Math.random(),此函数返回一个带有随机生成字节的缓冲区。然后,您需要将此函数的结果从字节转换为十进制数。可以使用biguint-format软件包完成此操作。要安装此软件包,只需使用以下命令:

npm install biguint-format --save

现在你需要将 crypto.randomBytes 的结果转换为十进制,可以按照以下方式进行:

var x= crypto.randomBytes(1);
return format(x, 'dec');

现在,您可以创建以下随机函数:

var crypto = require('crypto'),
    format = require('biguint-format');

function randomC (qty) {
    var x= crypto.randomBytes(qty);
    return format(x, 'dec');
}
function random (low, high) {
    return randomC(4)/Math.pow(2,4*8-1) * (high - low) + low;
}
console.log(random(50,1000));

@CodesInChaos:你的观点是正确的,但请注意OP只是想使用crypto.randomBytes,而不是想要结果用于密码学。另外,你所提到的“审查脚本”是什么? - President James K. Polk
1
@Mustafamg 如果你只生成了几个字节,就没有必要使用biguint-format npm。我们可以使用**parseInt(x.toString('hex'), 16)而不是format(x, 'dec')**,避免添加额外的模块。 - Nicolo
  1. @JamesKPolk 我无法以-1开头发表评论。
  2. 如果范围超过256个元素,则大多数结果都无法获得,即使对于较短的范围,偏差也非常大。
- CodesInChaos
1
@Mustafamg 还有一件事...你不能从一个字节中获取50到1000之间的所有可用数字。如果 high - low > 256,那么 randomC(1)/256 是错误的。如果 high - low <= 65536,则应使用 randomC(2)/65536,如果 high - low <= 16777216,则应使用 randomC(3)/16777216 等等... 如果 high - low 大于生成的字节的十进制值,则无法从范围中获取所有可能的数字。 - Nicolo
1
@Mustafamg 1) 不同的输出以不同的概率被选择,即它们是有偏差的。如果范围超过256,许多可能的结果将根本不会被选择。2) 如果范围很大,最大值将在不可能的数字之间,因为你除以256而不是255。3) Stackexchange防止发布以-1开头的评论,因为他们显然更喜欢人们在不给出理由的情况下进行负投票。 - CodesInChaos
显示剩余7条评论

5

感谢@Mustafamg的回答以及@CodesInChaos的大力帮助,我成功解决了这个问题。我进行了一些调整,并将范围增加到最大256^6-1或281,474,976,710,655。范围可以进一步增加,但需要使用额外的大整数库,因为256^7-1超出了Number.MAX_SAFE_INTEGER的限制。

如果有人遇到同样的问题,可以随意使用此解决方案。

var crypto = require('crypto');

/*
Generating random numbers in specific range using crypto.randomBytes from crypto library
Maximum available range is 281474976710655 or 256^6-1
Maximum number for range must be equal or less than Number.MAX_SAFE_INTEGER (usually 9007199254740991)
Usage examples:
cryptoRandomNumber(0, 350);
cryptoRandomNumber(556, 1250425);
cryptoRandomNumber(0, 281474976710655);
cryptoRandomNumber((Number.MAX_SAFE_INTEGER-281474976710655), Number.MAX_SAFE_INTEGER);

Tested and working on 64bit Windows and Unix operation systems.
*/

function cryptoRandomNumber(minimum, maximum){
 var distance = maximum-minimum;
 
 if(minimum>=maximum){
  console.log('Minimum number should be less than maximum');
  return false;
 } else if(distance>281474976710655){
  console.log('You can not get all possible random numbers if range is greater than 256^6-1');
  return false;
 } else if(maximum>Number.MAX_SAFE_INTEGER){
  console.log('Maximum number should be safe integer limit');
  return false;
 } else {
  var maxBytes = 6;
  var maxDec = 281474976710656;
  
  // To avoid huge mathematical operations and increase function performance for small ranges, you can uncomment following script
  /*
  if(distance<256){
   maxBytes = 1;
   maxDec = 256;
  } else if(distance<65536){
   maxBytes = 2;
   maxDec = 65536;
  } else if(distance<16777216){
   maxBytes = 3;
   maxDec = 16777216;
  } else if(distance<4294967296){
   maxBytes = 4;
   maxDec = 4294967296;
  } else if(distance<1099511627776){
   maxBytes = 4;
   maxDec = 1099511627776;
  }
  */
  
  var randbytes = parseInt(crypto.randomBytes(maxBytes).toString('hex'), 16);
  var result = Math.floor(randbytes/maxDec*(maximum-minimum+1)+minimum);
  
  if(result>maximum){
   result = maximum;
  }
  return result;
 }
}

目前为止,它的工作良好,您可以将其用作非常好的随机数生成器,但我严格不建议将此函数用于任何加密服务。如果您决定使用它,那么就要自行承担风险。

欢迎所有评论、建议和批评!


尝试使用 cryptoRandomNumber(-1,+1) 并检查每个结果的常见性。我期望它是 1/4 | 1/2 | 1/4,而不是您可能期望的均匀的 1/3。 - CodesInChaos
@CodesInChaos,感谢您的建议,您是对的。这是100万个请求的结果:-1x249836、0x501146、1x249018。而且这是我尝试的所有组合,不仅仅是从-1到1的范围内:( 有什么办法可以解决这个问题吗? - Nicolo
如果你使用max-min+1进行乘法运算并截断结果而不是四舍五入,从数学上讲是正确的,但确保浮点数舍入不会有时(非常罕见)返回max+1可能有点麻烦。由于您的代码没有使用双精度浮点数的全部53位,因此这应该是不可能的,但个人而言,我宁愿只使用整数算术编写代码。 - CodesInChaos
@CodesInChaos 非常感谢!它起作用了!经过数百万次的测试请求,我从未遇到过返回max+1的问题,但我添加了额外的代码来返回false,如果发生这种情况,我可以检测并重试。现在所有潜在的数字都是大致相等的。 - Nicolo
@CodesInChaos 你又说对了。由于最大值加1的概率非常低或根本不存在,如果出现max+1,我可以使用最大数。 - Nicolo
显示剩余3条评论

5

现在,crypto 包中有一个 randomInt() 函数。它是在 v14.10.0 和 v12.19.0 中添加的。

console.log(crypto.randomInt(55, 957)); // where 55 is minimum and 956 is maximum

上限是排除在外的。
这是(缩略版)实现代码:
// Largest integer we can read from a buffer.
// e.g.: Buffer.from("ff".repeat(6), "hex").readUIntBE(0, 6);
const RAND_MAX = 0xFFFF_FFFF_FFFF;

const range = max - min;

const excess = RAND_MAX % range;
const randLimit = RAND_MAX - excess;

while (true) {
  const x = randomBytes(6).readUIntBE(0, 6);
  // If x > (maxVal - (maxVal % range)), we will get "modulo bias"
  if (x > randLimit) {
    // Try again
    continue;
  }
  const n = (x % range) + min;
  return n;
}

请参考完整源代码官方文档获取更多信息。


4
要生成在范围 [55 .. 956] 内的数字,您需要先生成一个在范围 [0 .. 901] 内的随机数,其中 901 = 956 - 55。然后将刚生成的数字加上 55。
要生成在范围 [0 .. 901] 内的数字,请选择两个随机字节并屏蔽 6 位。这将给您一个在范围 [0 .. 1023] 内的 10 位随机数。如果该数字小于等于 901,则完成。如果大于 901,则丢弃它并获取两个新的随机字节。不要尝试使用 MOD 将数字转换为正确的范围,这会使输出失真,从而导致非随机性。
ETA:为了减少必须丢弃生成的数字的可能性。
由于我们从 RNG 中取出两个字节,因此我们得到一个在范围 [0 .. 65535] 内的数字。现在,65535 MOD 902 是 591。因此,如果我们的两个字节随机数小于 (65535 - 591),即小于 64944,我们可以安全地使用 MOD 运算符,因为现在范围内的每个数字都是等可能的。任何大于等于 64944 的两个字节数字仍然必须被丢弃,因为使用它会使输出偏离随机。之前,拒绝数字的机会是 (1024 - 901) / 1024 = 12%。现在,拒绝随机生成的数字的机会是 (65535 - 64944) / 65535 = 1%。我们更不太可能不得不拒绝随机生成的数字。
running <- true
while running
  num <- two byte random
  if (num < 64944)
    result <- num MOD 902
    running <- false
  endif
endwhile
return result + 55

你是指“MOD”运算符吗? - Artjom B.
1
除非您确切知道自己在做什么,否则请勿使用它。进行取模运算后很容易得到非随机结果。 - rossum
@rossum 感谢您的回答,但是这种方法还没有达到生产级别脚本的要求。总有可能连续几次得到超过901的数字...根据概率论,甚至有可能连续得到901以上的数字,一百万次或更多次。我知道这很不可能,但即使连续3次,也会对服务器性能产生长期影响。 - Nicolo
你尝试在测试环境中运行它了吗?看看问题实际上发生的频率和对系统产生的实际影响。过早的优化可能会导致比解决问题更多的问题。 - rossum
@rossum 在 10K 循环中,大于 901 的数字的平均概率约为 1K,我曾经连续出现了 2、3、4 和 5 次。这会导致性能下降约 10%,而我每小时必须运行数百万个请求。 - Nicolo
显示剩余2条评论

1
大多数其他解决方案的问题在于它们扭曲了分布(您可能希望分布是均匀的)。
@rossum的伪代码缺乏概括性。(但他在文本中提出了正确的解决方案)
// Generates a random integer in range [min, max]
function randomRange(min, max) {
    const diff = max - min + 1;

    // finds the minimum number of bit required to represent the diff
    const numberBit = Math.ceil(Math.log2(diff));
    // as we are limited to draw bytes, minimum number of bytes
    const numberBytes = Math.ceil(numberBit / 4);

    // as we might draw more bits than required, we look only at what we need (discard the rest)
    const mask = (1 << numberBit) - 1;

    let randomNumber;

    do {
        randomNumber = crypto.randomBytes(numberBytes).readUIntBE(0, numberBytes);
        randomNumber = randomNumber & mask;
    // number of bit might represent a numbers bigger than the diff, in that case try again
    } while (randomNumber >= diff);

    return randomNumber + min;
}

关于性能问题,基本上数字在50% - 100%的范围内(取决于参数)。最坏情况下,循环执行超过7次的概率不到1%,实际上,大多数情况下循环只执行一两次。 random-js库认为,大多数解决方案不能提供具有均匀分布的随机数,并提供了更完整的解决方案。

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