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^20
o 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 shasum
nostri 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*1000
o 60000
unici.
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 .length
di 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 0
e .
, 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 +1
e frequenza degli zeri
Se ti stai chiedendo del +1
, è possibile Math.random
restituire un 0
che 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 0
sarebbe 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 0
non è 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.random
implementazione