Come generare hash SHA1 casuale da utilizzare come ID in node.js?


137

Sto usando questa linea per generare un ID sha1 per node.js:

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

Il problema è che restituisce lo stesso ID ogni volta.

È possibile che generi ogni volta un ID casuale in modo da poterlo utilizzare come ID documento del database?


2
Non usare sha1. Non è più considerato sicuro (resistente alle collisioni). Ecco perché la risposta di Naomik è migliore.
Niels Abildgaard,

Risposte:


60

Dai un'occhiata qui: Come posso usare node.js Crypto per creare un hash HMAC-SHA1? Creerei un hash del timestamp corrente + un numero casuale per garantire l'unicità dell'hash:

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

44
Per un approccio molto migliore, vedi la risposta di @ naomik di seguito.
Gabi Purcaru,

2
Questa è stata anche un'ottima risposta Gabi, e solo un po 'più veloce, circa il 15%. Ottimo lavoro entrambi! In realtà mi piace vedere un Date () nel sale, dà allo sviluppatore una facile fiducia che questo sarà un valore unico in tutte le situazioni di elaborazione parallela tranne quelle più folli. So che il suo sciocco e randomBytes (20) sarà unico, ma è solo una fiducia che possiamo avere perché potremmo non avere familiarità con gli interni della generazione casuale di un'altra libreria.
Dmitri R117,

637

243.583.606.221.817.150.598.111.409x entropia in più

Consiglierei di usare crypto.randomBytes . Non è sha1, ma per scopi di identificazione, è più veloce e altrettanto "casuale".

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

La stringa risultante sarà lunga il doppio dei byte casuali generati; ogni byte codificato in esadecimale è di 2 caratteri. 20 byte saranno 40 caratteri esadecimali.

Usando 20 byte, abbiamo 256^20o 1.461.501.637.330.902.918.203.684.832.716.283.019.655.932.542.976 valori di output univoci. Ciò è identico alle possibili uscite di SHA1 a 160 bit (20 byte).

Sapendo questo, non è davvero significativo per noi per i shasumnostri byte casuali. È come tirare un dado due volte ma accettare solo il secondo lancio; non importa quale sia, hai 6 possibili esiti per ogni tiro, quindi il primo tiro è sufficiente.


Perché è meglio?

Per capire perché questo è meglio, dobbiamo prima capire come funzionano le funzioni di hashing. Le funzioni di hash (incluso SHA1) genereranno sempre lo stesso output se viene fornito lo stesso input.

Supponiamo di voler generare ID ma il nostro input casuale è generato da un lancio di una moneta. Abbiamo "heads"o"tails"

% echo -n "heads" | shasum
c25dda249cdece9d908cc33adcd16aa05e20290f  -

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

Se "heads"si ripresenta, l'uscita SHA1 sarà la stessa di prima volta

% echo -n "heads" | shasum
c25dda249cdece9d908cc33adcd16aa05e20290f  -

Ok, quindi il lancio di una moneta non è un ottimo generatore di ID casuali perché abbiamo solo 2 uscite possibili.

Se utilizziamo una matrice standard a 6 facciate, abbiamo 6 possibili ingressi. Indovina quante possibili uscite SHA1? 6!

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

È facile illuderci pensando solo perché l'output della nostra funzione sembra molto casuale, che è molto casuale.

Siamo entrambi d'accordo sul fatto che un lancio di una moneta o un dado a 6 facce renderebbe un cattivo generatore di ID casuale, perché i nostri possibili risultati SHA1 (il valore che utilizziamo per l'ID) sono molto pochi. E se usassimo qualcosa che ha molti più output? Come un timestamp con millisecondi? O JavaScript Math.random? O anche una combinazione di quei due ?!

Calcoliamo quanti ID unici avremmo ottenuto ...


L'unicità di un timestamp con millisecondi

Quando si utilizza (new Date()).valueOf().toString(), si ottiene un numero di 13 caratteri (ad esempio, 1375369309741). Tuttavia, poiché si tratta di un numero che si aggiorna in sequenza (una volta per millisecondo), le uscite sono quasi sempre le stesse. Diamo un'occhiata

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

Per essere onesti, a fini comparativi, in un dato minuto (un tempo di esecuzione generoso dell'operazione), avrai 60*1000o 60000unici.


L'unicità di Math.random

Ora, quando si utilizza Math.random, a causa del modo in cui JavaScript rappresenta i numeri in virgola mobile a 64 bit, si ottiene un numero con una lunghezza compresa tra 13 e 24 caratteri. Un risultato più lungo significa più cifre, il che significa più entropia. Innanzitutto, dobbiamo scoprire qual è la lunghezza più probabile.

Lo script seguente determinerà quale lunghezza è più probabile. Lo facciamo generando 1 milione di numeri casuali e incrementando un contatore in base al .lengthdi ogni numero.

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

Dividendo ciascun contatore per 1 milione, otteniamo la probabilità della lunghezza del numero restituito 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

Quindi, anche se non è del tutto vero, cerchiamo di essere generosi e diciamo che ottieni un output casuale di 19 caratteri; 0.1234567890123456789. I primi personaggi saranno sempre 0e ., quindi in realtà stiamo ottenendo solo 17 personaggi casuali. Questo ci lascia con 10^17 +1(per quanto possibile 0; vedi note sotto) o 100.000.000.000.000.001 uniques.


Quindi, quanti input casuali possiamo generare?

Ok, abbiamo calcolato il numero di risultati per un millisecondo di data e ora Math.random

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

Questo è un singolo dado da 6.000.000.000.000.000.060.000 di facce. Oppure, per rendere questo numero più umanamente digeribile, si tratta approssimativamente dello stesso numero di

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

Sembra abbastanza buono, vero? Bene, scopriamo ...

SHA1 produce un valore di 20 byte, con possibili esiti 256 ^ 20. Quindi non stiamo usando SHA1 per il suo pieno potenziale. Bene, quanto stiamo usando?

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

Un millisecondo di timestamp e Math.random utilizza solo il 4,11e-27 percento del potenziale a 160 bit di SHA1!

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

Gatti santi, amico! Guarda tutti quegli zeri. Quindi quanto è meglio crypto.randomBytes(20)? 243.583.606.221.817.150.598.111.409 volte meglio.


Note su +1e frequenza degli zeri

Se ti stai chiedendo del +1, è possibile Math.randomrestituire un 0che significa che c'è 1 altro risultato unico possibile che dobbiamo tenere in considerazione.

Sulla base della discussione svoltasi di seguito, ero curioso della frequenza che 0sarebbe venuta fuori. Ecco un piccolo script random_zero.js, ho fatto per ottenere alcuni dati

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

Quindi, l'ho eseguito in 4 thread (ho un processore a 4 core), aggiungendo l'output a un file

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

Quindi si scopre che a 0non è così difficile da ottenere. Dopo aver registrato 100 valori , la media era

1 in 3.164.854.823 randoms è uno 0

Freddo! Più ricerca sarebbe necessario sapere se quel numero è in pari con una distribuzione uniforme del v8 di Math.randomimplementazione


2
Si prega di consultare il mio aggiornamento; anche un millisecondo è molto tempo in terra javascript a velocità della luce! Su una nota più seria, le prime 10 cifre del numero rimangono invariate ogni secondo; questo è ciò che rende Dateterribile la produzione di buoni semi.
Grazie

1
Corretta. Anche se ho incluso solo quelli per dare il massimo contributo all'altra risposta per dimostrare che 20 byte casuali continuano a dominare in termini di entropia. Non credo Math.randomche produrrebbe mai un0.
Grazie

8
14 volte più voti rispetto alla risposta accettata ... ma chi conta? :)
zx81

2
@moka, i dadi sono la forma plurale di die . Sto usando la forma singolare.
Grazie

2
crypto.randomBytesè sicuramente la strada da percorrere ^^
Grazie il

28

Fallo anche nel browser!

EDIT: questo non si adattava davvero al flusso della mia risposta precedente. Lo lascio qui come seconda risposta per le persone che potrebbero essere interessate a farlo nel browser.

Puoi fare questo lato client nei browser moderni, se lo desideri

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

Requisiti del browser

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

3
Number.toString(radix)non garantisce sempre un valore di 2 cifre (es: (5).toString(16)= "5", non "05"). Questo non importa a meno che tu non dipenda dal tuo output finale per essere esattamente lenlungo i caratteri. In questo caso è possibile utilizzare return ('0'+n.toString(16)).slice(-2);all'interno della funzione della mappa.
The Brawny Man,

1
Ottimo codice, grazie. Volevo solo aggiungere: se lo utilizzerai per il valore di un idattributo, assicurati che l'ID inizi con una lettera: [A-Za-z].
GijsjanB,

Ottima risposta (e commenti) - ho davvero apprezzato il fatto che tu abbia incluso anche i requisiti del browser nella risposta!
Kevin

I requisiti del browser non sono corretti. Array.from () non è supportato in IE11.
Prefisso

1
È stato preso da una wiki al momento di questa risposta. Puoi modificare questa risposta se vuoi, ma a chi importa davvero di IE? Se stai cercando di supportarlo, devi comunque riempire con metà la metà di JavaScript ...
Grazie
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.