Nodejs進階:MD5入門介紹及crypto模組的應用

程式猿小卡_casper發表於2017-04-23

本文摘錄自《Nodejs學習筆記》,更多章節及更新,請訪問 github主頁地址。歡迎加群交流,群號 197339705

簡介

MD5(Message-Digest Algorithm)是電腦保安領域廣泛使用的雜湊函式(又稱雜湊演算法、摘要演算法),主要用來確保訊息的完整和一致性。常見的應用場景有密碼保護、下載檔案校驗等。

本文先對MD5的特點與應用進行簡要概述,接著重點介紹MD5在密碼保護場景下的應用,最後通過例子對MD5碰撞進行簡單介紹。

特點

  1. 運算速度快:對jquery.js求md5值,57254個字元,耗時1.907ms
  2. 輸出長度固定:輸入長度不固定,輸出長度固定(128位)。
  3. 運算不可逆:已知運算結果的情況下,無法通過通過逆運算得到原始字串。
  4. 高度離散:輸入的微小變化,可導致運算結果差異巨大。
  5. 弱碰撞性:不同輸入的雜湊值可能相同。

應用場景

  1. 檔案完整性校驗:比如從網上下載一個軟體,一般網站都會將軟體的md5值附在網頁上,使用者下載完軟體後,可對下載到本地的軟體進行md5運算,然後跟網站上的md5值進行對比,確保下載的軟體是完整的(或正確的)
  2. 密碼保護:將md5後的密碼儲存到資料庫,而不是儲存明文密碼,避免拖庫等事件發生後,明文密碼外洩。
  3. 防篡改:比如數字證照的防篡改,就用到了摘要演算法。(當然還要結合數字簽名等手段)

nodejs中md5運算的例子

在nodejs中,crypto模組封裝了一系列密碼學相關的功能,包括摘要運算。基礎例子如下,非常簡單:

var crypto = require('crypto');
var md5 = crypto.createHash('md5');

var result = md5.update('a').digest('hex');

// 輸出:0cc175b9c0f1b6a831c399e269772661
console.log(result);複製程式碼

例子:密碼保護

前面提到,將明文密碼儲存到資料庫是很不安全的,最不濟也要進行md5後進行儲存。比如使用者密碼是123456,md5執行後,得到輸出:e10adc3949ba59abbe56e057f20f883e

這樣至少有兩個好處:

  1. 防內部攻擊:網站主人也不知道使用者的明文密碼,避免網站主人拿著使用者明文密碼幹壞事。
  2. 防外部攻擊:如網站被黑客入侵,黑客也只能拿到md5後的密碼,而不是使用者的明文密碼。

示例程式碼如下:

var crypto = require('crypto');

function cryptPwd(password) {
    var md5 = crypto.createHash('md5');
    return md5.update(password).digest('hex');
}

var password = '123456';
var cryptedPassword = cryptPwd(password);

console.log(cryptedPassword);
// 輸出:e10adc3949ba59abbe56e057f20f883e複製程式碼

單純對密碼進行md5不安全

前面提到,通過對使用者密碼進行md5運算來提高安全性。但實際上,這樣的安全性是很差的,為什麼呢?

稍微修改下上面的例子,可能你就明白了。相同的明文密碼,md5值也是相同的。

var crypto = require('crypto');

function cryptPwd(password) {
    var md5 = crypto.createHash('md5');
    return md5.update(password).digest('hex');
}

var password = '123456';

console.log( cryptPwd(password) );
// 輸出:e10adc3949ba59abbe56e057f20f883e

console.log( cryptPwd(password) );
// 輸出:e10adc3949ba59abbe56e057f20f883e複製程式碼

也就是說,當攻擊者知道演算法是md5,且資料庫裡儲存的密碼值為e10adc3949ba59abbe56e057f20f883e時,理論上可以可以猜到,使用者的明文密碼就是123456

事實上,彩虹表就是這麼進行暴力破解的:事先將常見明文密碼的md5值運算好存起來,然後跟網站資料庫裡儲存的密碼進行匹配,就能夠快速找到使用者的明文密碼。(這裡不探究具體細節)

那麼,有什麼辦法可以進一步提升安全性呢?答案是:密碼加鹽。

密碼加鹽

“加鹽”這個詞看上去很玄乎,其實原理很簡單,就是在密碼特定位置插入特定字串後,再對修改後的字串進行md5運算。

例子如下。同樣的密碼,當“鹽”值不一樣時,md5值的差異非常大。通過密碼加鹽,可以防止最初級的暴力破解,如果攻擊者事先不知道”鹽“值,破解的難度就會非常大。

var crypto = require('crypto');

function cryptPwd(password, salt) {
    // 密碼“加鹽”
    var saltPassword = password + ':' + salt;
    console.log('原始密碼:%s', password);
    console.log('加鹽後的密碼:%s', saltPassword);

    // 加鹽密碼的md5值
    var md5 = crypto.createHash('md5');
    var result = md5.update(saltPassword).digest('hex');
    console.log('加鹽密碼的md5值:%s', result);
}

