一文詳解密碼學中的Hash 算法
上一篇文章裏面,我們介紹了隨機數以及隨機數中的應用,可以看到密碼學中到處都有隨機數的身影,這種作爲大部分密碼學算法的基本組成被稱之爲 “加密基元“。今天我們一起來看一下另外一個加密基元 - 密碼學Hash算法
什麼是密碼學Hash算法
密碼學Hash算法是一個非常重要,而且常見的算法,是計算機密碼學中的核心組成部分。密碼學Hash算法是指將任何長度的二進制值映射成較短的固定長度二進制值的算法,這個較短的固定長度二進制值就是Hash值。先說一下:這個表述其實不是特別嚴謹,“任意長度”其實應該是 “算法允許長度範圍內的任意長度”,因爲有些密碼學Hash算法是有輸入長度限制的。既然很長的輸入可以變成很短的輸出,這就像我們寫文章之後,需要寫一個摘要一樣,所以Hash值很多時候也叫做 “消息摘要”, Java中計算密碼學Hash值的基類更是直接叫 MessageDigest.
下面代碼能夠更直觀一點,我們來看一些密碼學Hash的例子:
public static void main(String[] args) throws NoSuchAlgorithmException {
System.out.println(md5("abcd") + " <- abcd");
System.out.println(md5("abcd") + " <- abcd");
System.out.println(md5("明月幾時有,把酒問青天,不知天上宮闕,今夕是何年") + " <- 明月幾時有,把酒問青天,不知天上宮闕,今夕是何年");
System.out.println(md5("1") + " <- 1 ");
}
public static String md5(String content) throws NoSuchAlgorithmException {
MessageDigest instance = MessageDigest.getInstance("MD5");
instance.update(content.getBytes());
return HexUtils.toHexString(instance.digest());
}
輸出結果爲:
e2fc714c4727ee9395f324cd2e7f331f <- abcd
e2fc714c4727ee9395f324cd2e7f331f <- abcd
06d9a9f655c1ac1f0391e7dacfac6cbb <- 明月幾時有,把酒問青天,不知天上宮闕,今夕是何年
c4ca4238a0b923820dcc509a6f75849b <- 1
密碼學Hash算法的特性
密碼學Hash算法有好幾個特性:
- 相同的輸入消息總是能得到相同的Hash值。給定的Hash算法,不管消息長度多少,最終的Hash值長度是相同的。上面的例子可以看到 輸入"abcd" 的MD5 Hash值一直都是
e2fc714c4727ee9395f324cd2e7f331f
- 不可逆. 不可逆也叫單向性 (pre-image resistance)。 很難通過Hash值反推出原始消息是什麼!設想一下,要是可逆的話,那麼我們將10GB的文件變成一個128位的hash值,然後進行傳輸,對方接到後進行逆運算就可以得到原來的文件。那麼我們的網絡2G就夠了,不用去爭取5G,6G的了,大華爲最近也就不用成天被滅總欺負了!
- 很難衝突。很難找到兩個不同的消息能夠產生相同的Hash值。這裏用"很難" 而不是直接寫“無衝突”是爲了稍微嚴謹一點,因爲Hash算法MD5已經在實踐中產生碰撞了, 也就是攻擊者不斷地運算,能夠找到兩個不同的消息,使用MD5算出來的Hash值是一樣的。
密碼學Hash算法的分類
密碼學Hash算法大致可以分爲兩個類別:普通的密碼學Hash算法以及安全的密碼學Hash算法。 前面列的幾個密碼學Hash算法的特性是所有Hash算法都具備的,不管是普通的還是安全的。而安全的密碼學Hash算法則多瞭如下幾個特性:
- 強抗碰撞 Collision Resistance
如果兩個不同的原文能產生相同的Hash,這個就是產生了Hash碰撞。而如果能隨機地找到這兩個原文M1, M2,使得h(M1) = h(M2), 那麼這個就是強碰撞。而能抵抗這種碰撞的特性就叫 強抗碰撞。
-
弱抗碰撞 Second pre-image Resistance
如果給定消息M1,能夠找到M2,使得h(M1) = h(M2),這個就是弱碰撞。而能抵抗這種碰撞的特性叫弱抗碰撞。
對於攻擊者來說,Hash算法的破解難度爲 強抗碰撞 < 弱抗碰撞 < 單向性。也就是說 首先破解的是 強抗碰撞,隨便找,只要能找到兩個不同的輸入有相同的輸出就算 破解了。這裏的破解
也分爲理論破解和實際破解, 一個Hash算法理論上產生了破解,不代表在實際使用場景中不能使用該算法,因爲現實世界發生碰撞的可能性還是很大的。再退一步說,即使發生了實際破解,也不代表不能用,比如MD5 早就理論跟實際都破解了,但你如果只是用於內部系統進行消息完整性地校驗,那應該可以大膽地用。
接下來我們來看幾種常見的Hash算法:
MD5
MD5 - Message Digest #5 是MIT教授1992年公開一種Hash算法,它接收任意長度的輸入,輸出一個128位的Hash值。這個算法其實挺佛性的:不管你來多大的輸入,1位也行,100GB也行,我最終的輸出有且僅有128位。比如前面數字 1
的MD5 hash值是c4ca4238a0b923820dcc509a6f75849b <- 1
, 由32個十六進制的字符,所以總輸出位數爲 32 x 4 =128位。2004年,這個算法被中科院王小云院士證明了可以產生碰撞。所以在一些安全要求比較高的場合下,慢慢地不再用MD5算法了。但是MD5還是出現在很多實現當中,因爲它雖然打破了強碰撞性,但是單一性還是有的,而且它的輸出是常見的Hash算法裏面最短的,只有128位。
SHA
安全的密碼學算法SHA - Secured Hash Algorithm, 是一組密碼學Hash算法的總稱。由美國國家標準與技術研究院(NIST)指定的算法。主要有3類算法: SHA-1, SHA2, SHA-3.
-
SHA-1
SHA-1類似於MD5的算法, 輸出的長度固定位160位。還是前面的數字
1
, 其SHA-1的輸出爲40個十六進制的字符356a192b7913b04c54574d18c28d46e6395428ab
. 跟MD5一樣,SHA-1也是被王小云院士在2005年證明爲不安全,在實踐中產生了碰撞。但還是那句話,實踐中被證實發生碰撞,不代表不能用。比如很多文件下載的都提供一個SHA-1的Hash值供用戶進行校驗。 -
SHA-2
SHA-2可以說是SHA-1的升級版,Hash的構造跟實現也不同。SHA-1是固定160位,而SHA-2 可以是224, 384, 256, 512位,分別對應的算法名字是 SHA-224, SHA-256, SHA-384, SHA-512. 簡單的不同就是位數越多,其產生碰撞的概率就越低,當然也運算需要的時間也相應的就長一些。SHA-2截止目前安全的,其中SHA-256是數字證書SSL的標準hash算法,打開帶有www.bing.com, 我們打開證書就能看到具體的簽名hash算法是SHA256。 所以如果沒有特殊的要求,建議採用SHA256作爲密碼學Hash算法的首選。
-
SHA-3
SHA-2 和SHA-1使用相同的處理引擎, 所以理論上都存在碰撞的風險.於是NIST提出了設計新的安全Hash算法的競賽,號召大家去設計新的安全Hash算法, 命名爲SHA-3。這些算法需要滿足如下4個要求:
- Hash函數需要容易實現
- 必須保守安全,能抵抗已知的碰撞攻擊。要和SHA-2一樣,有4個相同的Hash大小(224位,256位,384位,512位)
- Hash算法必須接收密碼分析,源代碼和分析結果要公開給第三方審查
- 算法實現多樣性
最終在2008年,Keccak算法(讀作爲“ket-chak”)從51個候選者中脫穎而出,成爲SHA-3的標準算法。現在很多產品應用已經開始使用SHA-3作爲Hash算法,比如區塊鏈實現 以太坊Ethereum的基礎Hash算法就是SHA-3.
SHA3對應的4種位數的算法名字爲: SHA3-224, SHA3-256, SHA3-384, SHA3-512.
Java 中密碼學Hash算法的使用
Java的SDK中已經內置了MD5, SHA1, SHA2的Hash算法,我們可以很方便地使用這些算法. 而SHA-3不包含在JDK裏面,需要在maven裏面引入Bouncy Castle的包
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.56</version>
</dependency>
public class HashUtil {
public static void main(String[] args) throws NoSuchAlgorithmException {
System.out.println("abcd -> "+md5("abcd"));
System.out.println("abcd -> "+sha1("abcd"));
System.out.println("abcd -> "+sha2("abcd"));
System.out.println("abcd -> "+sha3("abcd"));
}
public static String md5(String content) throws NoSuchAlgorithmException {
MessageDigest instance = MessageDigest.getInstance("MD5");
instance.update(content.getBytes());
return HexUtils.toHexString(instance.digest());
}
public static String sha1(String content) throws NoSuchAlgorithmException {
MessageDigest instance = MessageDigest.getInstance("SHA-1");
instance.update(content.getBytes());
return HexUtils.toHexString(instance.digest());
}
public static String sha2(String content) throws NoSuchAlgorithmException {
MessageDigest instance = MessageDigest.getInstance("SHA-256");
instance.update(content.getBytes());
return HexUtils.toHexString(instance.digest());
}
public static String sha3(String content) throws NoSuchAlgorithmException {
Digest digest = new SHA3Digest(256);
digest.update(content.getBytes(),0, content.length());
byte[] hashArray = new byte[digest.getDigestSize()];
digest.doFinal(hashArray, 0);
return HexUtils.toHexString(hashArray);
}
}
可以看到輸出爲:
abcd -> e2fc714c4727ee9395f324cd2e7f331f
abcd -> 81fe8bfe87576c3ecb22426f8e57847382917acf
abcd -> 88d4266fd4e6338d13b845fcf289579d209c897823b9217da3e161936f031589
abcd -> 6f6f129471590d2c91804c812b5750cd44cbdfb7238541c451e1ea2bc0193177
密碼學Hash算法的使用場景
數據一致性
密碼學Hash算法有 根據相同的輸入不管運行多少次都會得到相同的輸出這個特性,那麼我們就可以用來作爲數據一致性的校驗:
-
文件比較
第一種就是文件的比較,經常可以在各種軟件下載,或者Docker鏡像下載的地方,都會提供該文件的Hash值以及對應的Hash算法,這樣就可以在本地驗證文件是否完整下載下來。
-
數字簽名
很多地方都會用到數字簽名,比如前面說到的SSL證書,或者是JWT,這裏面都需要對原始數據計算Hash,並進行加密處理
身份驗證
前面提到密碼學Hash算法必須具備有單向性,也就是不可逆,不能從Hash值逆向推算出原始值,基於這種特性,我們可以用來進行用戶身份驗證。用戶登錄都需要進行用戶名密碼的校驗,如果我們將用戶密碼存在數據庫裏面,一旦數據庫泄露,那就所有人的密碼都泄露了,這樣的事情前幾年發生了不少!那麼如果我們將用戶的原始密碼進行Hash運算,並只是把Hash值保存在數據庫裏面,當用戶登錄時,我們計算用戶輸入密碼的Hash值,並與數據庫的Hash值進行比較,如果相同則驗證通過,不同則失敗。而用戶的明文密碼不進行保存,這樣一來,萬一數據庫泄露了,也不會一下子泄露了全部的用戶密碼。當然,這樣也避免不了彩虹表以及字典攻擊,不過我們可以通過對Hash算法加鹽進行處理。具體的操作後面再詳細介紹。
文件秒傳
用沒有試過向雲盤上傳一部1-2G的電影,結果幾秒就上傳成功? 以我們目前的網絡基礎設施,應該到不了這樣的速度的。那這是怎麼做的呢? 其中的一種實現方式就是Hash值的計算,在上傳之前,客戶端先計算出要上傳文件的Hash值,然後將這個值發送回後臺,後臺檢查是否已經存在該Hash值,如果已經存在,則告訴客戶端說存在相同的文件,無需再上傳,並且把對應的文件ID發給客戶端就行,這樣客戶端就實現了秒傳,同時也可以在服務器看到這個文件!