Comment générer un hachage SHA1 aléatoire à utiliser comme ID dans node.js?

137

J'utilise cette ligne pour générer un identifiant sha1 pour node.js:

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

Le problème est qu'il renvoie le même identifiant à chaque fois.

Est-il possible de le faire générer un identifiant aléatoire à chaque fois afin que je puisse l'utiliser comme identifiant de document de base de données?

Ajsie
la source
2
N'utilisez pas sha1. Il n'est plus considéré comme sûr (résistant aux collisions). C'est pourquoi la réponse de naomik est meilleure.
Niels Abildgaard

Réponses:

60

Jetez un oeil ici: Comment utiliser la crypto node.js pour créer un hachage HMAC-SHA1? Je créerais un hachage de l'horodatage actuel + un nombre aléatoire pour assurer l'unicité du hachage:

var current_date = (new Date()).valueOf().toString();
var random = Math.random().toString();
crypto.createHash('sha1').update(current_date + random).digest('hex');
Gabi Purcaru
la source
44
Pour une bien meilleure approche, voir la réponse de @ naomik ci-dessous.
Gabi Purcaru
2
C'était aussi une excellente réponse Gabi, et juste un tout petit peu plus rapide, environ 15%. Excellent travail à la fois! En fait, j'aime voir une Date () dans le sel, cela donne au développeur la confiance que ce sera une valeur unique dans toutes les situations de calcul parallèle, sauf les plus insensées. Je sais que ses octets stupides et aléatoires (20) seront uniques, mais c'est juste une confiance que nous pouvons avoir car nous ne sommes peut-être pas familiers avec les internes de la génération aléatoire d'une autre bibliothèque.
Dmitri R117
637

243,583,606,221,817,150,598,111,409x plus d'entropie

Je recommanderais d'utiliser crypto.randomBytes . Ce n'est pas le cas sha1, mais pour des raisons d'identification, c'est plus rapide et tout aussi "aléatoire".

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

La chaîne résultante sera deux fois plus longue que les octets aléatoires que vous générez; chaque octet codé en hexadécimal est de 2 caractères. 20 octets seront 40 caractères hexadécimaux.

En utilisant 20 octets, nous avons 256^20ou 1,461,501,637,330,902,918,203,684,832,716,283,019,655,932,542,976 valeurs de sortie uniques. Ceci est identique aux sorties possibles de 160 bits (20 octets) de SHA1.

Sachant cela, shasumnos octets aléatoires ne sont pas vraiment significatifs pour nous . C'est comme lancer un dé deux fois mais n'accepter que le deuxième lancer; quoi qu'il arrive, vous avez 6 résultats possibles à chaque jet, donc le premier jet est suffisant.


Pourquoi est-ce mieux?

Pour comprendre pourquoi c'est mieux, nous devons d'abord comprendre comment fonctionnent les fonctions de hachage. Les fonctions de hachage (y compris SHA1) généreront toujours la même sortie si la même entrée est donnée.

Disons que nous voulons générer des identifiants, mais notre entrée aléatoire est générée par un tirage au sort. Nous avons "heads"ou"tails"

% echo -n "heads" | shasum
c25dda249cdece9d908cc33adcd16aa05e20290f  -

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

Si "heads"revient, la sortie SHA1 sera la même que la première fois

% echo -n "heads" | shasum
c25dda249cdece9d908cc33adcd16aa05e20290f  -

Ok, donc un tirage au sort n'est pas un bon générateur d'ID aléatoires car nous n'avons que 2 sorties possibles.

Si nous utilisons une matrice standard à 6 faces, nous avons 6 entrées possibles. Devinez combien de sorties SHA1 possibles? 6!

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

Il est facile de se leurrer en pensant simplement parce que la sortie de notre fonction semble très aléatoire, qu'elle est très aléatoire.

Nous sommes tous les deux d'accord qu'un tirage au sort ou un dé à 6 faces ferait un mauvais générateur d'identifiant aléatoire, car nos résultats SHA1 possibles (la valeur que nous utilisons pour l'identifiant) sont très peu nombreux. Mais que faire si nous utilisons quelque chose qui a beaucoup plus de sorties? Comme un horodatage avec des millisecondes? Ou JavaScript Math.random? Ou même une combinaison de ces deux?!

Calculons le nombre d'identifiants uniques que nous obtiendrions ...


L'unicité d'un horodatage en millisecondes

Lors de l'utilisation (new Date()).valueOf().toString(), vous obtenez un numéro à 13 caractères (par exemple, 1375369309741). Cependant, comme il s'agit d'un nombre de mise à jour séquentielle (une fois par milliseconde), les sorties sont presque toujours les mêmes. Nous allons jeter un coup d'oeil

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

Pour être juste, à des fins de comparaison, dans une minute donnée (un temps d'exécution d'opération généreux), vous aurez 60*1000ou 60000uniques.


Le caractère unique de Math.random

Maintenant, lors de l'utilisation Math.random, en raison de la façon dont JavaScript représente les nombres à virgule flottante 64 bits, vous obtiendrez un nombre d'une longueur comprise entre 13 et 24 caractères. Un résultat plus long signifie plus de chiffres, ce qui signifie plus d'entropie. Tout d'abord, nous devons savoir quelle est la longueur la plus probable.

