如何生成随机的SHA1哈希以用作Node.js中的ID?


137

我正在使用此行为node.js生成sha1 id:

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

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

是否有可能每次都生成一个随机ID,因此我可以将其用作数据库文档ID?


2
不要使用sha1。它不再被认为是安全的(防碰撞)。这就是Naomik的答案更好的原因。
Niels Abildgaard

Answers:


60

在这里看看:如何使用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');

44
有关更好的方法,请参见下面的@naomik答案。
加比·普卡鲁

2
这对于加比来说也是一个很好的答案,而且速度要快一点,大约15%。都很棒!我实际上很喜欢看到盐中的Date(),它使开发人员容易放心,在除了最疯狂的并行计算情况下,这将是唯一的值。我知道它的傻和randomBytes(20)将会是唯一的,但是我们可以放心,因为我们可能不熟悉另一个库的随机生成的内部。
Dmitri R117

635

熵增加243,583,606,221,817,150,598,111,409x

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

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

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

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

知道这一点,对于我们shasum的随机字节而言,这实际上没有任何意义。这就像两次掷骰子,但只接受第二次掷骰子一样。无论如何,每轮您有6个可能的结果,因此第一轮就足够了。


为什么这样更好?

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

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

% echo -n "heads" | shasum
c25dda249cdece9d908cc33adcd16aa05e20290f  -

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

如果"heads"再次出现时,SHA1输出将是相同的,因为它是第一次

% 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。


时间戳的唯一性(以毫秒为单位)

使用时(new Date()).valueOf().toString(),您会得到一个13个字符的数字(例如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唯一的。


的独特性 Math.random

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

下面的脚本将确定最可能的长度。为此,我们生成了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个字符长的随机输出;0.1234567890123456789。前一个字符始终是0.,因此实际上我们只能得到17个随机字符。这给我们留下了10^17 +1(可能的话0;请参见下面的注释)或100,000,000,000,000,001个唯一身份。


那么我们可以产生多少个随机输入?

好的,我们计算了毫秒时间戳的结果数量, 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,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%

天哪,老兄!看所有那些零。那么有多少crypto.randomBytes(20)好呢?243,583,606,221,817,150,598,111,409倍。


关于+1零的和的注释

如果您想了解+1,则有可能Math.random返回a 0,这意味着我们还要考虑另外1种可能的唯一结果。

根据下面的讨论,我很好奇a 0出现的频率。这是一个小脚本,random_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 0并不难获得。记录100个值后,平均值为

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

凉!需要进行更多的研究才能知道该数字是否与v8 Math.random实施的均匀分布相称


2
请查看我的更新;在Lightspeed javascript领域,即使是一毫秒也是很长的时间!更为严重的是,该数字的前10个数字每秒都保持不变。这就是使人Date难以产生好的种子的原因。
谢谢您

1
正确。尽管我实际上只包括那些对其他答案贡献最大的内容,以证明20个随机字节仍然仅在熵方面占主导地位。我认为Math.random永远不会产生0.
谢谢您

8
投票数比接受的答案高14倍...但是谁在计数呢?:)
zx81 2014年

2
@moka,骰子die的复数形式。我使用的是单数形式。
谢谢您

2
crypto.randomBytes绝对是必经之路^^
谢谢

28

也可以在浏览器中做!

编辑:这与我之前的回答并不完全吻合。对于那些可能正在浏览器中执行此操作的人,我将其留在此处作为第二个答案。

如果愿意,您可以在现代浏览器中执行此客户端操作

// str byteToHex(uint8 byte)
//   converts a single byte to a hex string 
function byteToHex(byte) {
  return ('0' + byte.toString(16)).slice(-2);
}

// str generateId(int len);
//   len - must be an even number (default: 40)
function generateId(len = 40) {
  var arr = new Uint8Array(len / 2);
  window.crypto.getRandomValues(arr);
  return Array.from(arr, byteToHex).join("");
}

console.log(generateId())
// "1e6ef8d5c851a3b5c5ad78f96dd086e4a77da800"

console.log(generateId(20))
// "d2180620d8f781178840"

浏览器要求

Browser    Minimum Version
--------------------------
Chrome     11.0
Firefox    21.0
IE         11.0
Opera      15.0
Safari     5.1

3
Number.toString(radix)并不总是保证2位数的值(例如:(5).toString(16)=“ 5”,而不是“ 05”)。除非您依赖最终输出的len字符长度,否则这无关紧要。在这种情况下,您可以return ('0'+n.toString(16)).slice(-2);在map函数内部使用。
壮汉

1
很棒的代码,谢谢。只是想添加:如果要使用它作为id属性的值,请确保ID以字母开头:[A-Za-z]。
GijsjanB

很棒的答案(和评论)-非常感谢您在答案中包括了浏览器要求!
凯夫拉尔

浏览器要求不正确。IE11 支持Array.from()。
前缀

1
它是在回答此问题时来自Wiki的。您可以根据需要编辑此答案,但是谁真正关心IE?如果您想支持它,则无论如何都必须填充一半的JavaScript ...
谢谢
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.