cryptPwd('123456', 'abc');
// 輸出:
// 原始密碼:123456
// 加鹽後的密碼:123456:abc
// 加鹽密碼的md5值:51011af1892f59e74baf61f3d4389092

cryptPwd('123456', 'bcd');
// 輸出:
// 原始密碼:123456
// 加鹽後的密碼:123456:bcd
// 加鹽密碼的md5值:55a95bcb6bfbaef6906dbbd264ab4531複製程式碼

密碼加鹽:隨機鹽值

通過密碼加鹽,密碼的安全性已經提高了不少。但其實上面的例子存在不少問題。

假設字串拼接演算法、鹽值已外洩,上面的程式碼至少存在下面問題:

  1. 短鹽值:需要窮舉的可能性較少,容易暴力破解,一般採用長鹽值來解決。
  2. 鹽值固定:類似的,攻擊者只需要把常用密碼+鹽值的hash值表算出來,就完事大吉了。

短鹽值自不必說,應該避免。對於為什麼不應該使用固定鹽值,這裡需要多解釋一下。很多時候,我們的鹽值是硬編碼到我們的程式碼裡的(比如配置檔案),一旦壞人通過某種手段獲知了鹽值,那麼,只需要針對這串固定的鹽值進行暴力窮舉就行了。

比如上面的程式碼,當你知道鹽值是abc時,立刻就能猜到51011af1892f59e74baf61f3d4389092對應的明文密碼是123456

那麼,該怎麼優化呢?答案是:隨機鹽值。

示例程式碼如下。可以看到,密碼同樣是123456,由於採用了隨機鹽值,前後運算得出的結果是不同的。這樣帶來的好處是,多個使用者,同樣的密碼,攻擊者需要進行多次運算才能夠完全破解。同樣是純數字3位短鹽值,隨機鹽值破解所需的運算量,是固定鹽值的1000倍。

var crypto = require('crypto');

function getRandomSalt(){
    return Math.random().toString().slice(2, 5);
}

function cryptPwd(password, salt) {
    // 密碼“加鹽”
    var saltPassword = password + ':' + salt;
    console.log('原始密碼:%s', password);
    console.log('加鹽後的密碼:%s', saltPassword);

    // 加鹽密碼的md5值
    var md5 = crypto.createHash('md5');
    var result = md5.update(saltPassword).digest('hex');
    console.log('加鹽密碼的md5值:%s', result);
}

var password = '123456';

cryptPwd('123456', getRandomSalt());
// 輸出:
// 原始密碼:123456
// 加鹽後的密碼:123456:498
// 加鹽密碼的md5值:af3b7d32cc2a254a6bf1ebdcfd700115

cryptPwd('123456', getRandomSalt());
// 輸出:
// 原始密碼:123456
// 加鹽後的密碼:123456:287
// 加鹽密碼的md5值:65d7dd044c2db64c5e658d947578d759複製程式碼

MD5碰撞

簡單的說,就是兩段不同的字串,經過MD5運算後,得出相同的結果。

網上有不少例子,這裡就不贅述,直接上例子,參考(這裡)[www.mscs.dal.ca/~selinger/m…]

function getHashResult(hexString){

    // 轉成16進位制,比如 0x4d 0xc9 ...
    hexString = hexString.replace(/(\w{2,2})/g, '0x$1 ').trim();

    // 轉成16進位制陣列,如 [0x4d, 0xc9, ...]
    var arr = hexString.split(' ');

    // 轉成對應的buffer,如:<Buffer 4d c9 ...>
    var buff = Buffer.from(arr);

    var crypto = require('crypto');
    var hash = crypto.createHash('md5');

    // 計算md5值
    var result = hash.update(buff).digest('hex');

    return result;  
}

var str1 = 'd131dd02c5e6eec4693d9a0698aff95c2fcab58712467eab4004583eb8fb7f8955ad340609f4b30283e488832571415a085125e8f7cdc99fd91dbdf280373c5bd8823e3156348f5bae6dacd436c919c6dd53e2b487da03fd02396306d248cda0e99f33420f577ee8ce54b67080a80d1ec69821bcb6a8839396f9652b6ff72a70';
var str2 = 'd131dd02c5e6eec4693d9a0698aff95c2fcab50712467eab4004583eb8fb7f8955ad340609f4b30283e4888325f1415a085125e8f7cdc99fd91dbd7280373c5bd8823e3156348f5bae6dacd436c919c6dd53e23487da03fd02396306d248cda0e99f33420f577ee8ce54b67080280d1ec69821bcb6a8839396f965ab6ff72a70';

var result1 = getHashResult(str1);
var result2 = getHashResult(str2);

if(result1 === result2) {
    console.log(`Got the same md5 result: ${result1}`);
}else{
    console.log(`Not the same md5 result`);
}複製程式碼

寫在後面

如有錯漏,敬請指出,歡迎多交流 :)

相關連結

MD5碰撞的一些例子
www.jianshu.com/p/c9089fd5b…

MD5 Collision Demo
www.mscs.dal.ca/~selinger/m…

Free Password Hash Cracker
crackstation.net/

相關文章