如何生成随机SHA1哈希值以用作节点中的ID.js?243,583,606,221,817,150,598,111,409 倍熵我们很容易自欺欺人地认为,仅仅因为我们函数的输出看起来非常随机,它非常随机。毫秒时间戳和 Math.random 仅使用 SHA1 160 位电位的 4.11e-27%!

2022-08-30 03:02:52

我使用此行为节点生成sha1 id.js:

crypto.createHash('sha1').digest('hex');

问题是它每次都返回相同的id。

是否可以让它每次都生成一个随机ID,以便我可以将其用作数据库文档ID?


答案 1

243,583,606,221,817,150,598,111,409 倍熵

我建议使用crypto.randomBytes。它不是,但出于id目的,它更快,就像“随机”一样。sha1

var id = crypto.randomBytes(20).toString('hex');
//=> f26d60305dae929ef8640a75e70dd78ab809cfe9

生成的字符串将是您生成的随机字节的两倍;编码为十六进制的每个字节为 2 个字符。20 个字节将是 40 个字符的十六进制。

使用20字节,我们有或1,461,501,637,330,902,918,203,684,832,716,283,019,655,932,542,976唯一输出值。这与SHA1的160位(20字节)可能输出相同256^20

知道了这一点,这对我们的随机字节来说并不是真正有意义的。这就像掷骰子两次,但只接受第二次掷骰子;无论如何,你每个滚动有6个可能的结果,所以第一个滚动就足够了。shasum


为什么这样更好?

要理解为什么这更好,我们首先必须了解哈希函数的工作原理。如果给出相同的输入,散列函数(包括 SHA1)将始终生成相同的输出。

假设我们想要生成ID,但是我们的随机输入是由抛硬币生成的。我们有或"heads""tails"

% echo -n "heads" | shasum
c25dda249cdece9d908cc33adcd16aa05e20290f  -

% echo -n "tails" | shasum
71ac9eed6a76a285ae035fe84a251d56ae9485a4  -

如果再次出现,SHA1 输出将与第一次相同"heads"

% echo -n "heads" | shasum
c25dda249cdece9d908cc33adcd16aa05e20290f  -

好吧,所以掷硬币不是一个很好的随机ID生成器,因为我们只有2个可能的输出。

如果我们使用标准的6面芯片,我们有6个可能的输入。猜猜有多少个可能的SHA1输出?6!

input => (sha1) => output
1 => 356a192b7913b04c54574d18c28d46e6395428ab
2 => da4b9237bacccdf19c0760cab7aec4a8359010b0
3 => 77de68daecd823babbb58edb1c8e14d7106e83bb
4 => 1b6453892473a467d07372d45eb05abc2031647a
5 => ac3478d69a3c81fa62e60f5c3696165a4e5e6ac4
6 => c1dfd96eea8cc2b62785275bca38ac261256e278

我们很容易自欺欺人地认为,仅仅因为我们函数的输出看起来非常随机,非常随机。

我们都同意,抛硬币或6面骰子会使随机ID生成器出错,因为我们可能的SHA1结果(我们用于ID的值)非常少。但是,如果我们使用具有更多输出的东西呢?像带有毫秒的时间戳?还是JavaScript的?甚至是这两者的结合?!Math.random

让我们计算一下我们将获得多少个唯一ID...


毫秒时间戳的唯一性

使用 时,您将获得一个 13 个字符的数字(例如,)。但是,由于这是一个按顺序更新的数字(每毫秒一次),因此输出几乎总是相同的。一起来看看(new Date()).valueOf().toString()1375369309741

for (var i=0; i<10; i++) {
  console.log((new Date()).valueOf().toString());
}
console.log("OMG so not random");

// 1375369431838
// 1375369431839
// 1375369431839
// 1375369431839
// 1375369431839
// 1375369431839
// 1375369431839
// 1375369431839
// 1375369431840
// 1375369431840
// OMG so not random

公平地说,为了进行比较,在给定的分钟(慷慨的操作执行时间)内,您将拥有或唯一。60*100060000


数学的独特性.随机

