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

ajs*_*sie 123 javascript random sha1 entropy node.js

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

crypto.createHash('sha1').digest('hex');
Run Code Online (Sandbox Code Playgroud)

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

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

Tha*_*you 594

243,583,606,221,817,150,598,111,409x更多熵

我建议使用crypto.randomBytes.它不是sha1,但出于身份目的,它更快,而且就像"随机"一样.

var id = crypto.randomBytes(20).toString('hex');
//=> f26d60305dae929ef8640a75e70dd78ab809cfe9
Run Code Online (Sandbox Code Playgroud)

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

使用20个字节,我们256^20还是1,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  -
Run Code Online (Sandbox Code Playgroud)

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

% echo -n "heads" | shasum
c25dda249cdece9d908cc33adcd16aa05e20290f  -
Run Code Online (Sandbox Code Playgroud)

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

如果我们使用标准的6面模具,我们有6种可能的输入.猜猜有多少可能的SHA1输出?6!

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

只是因为我们函数的输出看起来非常随机,它非常随机的,所以很容易欺骗自己.

我们都同意抛硬币或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
Run Code Online (Sandbox Code Playgroud)

公平地说,为了进行比较,在给定的时间内(慷慨的操作执行时间),您将拥有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 });
Run Code Online (Sandbox Code Playgroud)

通过将每个计数器除以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
Run Code Online (Sandbox Code Playgroud)

所以,即使它并不完全正确,让我们慷慨地说你得到一个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
Run Code Online (Sandbox Code Playgroud)

这是一个单独的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
Run Code Online (Sandbox Code Playgroud)

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

SHA1产生一个20字节的值,可能有256 ^ 20个结果.所以我们真的没有使用SHA1来充分发挥其潜力.那我们用多少钱?

node> 6000000000000000060000 / Math.pow(256,20) * 100
Run Code Online (Sandbox Code Playgroud)

毫秒时间戳和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%
Run Code Online (Sandbox Code Playgroud)

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


关于+1零的频率和频率的注释

如果您对此感到疑惑+1,可以Math.random返回一个0,这意味着我们必须考虑另外一个可能的独特结果.

根据下面发生的讨论,我很想知道0会出现的频率.这是一个小脚本,random_zero.js我做了一些数据

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

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

$ yes | xargs -n 1 -P 4 node random_zero.js >> zeroes.txt
Run Code Online (Sandbox Code Playgroud)

事实证明,a 0并不难获得.记录100个值后,平均值为

1中3,164,854,823个 randoms是0

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

  • 我会选择这个作为正确的答案.很好! (34认同)
  • 比接受的答案多14倍...但是谁在数?:) (8认同)
  • 请看我的更新; 甚至一毫秒是在光速javascript土地很长一段时间!更严重的是,这个数字的前10位数每秒都保持不变; 这就是"日期"在生产优质种子方面的糟糕表现. (2认同)
  • @moka,*骰子*是*die*的复数形式.我正在使用单数形式. (2认同)
  • `crypto.randomBytes`绝对是走^^的方式 (2认同)

Gab*_*aru 56

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

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

  • 有关更好的方法,请参阅下面的@ naomik的答案. (42认同)
  • 这也是一个很好的答案 Gabi,只是快了一点点,大约 15%。两个都干得好!我实际上喜欢在盐中看到 Date(),它让开发人员轻松相信这将是唯一的价值,除了最疯狂的并行计算情况。我知道它的愚蠢和 randomBytes(20) 将是独一无二的,但这只是我们可以拥有的信心,因为我们可能不熟悉另一个库的随机生成的内部结构。 (2认同)

Tha*_*you 25

也可以在浏览器中完成!

编辑:这不符合我以前的答案的流程.我将它留在这里作为可能希望在浏览器中执行此操作的人的第二个答案.

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

// 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"
Run Code Online (Sandbox Code Playgroud)

好的,我们来看看吧!

Browser    Minimum Version
--------------------------
Chrome     11.0
Firefox    21.0
IE         11.0
Opera      15.0
Safari     5.1
Run Code Online (Sandbox Code Playgroud)

浏览器要求

// 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"
Run Code Online (Sandbox Code Playgroud)

  • 它是从发表此答案时的维基百科中获取的。如果你愿意,你可以编辑这个答案,但谁真正关心 IE?如果你想支持它,无论如何你都必须填充一半的 JavaScript... (3认同)
  • `Number.toString(radix)`并不总是保证2位数值(例如:`(5).toString(16)`="5",而不是"05").这并不重要,除非你依赖于你的最终输出完全是'len`字符长.在这种情况下,您可以使用`return('0'+ n.toString(16)).slice(-2);`在map函数内部. (2认同)