消息摘要算法是密碼學(xué)算法中非常重要的一個分支,它通過對所有數(shù)據(jù)提取指紋信息以實現(xiàn)數(shù)據(jù)簽名、數(shù)據(jù)完整性校驗等功能,由于其不可逆性,有時候會被用做敏感信息的加密。消息摘要算法也被稱為哈希(Hash)算法或散列算法。
任何消息經(jīng)過散列函數(shù)處理后,都會獲得唯一的散列值,這一過程稱為 “消息摘要”,其散列值稱為 “數(shù)字指紋”,其算法自然就是 “消息摘要算法”了。換句話說,如果其數(shù)字指紋一致,就說明其消息是一致的。
(圖片來源 —— https://zh.wikipedia.org/wiki/散列函數(shù))
消息摘要算法的主要特征是加密過程不需要密鑰,并且經(jīng)過加密的數(shù)據(jù)無法被解密,目前可以解密逆向的只有 CRC32 算法,只有輸入相同的明文數(shù)據(jù)經(jīng)過相同的消息摘要算法才能得到相同的密文。消息摘要算法不存在密鑰的管理與分發(fā)問題,適合于分布式網(wǎng)絡(luò)上使用。消息摘要算法主要應(yīng)用在 “數(shù)字簽名” 領(lǐng)域,作為對明文的摘要算法。著名的摘要算法有 RSA 公司的 MD5 算法和 SHA-1 算法及其大量的變體。
1.1 消息摘要算法的特點
- 無論輸入的消息有多長,計算出來的消息摘要的長度總是固定的。 例如應(yīng)用 MD5 算法摘要的消息有 128 個比特位,用 SHA-1 算法摘要的消息最終有 160 個比特位的輸出,SHA-1的變體可以產(chǎn)生 192 個比特位和 256 個比特位的消息摘要。一般認(rèn)為,摘要的最終輸出越長,該摘要算法就越安全。
- 消息摘要看起來是 “隨機的”。 這些比特看上去是胡亂的雜湊在一起的,可以用大量的輸入來檢驗其輸出是否相同,一般,不同的輸入會有不同的輸出,而且輸出的摘要消息可以通過隨機性檢驗。 一般地,只要輸入的消息不同,對其進行摘要以后產(chǎn)生的摘要消息也必不相同;但相同的輸入必會產(chǎn)生相同的輸出。
- 消息摘要函數(shù)是單向函數(shù),即只能進行正向的信息摘要,而無法從摘要中恢復(fù)出任何的消息,甚至根本就找不到任何與原信息相關(guān)的信息。
- 好的摘要算法,沒有人能從中找到 “碰撞” 或者說極度難找到,雖然 “碰撞” 是肯定存在的(碰撞即不同的內(nèi)容產(chǎn)生相同的摘要)。
二、什么是 MD5 算法
MD5(Message Digest Algorithm 5,消息摘要算法版本5),它由 MD2、MD3、MD4 發(fā)展而來,由 Ron Rivest(RSA 公司)在 1992 年提出,目前被廣泛應(yīng)用于數(shù)據(jù)完整性校驗、數(shù)據(jù)(消息)摘要、數(shù)據(jù)簽名等。 MD2、MD4、MD5 都產(chǎn)生 16 字節(jié)(128 位)的校驗值,一般用 32 位十六進制數(shù)表示。MD2 的算法較慢但相對安全,MD4 速度很快,但安全性下降,MD5 比 MD4 更安全、速度更快。
隨著計算機技術(shù)的發(fā)展和計算水平的不斷提高,MD5 算法暴露出來的漏洞也越來越多。1996 年后被證實存在弱點,可以被加以破解,對于需要高度安全性的數(shù)據(jù),專家一般建議改用其他算法,如 SHA-2。2004 年,證實 MD5 算法無法防止碰撞(collision),因此不適用于安全性認(rèn)證,如 SSL 公開密鑰認(rèn)證或是數(shù)字簽名等用途。
2.1 MD5 特點
- 穩(wěn)定、運算速度快。
- 壓縮性:輸入任意長度的數(shù)據(jù),輸出長度固定(128 比特位)。
- 運算不可逆:已知運算結(jié)果的情況下,無法通過通過逆運算得到原始字符串。
- 高度離散:輸入的微小變化,可導(dǎo)致運算結(jié)果差異巨大。
2.2 MD5 散列
128 位的 MD5 散列在大多數(shù)情況下會被表示為 32 位十六進制數(shù)字。以下是一個 43 位長的僅 ASCII 字母列的MD5 散列:
MD5("The quick brown fox jumps over the lazy dog")= 9e107d9d372bb6826bd81d3542a419d6
即使在原文中作一個小變化(比如把 dog 改為 cog,只改變一個字符)其散列也會發(fā)生巨大的變化:
MD5("The quick brown fox jumps over the lazy cog")= 1055d3e698d289f2af8663725127bd4b
接著我們再來舉幾個 MD5 散列的例子:
MD5("") -> d41d8cd98f00b204e9800998ecf8427e MD5("semlinker") -> 688881f1c8aa6ffd3fcec471e0391e4d MD5("kakuqo") -> e18c3c4dd05aef020946e6afbf9e04ef
三、MD5 算法的用途
3.1 防止被篡改
3.1.1 文件分發(fā)防篡改
在互聯(lián)網(wǎng)上分發(fā)軟件安裝包時,出于安全性考慮,為了防止軟件被篡改,比如在軟件安裝程序中添加木馬程序。軟件開發(fā)者通常會使用消息摘要算法,比如 MD5 算法產(chǎn)生一個與文件匹配的數(shù)字指紋,這樣接收者在接收到文件后,就可以利用一些現(xiàn)成的工具來檢查文件完整性。
(圖片來源 —— https://en.wikipedia.org/wiki/MD5)
這里我們來舉一個實際的例子,下圖是 MySQL Community Server 8.0.19 版本的下載頁,該下載頁通過 MD5 算法分別計算出不同軟件包的數(shù)字指紋,具體如下圖所示:
(圖片來源 —— https://dev.mysql.com/downloads/mysql/)
當(dāng)用戶從官網(wǎng)上下載到對應(yīng)的安裝包之后,可以利用一些 MD5 校驗工具對已下載的文件進行校驗,然后比對最終的 MD5 數(shù)字指紋,若結(jié)果與官網(wǎng)公布的數(shù)字指紋一致,則表示該安裝包未經(jīng)過任何修改是安全的,基本可以放心安裝。
3.1.2 消息傳輸防篡改
假設(shè)在網(wǎng)絡(luò)上你需要發(fā)送電子文檔給你的朋友,在文件發(fā)送前,先對文檔的內(nèi)容進行 MD5 運算,得出該電子文檔的 “數(shù)字指紋”,并把該 “數(shù)字指紋” 隨電子文檔一同發(fā)送給對方。當(dāng)對方接收到電子文檔之后,也使用 MD5 算法對文檔的內(nèi)容進行哈希運算,在運算完成后也會得到一個對應(yīng) “數(shù)字指紋”,當(dāng)該指紋與你所發(fā)送文檔的 “數(shù)字指紋” 一致時,表示文檔在傳輸過程中未被篡改。
3.2 信息保密
在互聯(lián)網(wǎng)初期很多網(wǎng)站在數(shù)據(jù)庫中以明文的形式存儲用戶的密碼,這存在很大的安全隱患,比如數(shù)據(jù)庫被黑客入侵,從而導(dǎo)致網(wǎng)站用戶信息的泄露。針對這個問題,一種解決方案是在保存用戶密碼時,不再使用明文,而是使用消息摘要算法,比如 MD5 算法對明文密碼進行哈希運算,然后把運算的結(jié)果保存到數(shù)據(jù)庫中。使用上述方案,避免了在數(shù)據(jù)庫中以明文方式保存密碼,提高了系統(tǒng)的安全性,不過這種方案并不安全,后面我們會詳細(xì)分析。
當(dāng)用戶登錄時,登錄系統(tǒng)對用戶輸入的密碼執(zhí)行 MD5 哈希運算,然后再使用用戶 ID 和密碼對應(yīng)的 MD5 “數(shù)字指紋” 進行用戶認(rèn)證。若認(rèn)證通過,則當(dāng)前的用戶可以正常登錄系統(tǒng)。用戶密碼經(jīng)過 MD5 哈希運算后存儲的方案至少有兩個好處:
- 防內(nèi)部攻擊:因為在數(shù)據(jù)庫中不會以明文的方式保存密碼,因此可以避免系統(tǒng)中用戶的密碼被具有系統(tǒng)管理員權(quán)限的人員知道。
- 防外部攻擊:網(wǎng)站數(shù)據(jù)庫被黑客入侵,黑客只能獲取經(jīng)過 MD5 運算后的密碼,而不是用戶的明文密碼。
四、MD5 算法使用示例
4.1 JAVA 示例
在 Java 中使用 MD5 算法很方便,可以直接使用 JDK 自帶的 MD5 實現(xiàn),也可以使用第三方庫提供的 MD5 實現(xiàn)。下面我們將介紹 JDK、Bouncy Castle 和 Guava 的 MD5 使用示例。為了保證以下示例的正常運行,首先我們需要在 pom.xml 文件中添加 Bouncy Castle 和 Guava 的坐標(biāo):
<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.64</version></dependency><dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>27.1-jre</version></dependency>
JDK 實現(xiàn)
public static void jdkMD5(String src) throws NoSuchAlgorithmException { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] md5Bytes = md.digest(src.getBytes()); System.out.println("JDK MD5:" + src + " -> " + bytesToHexString(md5Bytes));}
Bouncy Castle 實現(xiàn)
public static void bcMD5(String src) { MD5Digest digest = new MD5Digest(); digest.update(src.getBytes(), 0, src.getBytes().length); byte[] md5Bytes = new byte[digest.getDigestSize()]; digest.doFinal(md5Bytes, 0); System.out.println("Bouncy Castle MD5:" + src + " -> " + bytesToHexString(md5Bytes));}
Guava 實現(xiàn)
public static void guavaMD5(String src) { HashFunction hf = Hashing.md5(); HashCode hc = hf.newHasher().putString(src, Charset.defaultCharset()).hash(); System.out.println("Guava MD5:" + src + " -> " + hc);}
在 JDK 實現(xiàn)和 Bouncy Castle 實現(xiàn)的示例中使用了 bytesToHexString 方法,該方法用于把字節(jié)數(shù)組轉(zhuǎn)換成十六進制,它的具體實現(xiàn)如下:
private static String bytesToHexString(byte[] src) { StringBuilder stringBuilder = new StringBuilder(); if (src == null || src.length <= 0) { return null; } for (int i = 0; i < src.length; i++) { int v = src[i] & 0xFF; String hv = Integer.toHexString(v); if (hv.length() < 2) { stringBuilder.Append(0); } stringBuilder.append(hv); } return stringBuilder.toString();}
介紹完 MD5 算法不同的實現(xiàn),下面我們來測試一下上述的方法:
public static void main(String[] args) throws NoSuchAlgorithmException { jdkMD5("123"); bcMD5("123"); guavaMD5("123");}
以上示例代碼正常運行后,在控制臺中會輸出以下結(jié)果:
JDK MD5:123 -> 202cb962ac59075b964b07152d234b70Bouncy Castle MD5:123 -> 202cb962ac59075b964b07152d234b70Guava MD5:123 -> 202cb962ac59075b964b07152d234b70
4.2 Node.js 示例
在 Node.js 環(huán)境中,我們可以使用 crypto 原生模塊提供的 md5 實現(xiàn),當(dāng)然也可以使用主流的 MD5 第三方庫,比如 md5 這個可以同時運行在服務(wù)端和客戶端的第三方庫。與 Java 示例一樣,在介紹具體使用前,我們需要提前安裝 md5 這個第三方庫,具體安裝方式如下:
$ npm install md5 --save
Node.js Crypto 實現(xiàn)
const crypto = require('crypto'); const msg = "123";function md5(data){ const hash = crypto.createHash('md5'); return hash.update(data).digest('hex');}console.log("Node.js Crypto MD5:" + msg + " -> " + md5(msg));
Node.js MD5 第三方庫實現(xiàn)
const md5 = require('md5');const msg = "123";console.log("MD5 Lib MD5:" + msg + " -> " + md5(msg));
以上示例代碼正常運行后,在控制臺中會輸出以下結(jié)果:
Node.js Crypto MD5:123 -> 202cb962ac59075b964b07152d234b70MD5 Lib MD5:123 -> 202cb962ac59075b964b07152d234b70
五、MD5 算法的缺陷
哈希碰撞是指不同的輸入?yún)s產(chǎn)生了相同的輸出,好的哈希算法,應(yīng)該沒有人能從中找到 “碰撞” 或者說極度難找到,雖然 “碰撞” 是肯定存在的。
2005 年山東大學(xué)的王小云教授發(fā)布算法可以輕易構(gòu)造 MD5 碰撞實例,此后 2007 年,有國外學(xué)者在王小云教授算法的基礎(chǔ)上,提出了更進一步的 MD5 前綴碰撞構(gòu)造算法 “chosen prefix collision”,此后還有專家陸續(xù)提供了MD5 碰撞構(gòu)造的開源的庫。
2009 年,中國科學(xué)院的謝濤和馮登國僅用了 220.96 的碰撞算法復(fù)雜度,破解了 MD5 的碰撞抵抗,該攻擊在普通計算機上運行只需要數(shù)秒鐘。
MD5 碰撞很容易構(gòu)造,基于 MD5 來驗證數(shù)據(jù)完整性已不可靠,考慮到近期谷歌已成功構(gòu)造了 SHA-1(英語:Secure Hash Algorithm 1,中文名:安全散列算法1)的碰撞實例,對于數(shù)據(jù)完整性,應(yīng)使用 SHA256 或更強的算法代替。
下面我們來看個簡單的 MD5 碰撞示例:
HEX(十六進制)樣本A1
4dc968ff0ee35c209572d4777b721587d36fa7b21bdc56b74a3dc0783e7b9518afbfa200a8284bf36e8e4b55b35f427593d849676da0d1555d8360fb5f07fea2
HEX(十六進制)樣本A2
4dc968ff0ee35c209572d4777b721587d36fa7b21bdc56b74a3dc0783e7b9518afbfa202a8284bf36e8e4b55b35f427593d849676da0d1d55d8360fb5f07fea2
兩個樣本之間的差異如下圖所示:
下面我們來通過 Java 代碼實際驗證一下樣本 A1 和樣本 A2 經(jīng)過 MD5 運算后輸出的結(jié)果是否一致:
jdkMd5Hex 方法
public static void jdkMd5Hex(String hexStr) throws NoSuchAlgorithmException { byte[] bytes = hexStringToBytes(hexStr); MessageDigest md = MessageDigest.getInstance("MD5"); byte[] md5Bytes = md.digest(bytes); System.out.println("JDK MD5:" + hexStr + " -> " + bytesToHexString(md5Bytes));}
hexStringToBytes 方法
public static byte[] hexStringToBytes(String s) { int len = s.length(); byte[] data = new byte[len / 2]; for (int i = 0; i < len; i += 2) { data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i+1), 16)); } return data;}
main 方法
public static void main(String[] args) throws NoSuchAlgorithmException { jdkMd5Hex("4dc968ff..."); //樣本A jdkMd5Hex("4dc968ff..."); //樣本B}
以上示例代碼正常運行后,在控制臺中會輸出以下結(jié)果:
JDK MD5:4dc968ff... -> 008ee33a9d58b51cfeb425b0959121c9JDK MD5:4dc968ff... -> 008ee33a9d58b51cfeb425b0959121c9
如果你對其它 MD5 碰撞的樣本感興趣,可以查看 MD5碰撞的一些例子 這篇文章。由于基于 MD5 來驗證數(shù)據(jù)完整性已不可靠,因此很多人都熟悉的 Node.js 使用了 SHA256 算法來確保數(shù)據(jù)的完整性。
(圖片來源 —— https://nodejs.org/dist/v12.14.1/SHASUMS256.txt.asc)
六、MD5 密碼安全性
6.1 MD5 密文反向查詢
前面我們已經(jīng)提到通過對用戶密碼進行 MD5 運算可以提高系統(tǒng)的安全性。但實際上,這樣的安全性還是不高。為什么呢?因為只要輸入相同就會產(chǎn)生相同的輸出。接下來我們來舉一個示例,字符串 123456789 是一個很常用的密碼,它經(jīng)過 MD5 運算后會生成一個對應(yīng)的哈希值:
MD5("123456789") -> 25f9e794323b453885f5181f1b624d0b
由于輸入相同就會產(chǎn)生相同的結(jié)果,因此攻擊者就可以根據(jù)哈希結(jié)果反推輸入。其中一種常見的破解方式就是使用彩虹表。 彩虹表是一個用于加密散列函數(shù)逆運算的預(yù)先計算好的表,常用于破解加密過的密碼散列。 查找表常常用于包含有限字符固定長度純文本密碼的加密。 這是以空間換時間的典型實踐,在每一次嘗試都計算的暴力破解中使用更少的計算能力和更多的儲存空間,但卻比簡單的每個輸入一條散列的翻查表使用更少的儲存空間和更多的計算性能。
目前網(wǎng)上某些站點,比如 cmd5.com 已經(jīng)為我們提供了 MD5 密文的反向查詢服務(wù),我們以 MD5("123456789") 生成的結(jié)果,做個簡單的驗證,具體如下圖所示:
因為 123456789 是很常見的密碼,因此該網(wǎng)站能夠反向得出正確結(jié)果那就不足為奇了。以下是 cmd5 網(wǎng)站的站點說明,大家可以參考一下,感興趣的小伙伴可以親自驗證一下。
本站針對 md5、sha1 等全球通用公開的加密算法進行反向查詢,通過窮舉字符組合的方式,創(chuàng)建了明文密文對應(yīng)查詢數(shù)據(jù)庫,創(chuàng)建的記錄約 90 萬億條,占用硬盤超過 500 TB,查詢成功率 95% 以上,很多復(fù)雜密文只有本站才可查詢。已穩(wěn)定運行十余年,國內(nèi)外享有盛譽。
現(xiàn)在我們已經(jīng)知道如果用戶的密碼相同 MD5 的值就會一樣,通過一些 MD5 密文的反向查詢網(wǎng)站,密碼大概率會被解析出來,這樣使用相同密碼的用戶就會收到影響。那么該問題如何解決呢?答案是密碼加鹽。
6.2 密碼加鹽
鹽(Salt),在密碼學(xué)中,是指在散列之前將散列內(nèi)容(例如:密碼)的任意固定位置插入特定的字符串。這個在散列中加入字符串的方式稱為 “加鹽”。其作用是讓加鹽后的散列結(jié)果和沒有加鹽的結(jié)果不相同,在不同的應(yīng)用情景中,這個處理可以增加額外的安全性。
在大部分情況,鹽是不需要保密的。鹽可以是隨機產(chǎn)生的字符串,其插入的位置可以也是隨意而定。如果這個散列結(jié)果在將來需要進行驗證(例如:驗證用戶輸入的密碼),則需要將已使用的鹽記錄下來。為了便于理解,我們來舉個簡單的示例。
Node.js MD5 加鹽示例
const crypto = require("crypto");function cryptPwd(password, salt) { const saltPassword = password + ":" + salt; console.log("原始密碼:%s", password); console.log("加鹽后的密碼:%s", saltPassword); const md5 = crypto.createHash("md5"); const result = md5.update(saltPassword).digest("hex"); console.log("加鹽密碼的md5值:%s", result);}cryptPwd("123456789","exe");cryptPwd("123456789","eft");
以上示例代碼正常運行后,在控制臺中會輸出以下結(jié)果:
原始密碼:123456789加鹽后的密碼:123456789:exe加鹽密碼的md5值:3328003d9f786897e0749f349af490ca原始密碼:123456789加鹽后的密碼:123456789:eft加鹽密碼的md5值:3c45dd21ba03e8216d56dce8fe5ebabf
通過觀察以上結(jié)果,我們發(fā)現(xiàn)原始密碼一致,但使用的鹽值不一樣,最終生成的 MD5 哈希值差異也比較大。此外為了提高破解的難度,我們可以隨機生成鹽值并且提高鹽值的長度。
6.3 bcrypt
哈希加鹽的方式確實能夠增加攻擊者的成本,但是今天來看還遠遠不夠,我們需要一種更加安全的方式來存儲用戶的密碼,這也就是今天被廣泛使用的 bcrypt 。
bcrypt 是一個由 Niels Provos 以及 David Mazières 根據(jù) Blowfish 加密算法所設(shè)計的密碼散列函數(shù),于 1999 年在 USENIX 中展示。 bcrypt 這一算法就是為哈希密碼而專門設(shè)計的,所以它是一個執(zhí)行相對較慢的算法,這也就能夠減少攻擊者每秒能夠處理的密碼數(shù)量,從而避免攻擊者的字典攻擊。 實現(xiàn)中 bcrypt 會使用一個加鹽的流程以防御彩虹表攻擊,同時 bcrypt 還是適應(yīng)性函數(shù),它可以借由增加迭代之次數(shù)來抵御日益增進的電腦運算能力透過暴力法破解。
由 bcrypt 加密的文件可在所有支持的操作系統(tǒng)和處理器上進行轉(zhuǎn)移。它的口令必須是 8 至 56 個字符,并將在內(nèi)部被轉(zhuǎn)化為 448 位的密鑰。然而,所提供的所有字符都具有十分重要的意義。密碼越強大,您的數(shù)據(jù)就越安全。
下面我們以 Node.js 平臺的 bcryptjs 為例,介紹一下如何使用 bcrypt 算法來處理用戶密碼。首先我們需要先安裝 bcryptjs :
$ npm install bcryptjs --save
Node.js bcryptjs 處理密碼
const bcrypt = require("bcryptjs");const password = "123456789";const saltRounds = 10;async function bcryptHash(str, saltRounds) { let hashedResult; try { const salt = await bcrypt.genSalt(saltRounds); hashedResult = await bcrypt.hash(str, salt); } catch (error) { throw error; } return hashedResult;}bcryptHash(password, saltRounds).then(console.log);
以上示例代碼正常運行后,在控制臺中會輸出以下結(jié)果:
$2a$10$O1SrEy3KsgN0NQdQjaSU6OxjxDo0jf.j/e2goSwSEu4esz9i58dRm
很明顯密碼 123456789 經(jīng)過 bcrypt 的哈希運算后,得到了一串讀不懂的 “亂碼”。這里我們已經(jīng)完成第一步,即用戶登錄密碼的加密。下一步我們要實現(xiàn)登錄密碼的比對,即要保證用戶輸入正確的密碼后,能正常登錄系統(tǒng)。
Node.js bcryptjs 密碼校驗
async function bcryptCompare(str, hashed) { let isMatch; try { isMatc = await bcrypt.compare(str, hashed); } catch (error) { throw error; } return isMatch;}bcryptCompare( "123456789", "$2a$10$O1SrEy3KsgN0NQdQjaSU6OxjxDo0jf.j/e2goSwSEu4esz9i58dRm").then(console.log);bcryptCompare( "123456", "$2a$10$O1SrEy3KsgN0NQdQjaSU6OxjxDo0jf.j/e2goSwSEu4esz9i58dRm").then(console.log);
以上示例代碼正常運行后,在控制臺中會輸出以下結(jié)果:
truefalse
因為我們的原始密碼是 123456789 ,很明顯與 123456 并不匹配,所以會輸出以上的匹配結(jié)果。
七、總結(jié)
本文首先介紹了消息摘要算法、MD5 算法的相關(guān)概念和特點,然后詳細(xì)介紹了 MD5 算法的用途和 Java 和 Node.js 平臺的使用示例,最后我們還分析了 MD5 算法存在的缺陷和 MD5 密碼的安全性問題。這里大家需要注意,由于 MD5 碰撞很容易構(gòu)造,基于 MD5 來驗證數(shù)據(jù)完整性已不可靠,考慮到近期谷歌已成功構(gòu)造了 SHA-1(英語:Secure Hash Algorithm 1,中文名:安全散列算法1)的碰撞實例,對于數(shù)據(jù)完整性,應(yīng)使用 SHA256 或更強的算法代替。
除了文中介紹的 MD5 應(yīng)用場景,MD5 還可以用于實現(xiàn) CDN (Content Delivery Network,內(nèi)容分發(fā)網(wǎng)絡(luò)) 內(nèi)容資源的防盜鏈,感興趣的小伙伴可以閱讀 深入了解 Token 防盜鏈 這篇文章。