现在,在使用 时,由于 JavaScript 表示 64 位浮点数的方式,您将获得一个长度在 13 到 24 个字符之间的数字。更长的结果意味着更多的数字,这意味着更多的熵。首先,我们需要找出最可能的长度。Math.random

下面的脚本将确定最有可能的长度。我们通过生成100万个随机数并根据每个数字增加一个计数器来实现此目的。.length

// get distribution
var counts = [], rand, len;
for (var i=0; i<1000000; i++) {
  rand = Math.random();
  len  = String(rand).length;
  if (counts[len] === undefined) counts[len] = 0;
  counts[len] += 1;
}

// calculate % frequency
var freq = counts.map(function(n) { return n/1000000 *100 });

通过将每个计数器除以 100 万,我们得到从 返回的数字长度的概率。Math.random

len   frequency(%)
------------------
13    0.0004  
14    0.0066  
15    0.0654  
16    0.6768  
17    6.6703  
18    61.133  <- highest probability
19    28.089  <- second highest probability
20    3.0287  
21    0.2989  
22    0.0262
23    0.0040
24    0.0004

所以,即使这并不完全正确,让我们慷慨地说,你得到了一个19个字符长的随机输出; 。第一个字符将始终是 和 ,因此实际上我们只得到 17 个随机字符。这给我们留下了(对于可能的;请参阅下面的注释)或100,000,000,000,000,000,001个唯一值。0.12345678901234567890.10^17+10


那么我们可以生成多少随机输入呢?

好的,我们计算了毫秒时间戳的结果数量,并且Math.random

      100,000,000,000,000,001 (Math.random)
*                      60,000 (timestamp)
-----------------------------
6,000,000,000,000,000,060,000

这是一个6,000,000,000,000,000,000,060,000面骰子。或者,为了使这个数字更容易被人类消化,这个数字大致

input                                            outputs
------------------------------------------------------------------------------
( 1×) 6,000,000,000,000,000,060,000-sided die    6,000,000,000,000,000,060,000
(28×) 6-sided die                                6,140,942,214,464,815,497,21
(72×) 2-sided coins                              4,722,366,482,869,645,213,696

听起来不错,对吧?好吧,让我们找出答案...

SHA1 生成一个 20 字节的值,可能具有 256^20 个结果。因此,我们真的没有使用SHA1来充分发挥其潜力。那么我们用了多少呢?

node> 6000000000000000060000 / Math.pow(256,20) * 100

毫秒时间戳和 Math.random 仅使用 SHA1 160 位电位的 4.11e-27%!

generator               sha1 potential used
-----------------------------------------------------------------------------
crypto.randomBytes(20)  100%
Date() + Math.random()    0.00000000000000000000000000411%
6-sided die               0.000000000000000000000000000000000000000000000411%
A coin                    0.000000000000000000000000000000000000000000000137%

神圣的猫,伙计!看看所有这些零。那么好多少呢?243,583,606,221,817,150,598,111,409倍。crypto.randomBytes(20)


关于 +1 和零频率的注意事项

如果您想知道 ,则可能会返回一个,这意味着我们必须考虑另外1个可能的唯一结果。+1Math.random0

根据下面发生的讨论,我对a出现的频率感到好奇。这里有一个小脚本,我做一些数据0random_zero.js

#!/usr/bin/env node
var count = 0;
while (Math.random() !== 0) count++;
console.log(count);

然后,我在4个线程中运行它(我有一个4核处理器),将输出附加到文件中

$ yes | xargs -n 1 -P 4 node random_zero.js >> zeroes.txt

所以事实证明,a并不难得到。记录 100 个值后,平均值为0

3,164,854,823 个随机数中有 1 个是 0

凉!需要更多的研究来了解这个数字是否与v8实现的均匀分布相当。Math.random


答案 2

看看这里:如何使用node.js Crypto来创建HMAC-SHA1哈希?我会创建当前时间戳的哈希值+随机数以确保哈希唯一性:

var current_date = (new Date()).valueOf().toString();
var random = Math.random().toString();
crypto.createHash('sha1').update(current_date + random).digest('hex');