Як генерувати випадковий хеш SHA1 для використання в якості ідентифікатора в node.js?


137

Я використовую цей рядок для створення ідентифікатора sha1 для node.js:

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

Проблема полягає в тому, що вона щоразу повертає один і той же ідентифікатор.

Чи можливо, щоб він генерував випадковий ідентифікатор кожного разу, щоб я міг використовувати його як ідентифікатор документа бази даних?


2
Не використовуйте sha1. Він більше не вважається надійним (стійким до зіткнень). Ось чому відповідь naomik краща.
Нільс Абілдгаард

Відповіді:


60

Подивіться тут: Як я можу використовувати Crypto 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');

44
Для набагато кращого підходу дивіться відповідь @ naomik нижче.
Габі Пуркару

2
Це також була чудовою відповіддю Габі, і лише крихітна швидкість, приблизно 15%. Чудова робота обох! Мені справді подобається бачити дату () в солі, це дає розробникові легку впевненість, що це буде унікальне значення у всіх, крім самих божевільних паралельних обчислювальних ситуаціях. Я знаю, що його дурні та випадкові біти (20) будуть унікальними, але ми можемо мати певну впевненість, оскільки ми можемо не бути знайомі з внутрішніми органами випадкової генерації іншої бібліотеки.
Дмитро R117

637

243,583,606,221,817,150,598,111,409x більше ентропії

Я рекомендую використовувати crypto.randomBytes . Це не так sha1, але в цілях ідентифікації це швидше і так само «випадково».

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

Отриманий рядок буде вдвічі довшим, ніж генеровані випадковими байтами; кожен байт, закодований у шістнадцятку, - це 2 символи. 20 байт становитиме 40 символів шістнадцяткової.

Використовуючи 20 байт, ми маємо 256^20або 1,461,501,637,330,902,918,203,684,832,716,283,019,655,932,542,976 унікальних вихідних значень. Це ідентично можливим 160-бітовим (20-байтовим) виходу SHA1.

Знаючи це, для нас shasumвипадкові байти насправді не мають сенсу . Це як закатати штамп двічі, але лише прийняти другий рулон; незважаючи ні на що, у вас є 6 можливих результатів для кожного випуску, тому першого виклику достатньо.


Чому це краще?

Щоб зрозуміти, чому це краще, спершу треба зрозуміти, як працюють хеширующие функції. Функції хешування (включаючи SHA1) завжди будуть генерувати один і той же вихід, якщо подається однаковий вхід.

Скажімо, ми хочемо генерувати ідентифікатори, але наш випадковий вхід генерується кидком монети. У нас є "heads"або"tails"

% echo -n "heads" | shasum
c25dda249cdece9d908cc33adcd16aa05e20290f  -

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

Якщо "heads"з'явиться знову, вихід SHA1 буде таким самим, як і вперше

% echo -n "heads" | shasum
c25dda249cdece9d908cc33adcd16aa05e20290f  -

Гаразд, тому кидання монети не є великим генератором випадкових ідентифікаторів, оскільки у нас є лише 2 можливих виходи.

Якщо ми використовуємо стандартну 6-сторонній штамп, у нас є 6 можливих входів. Відгадайте, скільки можливих виходів SHA1? 6!

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

Це легко обманювати себе, думаючи тільки тому , що вихід наших функцій виглядають дуже випадково, що вона є дуже випадковою.

Ми обоє погоджуємось, що кидання монети або 6-сторонній штамб призведе до поганого генератора випадкових ідентифікаторів, оскільки наші можливі результати SHA1 (значення, яке ми використовуємо для ідентифікатора) дуже мало. Але що робити, якщо ми використовуємо щось, що має набагато більше результатів? Як часова мітка з мілісекундами? Або JavaScript Math.random? Або навіть поєднання цих двох ?!

Давайте обчислимо, скільки унікальних ідентифікаторів ми отримаємо ...


Унікальність часової позначки з мілісекундами

Під час використання (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*1000або 60000унікальні речі .


Унікальність Math.random

Тепер, використовуючи Math.random, через те, як JavaScript представляє 64-бітні числа з плаваючою точкою, ви отримаєте число довжиною в будь-якому місці від 13 до 24 символів. Більш довгий результат означає більше цифр, що означає більше ентропії. Спочатку нам потрібно з’ясувати, яка найбільш вірогідна довжина.

Сценарій нижче визначає, яка довжина найбільш вірогідна. Ми робимо це, генеруючи 1 мільйон випадкових чисел і збільшуючи лічильник на основі .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 });

Розділивши кожен лічильник на 1 мільйон, ми отримуємо ймовірність довжини числа, повернутого з 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 використовує лише 4,11e-27 відсотків 160-бітного потенціалу SHA1!

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повернути показник, 0що означає ще 1 можливий унікальний результат, за який ми маємо враховувати.

Грунтуючись на обговоренні, яке відбулося нижче, мені було цікаво, наскільки часто 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

Тож виходить, що а 0не так складно дістатись. Після того як було записано 100 значень , середнє значення було

1 з 31648548523 виплат становить 0

Класно! Більше дослідження необхідно буде знати , якщо це число на одному рівні з рівномірним розподілом v8 по Math.randomреалізації


2
Будь ласка, дивіться моє оновлення; навіть мілісекунда - це тривалий час у світлі швидкості javascript! Якщо більш серйозна примітка, перші 10 цифр числа залишаються однаковими щосекунди; саме це робить Dateжахливим при виробництві хорошого насіння.
Дякую

1
Правильно. Хоча я дійсно включав лише тих, хто дав найбільший внесок в іншу відповідь, щоб продемонструвати, що 20 випадкових байтів все ще просто домінують з точки зору ентропії. Я не думаю, Math.randomщо коли-небудь виробить0.
Дякую

8
На 14 разів більше відгуків, ніж прийнятих відповідей ... але хто рахує? :)
zx81

2
@moka, кістки це множинне число форма штампу . Я використовую форму однини.
Дякую

2
crypto.randomBytesце безперечно шлях ^^
Дякую

28

Зробіть це і в браузері!

EDIT: це не дуже вписувалося в потік моєї попередньої відповіді. Я залишаю це як другу відповідь для людей, які, можливо, хочуть зробити це у браузері.

За бажанням цього клієнта можна використовувати в сучасних браузерах

// 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)не завжди гарантує двозначне значення (наприклад: (5).toString(16)= "5", а не "05"). Це не має значення, якщо ви не залежите від того, щоб ваш кінцевий результат був lenдовгим саме символів. У цьому випадку ви можете використовувати return ('0'+n.toString(16)).slice(-2);всередині своєї функції карти.
The Brawny Man

1
Чудовий код, дякую. Просто хотів додати: якщо ви збираєтесь використовувати його для значення idатрибута, переконайтеся, що ідентифікатор починається з літери: [A-Za-z].
GijsjanB

Чудова відповідь (та коментарі) - дуже вдячний, що ви також включили вимоги браузера у відповідь!
kevlarr

Вимоги до браузера неправильні. Array.from () не підтримується в IE11.
Префікс

1
Це було взято з вікі під час цієї відповіді. Ви можете редагувати цю відповідь, якщо хочете, але хто насправді переймається IE? Якщо ви намагаєтесь його підтримати, то все одно потрібно заповнити половину JavaScript ...
Дякую
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.