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

137

Estoy usando esta línea para generar una identificación sha1 para node.js:

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

El problema es que está devolviendo la misma identificación cada vez.

¿Es posible que genere una identificación aleatoria cada vez para que pueda usarla como una identificación de documento de base de datos?

ajsie
fuente
2
No uses sha1. Ya no se considera seguro (resistente a colisiones). Es por eso que la respuesta de Naomik es mejor.
Niels Abildgaard

Respuestas:

60

Eche un vistazo aquí: ¿Cómo uso el cifrado node.js 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');
Gabi Purcaru
fuente
44
Para un enfoque mucho mejor, vea la respuesta de @ naomik a continuación.
Gabi Purcaru
2
Esta también fue una gran respuesta Gabi, y solo un poco más rápido, alrededor del 15%. ¡Buen trabajo ambos! De hecho, me gusta ver una Fecha () en la sal, le da al desarrollador una confianza fácil de que este será un valor único en todas las situaciones de computación paralela más locas. Sé que su tonto y randomBytes (20) será único, pero es solo una confianza que podemos tener porque es posible que no estemos familiarizados con los elementos internos de generación aleatoria de otra biblioteca.
Dmitri R117
636

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

Recomiendo usar crypto.randomBytes . No lo es sha1, pero para fines de identificación, es más rápido e igual de "aleatorio".

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

La cadena resultante tendrá el doble de longitud que los bytes aleatorios que genere; cada byte codificado para hexadecimal tiene 2 caracteres. 20 bytes serán 40 caracteres de hexadecimal.

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

Sabiendo esto, no es realmente significativo para nosotros para shasumnuestros bytes aleatorios. Es como lanzar un dado dos veces, pero solo aceptar el segundo lanzamiento; pase lo que pase, tiene 6 resultados posibles en cada lanzamiento, por lo que el primer lanzamiento es suficiente.


¿Por qué es esto mejor?

Para entender por qué esto es mejor, primero tenemos que entender cómo funcionan las funciones de 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 es generada por un lanzamiento de moneda. Tenemos "heads"o"tails"

% echo -n "heads" | shasum
c25dda249cdece9d908cc33adcd16aa05e20290f  -

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

Si "heads"vuelve a aparecer, la salida SHA1 será la misma que la primera vez.

% echo -n "heads" | shasum
c25dda249cdece9d908cc33adcd16aa05e20290f  -

Ok, entonces 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 SHA1 posibles? 6!

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

Es fácil engañarse pensando simplemente porque la salida de nuestra función parece muy aleatoria, que es muy aleatoria.

Ambos acordamos que un lanzamiento de moneda o un dado de 6 caras sería un generador de identificación aleatorio incorrecto, 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 una combinación de esos dos?

Calculemos cuántos ID únicos obtendríamos ...


La singularidad de una marca de tiempo con milisegundos

Al usarlo (new Date()).valueOf().toString(), obtienes un número de 13 caracteres (por ejemplo, 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, para fines de comparación, en un minuto dado (un tiempo de ejecución de operación generoso), tendrá 60*1000o 60000únicos.


La singularidad de Math.random

Ahora, cuando se usa Math.random, debido a la forma en que JavaScript representa números de coma 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 script determinará qué longitud es más probable. Hacemos esto generando 1 millón de números aleatorios e incrementando un contador basado en el .lengthde 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 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 cierto, seamos generosos y digamos que obtienes una salida aleatoria de 19 caracteres; 0.1234567890123456789. Los primeros caracteres siempre serán 0y ., por lo tanto, realmente solo obtendremos 17 caracteres aleatorios. Esto nos deja con 10^17 +1(para posible 0; vea las notas a continuación) o 100,000,000,000,000,001 únicos.


Entonces, ¿cuántas entradas aleatorias podemos generar?

Ok, 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 lados. O, para que este número sea más digerible humanamente, 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, descubramos ...

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

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

¡Una marca de tiempo de milisegundos y Math.random usa 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, ¿cuánto mejor es crypto.randomBytes(20)? 243,583,606,221,817,150,598,111,409 veces mejor.


Notas sobre la +1frecuencia y los ceros

Si se está preguntando acerca de +1esto, es posible Math.randomdevolver un, lo 0que significa que hay 1 resultado único más posible que debemos tener en cuenta.

Basado en la discusión que sucedió a continuación, tenía curiosidad sobre la frecuencia con la que 0surgiría. Aquí hay un pequeño script random_zero.jsque 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 hilos (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 a 0no es tan difícil de conseguir. Después de registrar 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 v8Math.random implementación

Gracias
fuente
2
Por favor vea mi actualización; ¡incluso un milisegundo es mucho tiempo en la tierra javascript de velocidad de la luz! En una nota más seria, los primeros 10 dígitos del número se mantienen iguales cada segundo; Esto es lo que hace Datehorrible producir buenas semillas.
Gracias el
1
Correcto. Aunque realmente solo incluí aquellos para dar la contribución más alta a la otra respuesta para demostrar que 20 bytes aleatorios todavía dominan en términos de entropía. No creo Math.randomque produzca un0.
Gracias, el
8
14 veces más votos positivos que la respuesta aceptada ... pero ¿quién está contando? :)
zx81
2
@moka, dice es la forma plural de morir . Estoy usando la forma singular.
Gracias
2
crypto.randomByteses definitivamente el camino a seguir ^^
Gracias
28

¡Hazlo también en el navegador!

EDITAR: esto realmente no encaja 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 este 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
Gracias
fuente
3
Number.toString(radix)no siempre garantiza un valor de 2 dígitos (ej .: (5).toString(16)= "5", no "05"). Esto no importa a menos que dependa de que su salida final tenga exactamente una lenlongitud de caracteres. En este caso, puede usar return ('0'+n.toString(16)).slice(-2);dentro de su función de mapa.
The Brawny Man
1
Gran código, gracias. Solo quería agregar: si lo va a usar para el valor de un idatributo, asegúrese de que la ID comience con una letra: [A-Za-z].
GijsjanB
Gran respuesta (y comentarios): ¡realmente aprecio que también hayas incluido los requisitos del navegador en la respuesta!
kevlarr
Los requisitos del navegador son incorrectos. Array.from () no es compatible con IE11.
Prefijo
1
Fue tomado de una wiki en el momento de esta respuesta. Puede editar esta respuesta si lo desea, pero ¿a quién le importa realmente IE? Si está tratando de soportarlo, tiene que rellenar la mitad de JavaScript de todos modos ...
Gracias el