Saltar al contenido

¿Cómo generar hash SHA1 aleatorio para usar como ID en node.js?

Posteriormente a mirar en diversos repositorios y sitios webs al terminar hemos descubierto la solución que te mostramos pronto.

Solución:

243,583,606,221,817,150,598,111,409x más entropía

Recomendaría usar crypto.randomBytes. No es sha1, pero para propósitos de identificación, es más rápido y tan “aleatorio”.

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

La resultante string será el doble de largo que los bytes aleatorios que genere; cada byte codificado en hexadecimal tiene 2 caracteres. 20 bytes serán 40 caracteres hexadecimales.

Usando 20 bytes, tenemos 256^20 o 1,461,501,637,330,902,918,203,684,832,716,283,019,655,932,542,976 valores de salida únicos. Este es idéntico a las posibles salidas de 160 bits (20 bytes) de SHA1.

Sabiendo esto, no es realmente significativo para nosotros shasum nuestros bytes aleatorios. Es como tirar un dado dos veces pero solo aceptar la segunda tirada; pase lo que pase, tienes 6 resultados posibles en cada tirada, por lo que la primera tirada es suficiente.


¿Por qué es esto mejor?

Para entender por qué esto es mejor, primero tenemos que entender cómo funcionan las funciones hash. Las funciones de hash (incluido SHA1) siempre generarán la misma salida si se proporciona la misma entrada.

Digamos que queremos generar ID, pero nuestra entrada aleatoria se genera mediante el lanzamiento de una moneda. Tenemos "heads" o "tails"

% echo -n "heads" | shasum
c25dda249cdece9d908cc33adcd16aa05e20290f  -

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

Si "heads" aparece de nuevo, la salida SHA1 será la mismo como era la primera vez

% echo -n "heads" | shasum
c25dda249cdece9d908cc33adcd16aa05e20290f  -

Ok, lanzar una moneda no es un gran generador de ID aleatorio porque solo tenemos 2 salidas posibles.

Si usamos un dado estándar de 6 lados, tenemos 6 entradas posibles. ¿Adivina cuántas salidas posibles de SHA1? 6!

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

Es fácil engañarnos a nosotros mismos pensando solo porque el resultado de nuestra función aspecto muy al azar, que es muy aleatorio.

Ambos estamos de acuerdo en que un lanzamiento de una moneda o un dado de 6 caras sería un mal generador de ID aleatorio, porque nuestros posibles resultados SHA1 (el valor que usamos para la ID) son muy pocos. Pero, ¿qué pasa si usamos algo que tiene muchos más resultados? ¿Como una marca de tiempo con milisegundos? O JavaScript Math.random? O incluso un combinación de esos dos?

Calculemos cuántos identificadores únicos obtendríamos …


La singularidad de una marca de tiempo con milisegundos

Cuando usas (new Date()).valueOf().toString(), obtiene un número de 13 caracteres (p. ej., 1375369309741). Sin embargo, dado que este es un número de actualización secuencial (una vez por milisegundo), las salidas son casi siempre las mismas. Vamos a ver

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

Para ser justos, a efectos de comparación, en un minuto dado (un tiempo de ejecución de operación generoso), tendrá 60*1000 o 60000 únicos.


La singularidad de Math.random

Ahora, al usar Math.random, debido a la forma en que JavaScript representa números de punto flotante de 64 bits, obtendrá un número con una longitud de entre 13 y 24 caracteres. Un resultado más largo significa más dígitos, lo que significa más entropía. Primero, necesitamos averiguar cuál es la longitud más probable.

El siguiente guión determinará qué longitud es más probable. Hacemos esto generando 1 millón de números aleatorios e incrementando un contador basado en el .length de cada número.

// 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 );

Al dividir cada contador por 1 millón, obtenemos la probabilidad de la longitud del número devuelto por 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

Entonces, aunque no es del todo true, seamos generosos y digamos que obtiene una salida aleatoria de 19 caracteres; 0.1234567890123456789. Los primeros personajes siempre serán 0 y ., así que en realidad solo tenemos 17 caracteres aleatorios. Esto nos deja con 10^17+1 (para posible 0; ver notas a continuación) o 100,000,000,000,000,001 únicos.


Entonces, ¿cuántas entradas aleatorias podemos generar?

Bien, calculamos el número de resultados para una marca de tiempo de milisegundos y Math.random

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

Eso es un solo dado de 6.000.000.000.000.000.060.000 caras. O, para hacer que este número sea más digerible humanamente, esto es aproximadamente el mismo número que

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

Suena bastante bien, ¿verdad? Bueno, averigüémoslo ...

SHA1 produce un valor de 20 bytes, con posibles resultados de 256 ^ 20. Así que realmente no estamos usando SHA1 en todo su potencial. Bueno, ¿cuánto estamos usando?

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

¡Una marca de tiempo de milisegundos y Math.random usan solo 4.11e-27 por ciento del potencial de 160 bits de SHA1!

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

¡Santos gatos, hombre! Mira todos esos ceros. Entonces, cuanto mejor es crypto.randomBytes(20)? 243,583,606,221,817,150,598,111,409 veces mejor.


Notas sobre el +1 y frecuencia de ceros

Si te preguntas sobre el +1, es posible para Math.random para devolver un 0 lo que significa que hay 1 resultado único posible más que debemos tener en cuenta.

Basado en la discusión que ocurrió a continuación, tenía curiosidad acerca de la frecuencia con la que 0 vendría. Aquí hay un pequeño guión random_zero.js, Hice para obtener algunos datos

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

Luego, lo ejecuté en 4 subprocesos (tengo un procesador de 4 núcleos), agregando la salida a un archivo

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

Entonces resulta que un 0 no es tan difícil de conseguir. Después de que se registraron 100 valores, el promedio fue

1 en 3,164,854,823 randoms es un 0

¡Frio! Se requeriría más investigación para saber si ese número está a la par con una distribución uniforme de v8 Math.random implementación

Eche un vistazo aquí: ¿Cómo uso node.js Crypto para crear un hash HMAC-SHA1? Crearía un hash de la marca de tiempo actual + un número aleatorio para garantizar la unicidad del hash:

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

¡Hágalo también en el navegador!

EDITAR: esto realmente no encajaba en el flujo de mi respuesta anterior. Lo dejo aquí como una segunda respuesta para las personas que podrían estar buscando hacer esto en el navegador.

Puede hacer esto del lado del cliente en los navegadores modernos, si lo desea

// 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"

Requisitos del navegador

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

Si posees algún reparo o capacidad de mejorar nuestro sección eres capaz de dejar una explicación y con gusto lo interpretaremos.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *