วิธีสร้างแฮช SHA1 แบบสุ่มเพื่อใช้เป็น ID ใน node.js?


137

ฉันใช้บรรทัดนี้เพื่อสร้างรหัส sha1 สำหรับ node.js:

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

ปัญหาคือมันคืนรหัสเดียวกันทุกครั้ง

เป็นไปได้หรือไม่ที่จะสร้างรหัสสุ่มในแต่ละครั้งเพื่อให้ฉันสามารถใช้เป็นรหัสเอกสารฐานข้อมูลได้


2
อย่าใช้ sha1 จะไม่ถือว่าปลอดภัยอีกต่อไป (ทนต่อการชน) นี่คือเหตุผลที่คำตอบของ naomikดีกว่า
Niels Abildgaard

คำตอบ:


60

ดูที่นี่: ฉันจะใช้ node.js Crypto เพื่อสร้างแฮช HMAC-SHA1 ได้อย่างไร ฉันต้องการสร้างแฮชของการประทับเวลาปัจจุบัน + หมายเลขสุ่มเพื่อให้แน่ใจว่ามีแฮชที่ไม่ซ้ำใคร:

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

44
สำหรับแนวทางที่ดีกว่านี้ดูคำตอบของ @ naomik ด้านล่าง
Gabi Purcaru

2
นี่เป็นคำตอบที่ยอดเยี่ยมสำหรับ Gabi และเร็วขึ้นเล็กน้อยประมาณ 15% ยอดเยี่ยมทั้งงาน! จริง ๆ แล้วฉันชอบดู Date () ในเกลือมันให้ความมั่นใจแก่ผู้พัฒนาได้ง่ายว่านี่จะเป็นค่าที่ไม่ซ้ำกันในทุกสถานการณ์ แต่ในสถานการณ์การคำนวณแบบขนานที่บ้าคลั่งที่สุด ฉันรู้ว่ามันไร้สาระและแบบสุ่ม (20) จะไม่ซ้ำกัน แต่มันเป็นความมั่นใจที่เรามีเพราะเราอาจไม่คุ้นเคยกับการสร้างแบบสุ่มของห้องสมุดอื่น
Dmitri R117

637

243,583,606,221,817,150,598,111,409x มากกว่าเอนโทรปี

ผมขอแนะนำให้ใช้crypto.randomBytes มันไม่ใช่sha1แต่เพื่อวัตถุประสงค์ id มันเร็วกว่าและเหมือนกับ "สุ่ม"

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

สตริงผลลัพธ์จะยาวเป็นสองเท่าของไบต์สุ่มที่คุณสร้าง แต่ละไบต์ที่เข้ารหัสถึงฐานสิบหกคือ 2 อักขระ 20 ไบต์จะเป็นอักขระฐานสิบหก 40 ตัว

ใช้ 20 ไบต์เรามี256^20หรือ1,461,501,637,330,902,918,203,684,832,716,2816,283,016,283,019,655,932,542,976ค่าการส่งออกที่ไม่ซ้ำกัน สิ่งนี้เหมือนกับเอาต์พุตที่เป็นไปได้ 160- บิต (20- ไบต์) ของ SHA1

เมื่อรู้สิ่งนี้มันไม่มีความหมายสำหรับเราที่จะshasumสุ่มไบต์ของเรา มันเหมือนกับการกลิ้งตายสองครั้ง แต่ยอมรับการหมุนรอบที่สองเท่านั้น ไม่ว่าจะเกิดอะไรขึ้นคุณมี 6 ผลลัพธ์ที่เป็นไปได้ในแต่ละม้วนดังนั้นม้วนแรกก็เพียงพอ


ทำไมสิ่งนี้ถึงดีกว่า

เพื่อให้เข้าใจว่าทำไมสิ่งนี้ถึงดีกว่าก่อนอื่นเราต้องเข้าใจวิธีการทำงานของ hashing ฟังก์ชันการแฮช (รวมถึง SHA1) จะสร้างผลลัพธ์เดียวกันเสมอหากมีการป้อนข้อมูลเดียวกัน

สมมติว่าเราต้องการสร้าง ID แต่การป้อนข้อมูลแบบสุ่มของเราสร้างขึ้นโดยการโยนเหรียญ เรามี"heads"หรือ"tails"

% echo -n "heads" | shasum
c25dda249cdece9d908cc33adcd16aa05e20290f  -

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

หาก"heads"ขึ้นมาอีกครั้งเอาต์พุต SHA1 จะเหมือนเดิมเป็นครั้งแรก

% echo -n "heads" | shasum
c25dda249cdece9d908cc33adcd16aa05e20290f  -

ตกลงดังนั้นการโยนเหรียญไม่ใช่เครื่องสร้างรหัสสุ่มที่ยอดเยี่ยมเพราะเรามีเพียง 2 เอาต์พุตที่เป็นไปได้

หากเราใช้แม่พิมพ์แบบ 6 ด้านมาตรฐานเรามี 6 อินพุตที่เป็นไปได้ ลองเดาว่ามีความเป็นไปได้ที่ SHA1 ส่งออกมากี่ตัว? 6!

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

มันง่ายที่จะหลอกตัวเองด้วยการคิดเพียงเพราะการส่งออกของฟังก์ชั่นของเรามีลักษณะสุ่มมากว่ามันเป็นแบบสุ่มมาก

เราทั้งสองตกลงกันว่าการโยนเหรียญหรือตัวตายแบบ 6 ด้านจะทำให้เครื่องสร้างรหัสสุ่มไม่ถูกต้องเนื่องจากผลลัพธ์ SHA1 ที่เป็นไปได้ของเรา (ค่าที่เราใช้สำหรับ ID) มีน้อยมาก แต่ถ้าเราใช้บางอย่างที่มีเอาต์พุตมากกว่านั้นล่ะ ชอบเวลาประทับที่มีมิลลิวินาทีหรือไม่ หรือ JavaScript เป็นMath.random? หรือแม้แต่การรวมกันของสองคนนี้!

ลองคำนวณจำนวนไอดีที่เราจะได้รับ ...


ความเป็นเอกลักษณ์ของเวลาประทับที่มีมิลลิวินาที

เมื่อใช้(new Date()).valueOf().toString()งานคุณจะได้ตัวเลข 13 ตัว (เช่น1375369309741) อย่างไรก็ตามเนื่องจากนี่เป็นหมายเลขการปรับปรุงตามลำดับ (หนึ่งครั้งต่อมิลลิวินาที) เอาต์พุตจึงเกือบจะเหมือนกันทุกครั้ง ลองมาดูกัน

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

เพื่อความยุติธรรมสำหรับการเปรียบเทียบในนาทีที่กำหนด (เวลาดำเนินการเผื่อแผ่) คุณจะมี60*1000หรือ60000ไม่เหมือนใคร


ความเป็นเอกลักษณ์ของ Math.random

ตอนนี้เมื่อใช้Math.randomงานเนื่องจากวิธีที่ JavaScript แสดงตัวเลขจุดลอยตัว 64 บิตคุณจะได้รับหมายเลขที่มีความยาวทุกที่ระหว่าง 13 ถึง 24 อักขระ ผลลัพธ์ที่ยาวขึ้นหมายถึงตัวเลขที่มากกว่าซึ่งหมายถึงเอนโทรปีที่มากขึ้น ก่อนอื่นเราต้องหาว่าอันไหนที่มีความยาวน่าจะเป็นที่สุด

สคริปต์ด้านล่างจะกำหนดความยาวที่เป็นไปได้มากที่สุด เราทำสิ่งนี้โดยการสร้างตัวเลขสุ่ม 1 ล้านตัวและเพิ่มตัวนับตาม.lengthจำนวนของแต่ละหมายเลข

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

โดยการหารแต่ละเคาน์เตอร์โดย 1 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

ดังนั้นแม้ว่ามันจะไม่เป็นความจริง แต่อย่างใดขอให้เผื่อแผ่และพูดว่าคุณได้รับผลลัพธ์แบบสุ่มขนาด 19 อักขระ 0.1234567890123456789. ตัวละครตัวแรกจะเป็นเสมอ0และ.ดังนั้นจริงๆแล้วเราได้รับเพียง 17 ตัวอักษรแบบสุ่ม ใบนี้เราด้วย10^17 +1(สำหรับเป็นไปได้0; ดูหมายเหตุด้านล่าง) หรือ100.000.000.000.000.001ซ้ำ


ดังนั้นเราสามารถสร้างอินพุตสุ่มจำนวนเท่าใด

ตกลงเราคำนวณจำนวนผลลัพธ์สำหรับการประทับเวลามิลลิวินาทีและ Math.random

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

6,000,000,000,000,000,060,000 ชิ้นเดียว หรือเพื่อให้ตัวเลขนี้มากขึ้นอย่างมนุษย์ปุถุชนย่อยนี้เป็นประมาณจำนวนเดียวกับ

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

ฟังดูดีใช่มั้ย มาดูกันดีกว่า ...

SHA1สร้างมูลค่า 20 ไบต์พร้อมผลลัพธ์ที่เป็นไปได้ 256 ^ 20 ดังนั้นเราจึงไม่ได้ใช้ SHA1 กับศักยภาพสูงสุด เราใช้มากแค่ไหน

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

การประทับเวลามิลลิวินาทีและ Math.random ใช้เพียง 4.11e-27 เปอร์เซ็นต์ของศักยภาพ 160 บิตของ SHA1!

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

แมวศักดิ์สิทธิ์ผู้ชาย! ดูเลขศูนย์ทั้งหมด ดังนั้นวิธีที่ดีกว่าคือcrypto.randomBytes(20)อะไร? 243,583,606,221,817,150,598,111,409เท่าดีกว่า


หมายเหตุเกี่ยวกับ +1และความถี่ของเลขศูนย์

หากคุณสงสัยเกี่ยวกับสิ่งที่+1เป็นไปได้Math.randomจะส่งคืน0ซึ่งหมายความว่ามีผลลัพธ์ที่เป็นเอกลักษณ์ที่เป็นไปได้อีก 1 รายการที่เราต้องพิจารณา

จากการสนทนาที่เกิดขึ้นด้านล่างฉันสงสัยเกี่ยวกับความถี่ที่0จะเกิดขึ้น นี่เป็นสคริปต์เล็กน้อยrandom_zero.jsฉันได้รับข้อมูลบางอย่าง

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

จากนั้นฉันรันใน 4 เธรด (ฉันมีโปรเซสเซอร์ 4 คอร์) ต่อท้ายเอาต์พุตไปยังไฟล์

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

ดังนั้นมันกลับกลาย0เป็นว่าไม่ใช่เรื่องยากที่จะได้รับ หลังจากบันทึกค่าได้100 ค่าโดยเฉลี่ยแล้ว

1 ใน3,164,854,823 randoms คือ 0

เย็น! จะต้องมีการวิจัยเพิ่มเติมเพื่อทราบว่าหมายเลขดังกล่าวมีการกระจายMath.randomการดำเนินงานของ v8 อย่างสม่ำเสมอ


2
โปรดดูการปรับปรุงของฉัน; แม้มิลลิวินาทีเป็นเวลานานในดินแดนจาวาสคริปต์ lightspeed! เมื่อทราบอย่างจริงจังมากขึ้นตัวเลข 10 หลักแรกของตัวเลขจะคงเดิมทุกวินาที นี่คือสิ่งที่ทำให้Dateน่ากลัวในการผลิตเมล็ดพันธุ์ที่ดี
ขอบคุณ

1
แก้ไข. แม้ว่าฉันจะรวมเฉพาะผู้ที่ให้การสนับสนุนมากที่สุดกับคำตอบอื่น ๆ เพื่อแสดงให้เห็นว่า 20 ไบต์แบบสุ่มยังคงครอบงำในแง่ของเอนโทรปี ฉันไม่คิดว่าMath.randomจะผลิต a0.
ขอขอบคุณคุณ

8
upvotes 14x มากกว่าคำตอบที่ยอมรับ ... แต่ใครจะนับ? :)
zx81

2
@moka, ลูกเต๋าเป็นรูปแบบพหูพจน์ของตาย ฉันใช้รูปแบบเอกพจน์
ขอบคุณ

2
crypto.randomBytesเป็นวิธีที่จะไปแน่นอน ^^
ขอบคุณ

28

ทำในเบราว์เซอร์ด้วย!

แก้ไข: นี่ไม่เหมาะกับการไหลของคำตอบก่อนหน้าของฉัน ฉันทิ้งไว้ที่นี่เป็นคำตอบที่สองสำหรับผู้ที่อาจมองหาวิธีนี้ในเบราว์เซอร์

คุณสามารถทำด้านลูกค้านี้ในเบราว์เซอร์ที่ทันสมัยถ้าคุณต้องการ

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

ข้อกำหนดของเบราว์เซอร์

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

3
Number.toString(radix)ไม่รับประกันค่า 2 หลักเสมอ (เช่น: (5).toString(16)= "5" ไม่ใช่ "05") ไม่สำคัญว่าคุณจะขึ้นอยู่กับผลลัพธ์สุดท้ายของคุณว่ามีความlenยาวอักขระอย่างแน่นอน ในกรณีนี้คุณสามารถใช้return ('0'+n.toString(16)).slice(-2);ภายในฟังก์ชั่นแผนที่ของคุณ
ชายร่างผอม

1
รหัสที่ดีขอบคุณ แค่อยากจะเพิ่ม: ถ้าคุณจะใช้มันเพื่อคุณค่าของidแอตทริบิวต์ให้แน่ใจว่า ID เริ่มต้นด้วยตัวอักษร: [A-Za-z]
GijsjanB

คำตอบที่ดี (และความคิดเห็น) - ชื่นชมจริง ๆ ว่าคุณได้รวมข้อกำหนดของเบราว์เซอร์ไว้ในคำตอบด้วย!
kevlarr

ข้อกำหนดของเบราว์เซอร์ไม่ถูกต้อง Array.from () ไม่รองรับใน IE11
คำนำหน้า

1
มันถูกพรากไปจากวิกิตอนที่ตอบคำถามนี้ คุณสามารถแก้ไขคำตอบนี้ได้ถ้าต้องการ แต่ใครสนใจ IE จริงๆ หากคุณพยายามสนับสนุนคุณต้อง polyfill ครึ่งหนึ่งของ JavaScript ต่อไป ...
ขอบคุณ
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.