Как сгенерировать случайный хеш SHA1 для использования в качестве идентификатора в node.js?

Я использую эту строку для генерации идентификатора sha1 для node.js:

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

Проблема в том, что он возвращает один и тот же идентификатор каждый раз.

Можно ли каждый раз генерировать случайный идентификатор, чтобы я мог использовать его в качестве идентификатора документа базы данных?

6 ответов

Решение

Посмотрите здесь: Как я могу использовать 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');

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

Легко обмануть себя, думая только потому, что вывод нашей функции выглядит очень случайным, что он очень случайный.

Мы оба согласны с тем, что бросок монеты или шестигранный кубик могут привести к плохому генератору случайных идентификаторов, потому что наших возможных результатов SHA1 (значение, которое мы используем для идентификатора) очень мало. Но что, если мы используем что-то, что имеет гораздо больше результатов? Как отметка времени с миллисекундами? Или JavaScriptMath.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 000001 уник.


Итак, сколько случайных входов мы можем генерировать?

Хорошо, мы рассчитали количество результатов для отметки времени в миллисекундах и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 15050981111409 раз лучше.


Заметки о+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 из 3 164 854 823 рандомов - это 0

Здорово! Потребуются дополнительные исследования, чтобы узнать, соответствует ли это число равномерному распределению v8. Math.random реализация

Сделайте это и в браузере!

РЕДАКТИРОВАТЬ: это действительно не вписывается в поток моего предыдущего ответа. Я оставляю это здесь в качестве второго ответа для людей, которые могут делать это в браузере.

Вы можете сделать это на стороне клиента в современных браузерах, если хотите

// 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) {
  var arr = new Uint8Array((len || 40) / 2);
  window.crypto.getRandomValues(arr);
  return [].map.call(arr, byteToHex).join("");
}

Хорошо, давайте проверим это!

generateId();
// "1e6ef8d5c851a3b5c5ad78f96dd086e4a77da800"

generateId(20);
// "d2180620d8f781178840"

Требования к браузеру

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

Если вы хотите получить уникальные идентификаторы, вы должны использовать UUID (универсальный уникальный идентификатор) / GUID (глобальный уникальный идентификатор).

Хэш должен быть детерминированным, уникальным и фиксированной длины для ввода любого размера. Таким образом, независимо от того, сколько раз вы запускаете хеш-функцию, вывод будет одинаковым, если вы используете один и тот же ввод.

UUID уникальны и генерируются случайным образом! Существует пакет под названием «uuid», вы можете установить его через npm с помощью

npm установить UUID

& В вашем коде импортируйте модуль

const { v4:uuidv4} = require('uuid');

// Вызовите метод uuidv4 или как вы его назовете при импорте и зарегистрируйте его, сохраните или назначьте. Метод возвращает UUID в виде строки.

console.log(uuidv4()); // Пример вывода: '59594fc8-6a35-4f50-a966-4d735d8402ea'

Вот ссылка на npm (если она вам нужна):https://www.npmjs.com/package/uuid

С помощью crypto это хороший подход, потому что это собственный и стабильный модуль, но есть случаи, когда вы можете использовать bcryptесли вы хотите создать действительно надежный и безопасный хеш. Я использую его для паролей, у него много методов хеширования, создания соли и сравнения паролей.

Метод 1 (генерировать соль и хеш для отдельных вызовов функций)

const salt = bcrypt.genSaltSync(saltRounds);
const hash = bcrypt.hashSync(myPlaintextPassword, salt);

Метод 2 (автогенерация соли и хеша):

const hash = bcrypt.hashSync(myPlaintextPassword, saltRounds);

Дополнительные примеры вы можете найти здесь: https://www.npmjs.com/package/bcrypt

Краткий метод, который работает в браузере:

      // Returns a 256 bit string, or the equivalent Uint8Array if `false` is
// passed in.

function get256RandomBits(returnAsString = true) {
  const uint8Array = new Uint8Array(32); // 32 bytes = 256 bits
  const rng = crypto.getRandomValues(uint8Array);
  if (returnAsString) {
    return Array.from(rng).map(b => b.toString(16).padStart(2, '0')).join('');
  }
  else {
    return rng;
  }
}

Пример вывода:
-c2fec24e465658aad1208d0a3f863585aa2e5fd30f4e0712e2c74239419700d3
-8f9ff25c2948b8ee39f77303e0678b6eae1382e3d2517e15a1c4de6840f5673b
-d212ff805630edb41d22c9b6fdd8db6e7f910ea25483bad8b598249a6c73d950

Я использую 256 бит вместо 160, поскольку этот вопрос был задан более 10 лет назад, но идея та же; просто измените вторую строку по мере необходимости.

Другие вопросы по тегам