Le script ci-dessous déterminera la longueur la plus probable. Nous faisons cela en générant 1 million de nombres aléatoires et en incrémentant un compteur basé sur le .lengthde chaque nombre.

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

En divisant chaque compteur par 1 million, nous obtenons la probabilité de la longueur du nombre retourné 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

Donc, même si ce n'est pas tout à fait vrai, soyons généreux et disons que vous obtenez une sortie aléatoire de 19 caractères; 0.1234567890123456789. Les premiers personnages seront toujours 0et ., donc en réalité, nous n'obtenons que 17 caractères aléatoires. Cela nous laisse avec 10^17 +1(pour possible 0; voir les notes ci-dessous) ou 100 000 000 000 000 001 uniques.


Alors, combien d'entrées aléatoires pouvons-nous générer?

Ok, nous avons calculé le nombre de résultats pour un horodatage en millisecondes et Math.random

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

C'est un seul dé à 6 000 000 000 000 000 060 000 faces. Ou, pour rendre ce nombre plus digestible humainement, c'est à peu près le même nombre 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

Ça sonne plutôt bien, non? Eh bien, découvrons ...

SHA1 produit une valeur de 20 octets, avec un possible 256 ^ 20 résultats. Nous n'utilisons donc vraiment pas SHA1 à son plein potentiel. Eh bien, combien utilisons-nous?

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

Un horodatage à la milliseconde et Math.random n'utilise que 4,11e-27% du potentiel 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%

Saints chats, mec! Regardez tous ces zéros. Alors, à quel point c'est mieux crypto.randomBytes(20)? 243,583,606,221,817,150,598,111,409 fois mieux.


Remarques sur la +1fréquence et la fréquence des zéros

Si vous vous interrogez sur le +1, il est possible Math.randomde renvoyer un, 0ce qui signifie qu'il y a 1 autre résultat unique possible que nous devons prendre en compte.

Sur la base de la discussion qui s'est déroulée ci-dessous, j'étais curieux de savoir à quelle fréquence un 0allait arriver. Voici un petit script random_zero.js, j'ai fait pour obtenir des données

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

Ensuite, je l'ai exécuté en 4 threads (j'ai un processeur à 4 cœurs), en ajoutant la sortie à un fichier

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

Il s'avère donc que ce 0n'est pas si difficile à obtenir. Après l' enregistrement de 100 valeurs , la moyenne était

1 randoms sur 3,164,854,823 est un 0

Cool! Des recherches supplémentaires seraient nécessaires pour savoir si ce nombre correspond à une distribution uniforme de la Math.randommise en œuvre de la v8

Je vous remercie
la source
2
Veuillez consulter ma mise à jour; même une milliseconde est une longue période en javascript land lightspeed! Sur une note plus sérieuse, les 10 premiers chiffres du numéro restent les mêmes toutes les secondes; c'est ce qui rend Datemal la production de bonnes graines.
Merci
1
Correct. Bien que je n'ai vraiment inclus que ceux pour donner la plus grande contribution à l'autre réponse pour démontrer que 20 octets aléatoires dominent toujours juste en termes d'entropie. Je ne pense pas que Math.randomcela produirait jamais un0.
Merci
8
14x plus de votes positifs que la réponse acceptée ... mais qui compte? :)
zx81
2
@moka, dice est la forme plurielle de die . J'utilise la forme singulière.
Merci
2
crypto.randomBytesest définitivement la voie à suivre ^^
Merci
28

Faites-le aussi dans le navigateur!

EDIT: cela ne correspondait pas vraiment au flux de ma réponse précédente. Je le laisse ici comme deuxième réponse pour les personnes qui pourraient chercher à le faire dans le navigateur.

Vous pouvez faire ce côté client dans les navigateurs modernes, si vous le souhaitez

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

Exigences du navigateur

Browser    Minimum Version
--------------------------
Chrome     11.0
Firefox    21.0
IE         11.0
Opera      15.0
Safari     5.1
Je vous remercie
la source
3
Number.toString(radix)ne garantit pas toujours une valeur à 2 chiffres (ex: (5).toString(16)= "5", pas "05"). Cela n'a pas d'importance, sauf si vous comptez sur votre sortie finale pour contenir exactement des lencaractères. Dans ce cas, vous pouvez utiliser à l' return ('0'+n.toString(16)).slice(-2);intérieur de votre fonction de carte.
The Brawny Man
1
Excellent code, merci. Je voulais juste ajouter: si vous allez l'utiliser pour la valeur d'un idattribut, assurez-vous que l'ID commence par une lettre: [A-Za-z].
GijsjanB
Excellente réponse (et commentaires) - vraiment apprécié que vous ayez également inclus les exigences du navigateur dans la réponse!
kevlarr
Les exigences du navigateur sont incorrectes. Array.from () n'est pas pris en charge dans IE11.
Préfixe du
1
Il a été tiré d'un wiki au moment de cette réponse. Vous pouvez modifier cette réponse si vous le souhaitez, mais qui se soucie vraiment d'IE? Si vous essayez de le supporter, vous devez quand même remplir la moitié de JavaScript ...
Merci le