Hash算法總結(轉載)

1. Hash是什麼,它的做用

先舉個例子。咱們每一個活在世上的人,爲了可以參與各類社會活動,都須要一個用於識別本身的標誌。也許你以爲名字或是身份證就足以表明你這我的,可是這種表明性很是脆弱,由於重名的人不少,身份證也能夠僞造。最可靠的辦法是把一我的的全部基因序列記錄下來用來表明這我的,但顯然,這樣作並不實際。而指紋看上去是一種不錯的選擇,雖然一些專業組織仍然能夠模擬某我的的指紋,但這種代價實在過高了。java

而對於在互聯網世界裏傳送的文件來講,如何標誌一個文件的身份一樣重要。好比說咱們下載一個文件,文件的下載過程當中會通過不少網絡服務器、路由器的中轉,如何保證這個文件就是咱們所須要的呢?咱們不可能去一一檢測這個文件的每一個字節,也不能簡單地利用文件名、文件大小這些極容易假裝的信息,這時候,咱們就須要一種指紋同樣的標誌來檢查文件的可靠性,這種指紋就是咱們如今所用的Hash算法(也叫散列算法)。node

散列算法(Hash Algorithm),又稱哈希算法,雜湊算法,是一種從任意文件中創造小的數字「指紋」的方法。與指紋同樣,散列算法就是一種以較短的信息來保證文件惟一性的標誌,這種標誌與文件的每個字節都相關,並且難以找到逆向規律。所以,當原有文件發生改變時,其標誌值也會發生改變,從而告訴文件使用者當前的文件已經不是你所需求的文件。python

這種標誌有何意義呢?以前文件下載過程就是一個很好的例子,事實上,如今大部分的網絡部署和版本控制工具都在使用散列算法來保證文件可靠性。而另外一方面,咱們在進行文件系統同步、備份等工具時,使用散列算法來標誌文件惟一性能幫助咱們減小系統開銷,這一點在不少雲存儲服務器中都有應用。android

以Git爲表明的衆多版本控制工具都在使用SHA1等散列函數檢查文件更新

固然,做爲一種指紋,散列算法最重要的用途在於給證書、文檔、密碼等高安全係數的內容添加加密保護。這一方面的用途主要是得益於散列算法的不可逆性,這種不可逆性體如今,你不只不可能根據一段經過散列算法獲得的指紋來得到原有的文件,也不可能簡單地創造一個文件並讓它的指紋與一段目標指紋相一致。散列算法的這種不可逆性維持着不少安全框架的運營,而這也將是本文討論的重點。git

2. Hash算法有什麼特色

一個優秀的 hash 算法,將能實現:算法

  • 正向快速:給定明文和 hash 算法,在有限時間和有限資源內能計算出 hash 值。
  • 逆向困難:給定(若干) hash 值,在有限時間內很難(基本不可能)逆推出明文。
  • 輸入敏感:原始輸入信息修改一點信息,產生的 hash 值看起來應該都有很大不一樣。
  • 衝突避免:很難找到兩段內容不一樣的明文,使得它們的 hash 值一致(發生衝突)。即對於任意兩個不一樣的數據塊,其hash值相同的可能性極小;對於一個給定的數據塊,找到和它hash值相同的數據塊極爲困難。

但在不一樣的使用場景中,如數據結構和安全領域裏,其中對某一些特色會有所側重。sql

2.1 Hash在管理數據結構中的應用

在用到hash進行管理的數據結構中,就對速度比較重視,對抗碰撞不太看中,只要保證hash均勻分佈就能夠。好比hashmap,hash值(key)存在的目的是加速鍵值對的查找,key的做用是爲了將元素適當地放在各個桶裏,對於抗碰撞的要求沒有那麼高。換句話說,hash出來的key,只要保證value大體均勻的放在不一樣的桶裏就能夠了。但整個算法的set性能,直接與hash值產生的速度有關,因此這時候的hash值的產生速度就尤其重要,以JDK中的String.hashCode()方法爲例:數據庫

public int hashCode() { int h = hash; //hash default value : 0 if (h == 0 && value.length > 0) { //value : char storage char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; } 

 

很簡潔的一個乘加迭代運算,在很多的hash算法中,使用的是異或+加法進行迭代,速度和前者差很少。數組

2.1 Hash在在密碼學中的應用

在密碼學中,hash算法的做用主要是用於消息摘要和簽名,換句話說,它主要用於對整個消息的完整性進行校驗。舉個例子,咱們登錄知乎的時候都須要輸入密碼,那麼知乎若是明文保存這個密碼,那麼黑客就很容易竊取你們的密碼來登錄,特別不安全。那麼知乎就想到了一個方法,使用hash算法生成一個密碼的簽名,知乎後臺只保存這個簽名值。因爲hash算法是不可逆的,那麼黑客即使獲得這個簽名,也絲毫沒有用處;而若是你在網站登錄界面上輸入你的密碼,那麼知乎後臺就會從新計算一下這個hash值,與網站中儲存的原hash值進行比對,若是相同,證實你擁有這個帳戶的密碼,那麼就會容許你登錄。銀行也是如此,銀行是萬萬不敢保存用戶密碼的原文的,只會保存密碼的hash值而而已。在這些應用場景裏,對於抗碰撞和抗篡改能力要求極高,對速度的要求在其次。一個設計良好的hash算法,其抗碰撞能力是很高的。以MD5爲例,其輸出長度爲128位,設計預期碰撞機率爲,這是一個極小極小的數字——而即使是在MD5被王小云教授破解以後,其碰撞機率上限也高達,也就是說,至少須要找次纔能有1/2的機率來找到一個與目標文件相同的hash值。而對於兩個類似的字符串,MD5加密結果以下:安全

MD5("version1") = "966634ebf2fc135707d6753692bf4b1e"; MD5("version2") = "2e0e95285f08a07dea17e7ee111b21c8";

 

能夠看到僅僅一個比特位的改變,兩者的MD5值就天差地別了

ps : 其實把hash算法當成是一種加密算法,這是不許確的,咱們知道加密老是相對於解密而言的,沒有解密何談加密呢,HASH的設計以沒法解爲目的的。而且若是咱們不附加一個隨機的salt值,HASH口令是很容易被字典攻擊入侵的。

3. Hash算法是如何實現的?

密碼學和信息安全發展到如今,各類加密算法和散列算法已經不是隻言片語所能解釋得了的。在這裏咱們僅提供幾個簡單的概念供你們參考。

做爲散列算法,首要的功能就是要使用一種算法把原有的體積很大的文件信息用若干個字符來記錄,還要保證每個字節都會對最終結果產生影響。那麼你們也許已經想到了,求模這種算法就能知足咱們的須要。

事實上,求模算法做爲一種不可逆的計算方法,已經成爲了整個現代密碼學的根基。只要是涉及到計算機安全和加密的領域,都會有模計算的身影。散列算法也並不例外,一種最原始的散列算法就是單純地選擇一個數進行模運算,好比如下程序。

# 構造散列函數 def hash(a): return a % 8 # 測試散列函數功能 print(hash(233)) print(hash(234)) print(hash(235)) # 輸出結果 - 1 - 2 - 3

 

很顯然,上述的程序完成了一個散列算法所應當實現的初級目標:用較少的文本量表明很長的內容(求模以後的數字確定小於8)。但也許你已經注意到了,單純使用求模算法計算以後的結果帶有明顯的規律性,這種規律將致使算法將能難保證不可逆性。因此咱們將使用另一種手段,那就是異或。

再來看下面一段程序,咱們在散列函數中加入一個異或過程。

# 構造散列函數 def hash(a): return (a % 8) ^ 5 # 測試散列函數功能 print(hash(233)) print(hash(234)) print(hash(235)) # 輸出結果 - 4 - 7 - 6

 

很明顯的,加入一層異或過程以後,計算以後的結果規律性就不是那麼明顯了。

固然,你們也許會以爲這樣的算法依舊很不安全,若是用戶使用連續變化的一系列文本與計算結果相比對,就頗有可能找到算法所包含的規律。可是咱們還有其餘的辦法。好比在進行計算以前對原始文本進行修改,或是加入額外的運算過程(如移位),好比如下程序。

# 構造散列函數 def hash(a): return (a + 2 + (a << 1)) % 8 ^ 5 # 測試散列函數功能 print(hash(233)) print(hash(234)) print(hash(235)) # 輸出結果 - 0 - 5 - 6

 

這樣處理獲得的散列算法就很難發現其內部規律,也就是說,咱們並不能很輕易地給出一個數,讓它通過上述散列函數運算以後的結果等於4——除非咱們去窮舉測試。

上面的算法是否是很簡單?事實上,下面咱們即將介紹的經常使用算法MD5和SHA1,其本質算法就是這麼簡單,只不過會加入更多的循環和計算,來增強散列函數的可靠性。

4. Hash有哪些流行的算法

目前流行的 Hash 算法包括 MD五、SHA-1 和 SHA-2。

  • MD4(RFC 1320)是 MIT 的 Ronald L. Rivest 在 1990 年設計的,MD 是 Message Digest 的縮寫。其輸出爲 128 位。MD4 已證實不夠安全。

  • MD5(RFC 1321)是 Rivest 於1991年對 MD4 的改進版本。它對輸入仍以 512 位分組,其輸出是 128 位。MD5 比 MD4 複雜,而且計算速度要慢一點,更安全一些。MD5 已被證實不具有」強抗碰撞性」。

  • SHA (Secure Hash Algorithm)是一個 Hash 函數族,由 NIST(National Institute of Standards and Technology)於 1993 年發佈第一個算法。目前知名的 SHA-1 在 1995 年面世,它的輸出爲長度 160 位的 hash 值,所以抗窮舉性更好。SHA-1 設計時基於和 MD4 相同原理,而且模仿了該算法。SHA-1 已被證實不具」強抗碰撞性」。

  • 爲了提升安全性,NIST 還設計出了 SHA-22四、SHA-25六、SHA-384,和 SHA-512 算法(統稱爲 SHA-2),跟 SHA-1 算法原理相似。SHA-3 相關算法也已被提出。

能夠看出,上面這幾種流行的算法,它們最重要的一點區別就是」強抗碰撞性」。

5. 那麼,何謂Hash算法的「碰撞」?

你可能已經發現了,在實現算法章節的第一個例子,咱們嘗試的散列算法獲得的值必定是一個不大於8的天然數,所以,若是咱們隨便拿9個數去計算,確定至少會獲得兩個相同的值,咱們把這種狀況就叫作散列算法的「碰撞」(Collision)。

這很容易理解,由於做爲一種可用的散列算法,其位數必定是有限的,也就是說它能記錄的文件是有限的——而文件數量是無限的,兩個文件指紋發生碰撞的機率永遠不會是零。

但這並不意味着散列算法就不能用了,由於凡事都要考慮代價,買光全部彩票去中一次頭獎是毫無心義的。現代散列算法所存在的理由就是,它的不可逆性能在較大機率上獲得實現,也就是說,發現碰撞的機率很小,這種碰撞能被利用的機率更小。

隨意找到一組碰撞是有可能的,只要窮舉就能夠。散列算法獲得的指紋位數是有限的,好比MD5算法指紋字長爲128位,意味着只要咱們窮舉21282128次,就確定能獲得一組碰撞——固然,這個時間代價是不可思議的,而更重要的是,僅僅找到一組碰撞並無什麼實際意義。更有意義的是,若是咱們已經有了一組指紋,可否找到一個原始文件,讓它的散列計算結果等於這組指紋。若是這一點被實現,咱們就能夠很容易地篡改和僞造網絡證書、密碼等關鍵信息。

你也許已經聽過MD5已經被破解的新聞——但事實上,即使是MD5這種已通過時的散列算法,也很難實現逆向運算。咱們如今更多的仍是依賴於海量字典來進行嘗試,也就是經過已經知道的大量的文件——指紋對應關係,搜索某個指紋所對應的文件是否在數據庫裏存在。

5.1 MD5的實際碰撞案例

下面讓咱們來看看一個真實的碰撞案例。咱們之因此說MD5過期,是由於它在某些時候已經很難表現出散列算法的某些優點——好比在應對文件的微小修改時,散列算法獲得的指紋結果應當有顯著的不一樣,而下面的程序說明了MD5並不能實現這一點。

import hashlib

# 兩段HEX字節串,注意它們有細微差異 a = bytearray.fromhex("0e306561559aa787d00bc6f70bbdfe3404cf03659e704f8534c00ffb659c4c8740cc942feb2da115a3f4155cbb8607497386656d7d1f34a42059d78f5a8dd1ef") b = bytearray.fromhex("0e306561559aa787d00bc6f70bbdfe3404cf03659e744f8534c00ffb659c4c8740cc942feb2da115a3f415dcbb8607497386656d7d1f34a42059d78f5a8dd1ef") # 輸出MD5,它們的結果一致 print(hashlib.md5(a).hexdigest()) print(hashlib.md5(b).hexdigest()) ### a和b輸出結果都爲: cee9a457e790cf20d4bdaa6d69f01e41 cee9a457e790cf20d4bdaa6d69f01e41 

 

而諸如此類的碰撞案例還有不少,上面只是原始文件相對較小的一個例子。事實上如今咱們用智能手機只要數秒就能找到MD5的一個碰撞案例,所以,MD5在數年前就已經不被推薦做爲應用中的散列算法方案,取代它的是SHA家族算法,也就是安全散列算法(Secure Hash Algorithm,縮寫爲SHA)。

5.2 SHA家族算法以及SHA1碰撞

安全散列算法與MD5算法本質上的算法是相似的,但安全性要領先不少——這種領先型更多的表如今碰撞攻擊的時間開銷更大,固然相對應的計算時間也會慢一點。

SHA家族算法的種類不少,有SHA0、SHA一、SHA25六、SHA384等等,它們的計算方式和計算速度都有差異。其中SHA1是如今用途最普遍的一種算法。包括GitHub在內的衆多版本控制工具以及各類雲同步服務都是用SHA1來區別文件,不少安全證書或是簽名也使用SHA1來保證惟一性。長期以來,人們都認爲SHA1是十分安全的,至少你們尚未找到一次碰撞案例。

但這一事實在2017年2月破滅了。CWI和Google的研究人員們成功找到了一例SHA1碰撞,並且很厲害的是,發生碰撞的是兩個真實的、可閱讀的PDF文件。這兩個PDF文件內容不相同,但SHA1值徹底同樣。(對於這件事的影響範圍及討論,可參考知乎上的討論:如何評價 2 月 23 日谷歌宣佈實現了 SHA-1 碰撞?)

因此,對於一些大的商業機構來講, MD5 和 SHA1 已經不夠安全,推薦至少使用 SHA2-256 算法。

6. Hash在Java中的應用

6.1 HashMap的複雜度

在介紹HashMap的實現以前,先考慮一下,HashMap與ArrayList和LinkedList在數據複雜度上有什麼區別。下圖是他們的性能對比圖:

  獲取 查找 添加/刪除 空間
ArrayList O(1) O(1) O(N) O(N)
LinkedList O(N) O(N) O(1) O(N)
HashMap O(N/Bucket_size) O(N/Bucket_size) O(N/Bucket_size) O(N)

能夠看出HashMap總體上性能都很是不錯,可是不穩定,爲O(N/Buckets),N就是以數組中沒有發生碰撞的元素,Buckets是因碰撞產生的鏈表。

注:發生碰撞其實是很是稀少的,因此N/Bucket_size約等於1

HashMap是對Array與Link的折衷處理,Array與Link能夠說是兩個速度方向的極端,Array注重於數據的獲取,而處理修改(添加/刪除)的效率很是低;Link因爲是每一個對象都保持着下一個對象的指針,查找某個數據須要遍歷以前全部的數據,因此效率比較低,而在修改操做中比較快。

6.2 HashMap的實現

本文以JDK8的API實現進行分析

6.2.1 對key進行Hash計算

在JDK8中,因爲使用了紅黑樹來處理大的鏈表開銷,因此hash這邊能夠更加省力了,只用計算hashCode並移動到低位就能夠了。

static final int hash(Object key) { int h; //計算hashCode,並沒有符號移動到低位 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }

 

舉個例子: 363771819^(363771819 >>> 16)

0001 0101 1010 1110 1011 0111 1010 1011(363771819)
0000 0000 0000 0000 0001 0101 1010 1110(5550) XOR
--------------------------------------- = 0001 0101 1010 1110 1010 0010 0000 0101(363766277)

 

這樣作能夠實現了高地位更加均勻地混到一塊兒。

下面給出在Java中幾個經常使用的哈希碼(hashCode)的算法。

  1. Object類的hashCode. 返回對象的通過處理後的內存地址,因爲每一個對象的內存地址都不同,因此哈希碼也不同。這個是native方法,取決於JVM的內部設計,通常是某種C地址的偏移。

  2. String類的hashCode. 根據String類包含的字符串的內容,根據一種特殊算法返回哈希碼,只要字符串的內容相同,返回的哈希碼也相同。

  3. Integer等包裝類,返回的哈希碼就是Integer對象裏所包含的那個整數的數值,例如Integer i1=new Integer(100), i1.hashCode的值就是100 。因而可知,2個同樣大小的Integer對象,返回的哈希碼也同樣。

  4. int,char這樣的基礎類,它們不須要hashCode,若是須要存儲時,將進行自動裝箱操做,計算方法同上。

6.2.2 獲取到數組的index的位置

計算了Hash,咱們如今要把它插入數組中了

i = (tab.length - 1) & hash;
  • 1

經過位運算,肯定了當前的位置,由於HashMap數組的大小老是2^n,因此實際的運算就是 (0xfff…ff) & hash ,這裏的tab.length-1至關於一個mask,濾掉了大於當前長度位的hash,使每一個i都能插入到數組中。

6.2.3 生成包裝類

這個對象是一個包裝類,Node

static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; //getter and setter .etc. }

 

6.2.4 插入包裝類到數組

(1). 若是輸入當前的位置是空的,就插進去,如圖,左爲插入前,右爲插入後

0           0
    |           |
    1 -> null   1 - > null
    |           |
    2 -> null   2 - > null
    |           | 
    ..-> null   ..- > null
    |           | 
    i -> null   i - > new node
    |           |
    n -> null   n - > null

 

(2). 若是當前位置已經有了node,且它們發生了碰撞,則新的放到前面,舊的放到後面,這叫作鏈地址法處理衝突。

0           0
    |           |
    1 -> null   1 - > null
    |           |
    2 -> null   2 - > null
    |           | 
    ..-> null   ..- > null
    |           | 
    i -> old    i - > new - > old
    |           |
    n -> null   n - > null

 

咱們能夠發現,失敗的hashCode算法會致使HashMap的性能由數組降低爲鏈表,因此想要避免發生碰撞,就要提升hashCode結果的均勻性。

6.3 擴容

若是當表中的75%已經被佔用,即視爲須要擴容了

(threshold = capacity * load factor ) < size
  • 1

它主要有兩個步驟:

6.3.1 容量加倍

左移1位,就是擴大到兩倍,用位運算取代了乘法運算

newCap = oldCap << 1; newThr = oldThr << 1;
  • 1
  • 2

6.3.2 遍歷計算Hash

for (int j = 0; j < oldCap; ++j) { Node<K,V> e; //若是發現當前有Bucket if ((e = oldTab[j]) != null) { oldTab[j] = null; //若是這裏沒有碰撞 if (e.next == null) //從新計算Hash,分配位置 newTab[e.hash & (newCap - 1)] = e; //這個見下面的新特性介紹,若是是樹,就填入樹 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //若是是鏈表,就保留順序....目前就看懂這點 else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } }

 

由此能夠看出擴容須要遍歷並從新賦值,成本很是高,因此選擇一個好的初始容量很是重要。

6.4 擴容如何提高性能?

  • 解決擴容損失:若是知道大體須要的容量,把初始容量設置好以解決擴容損失;
    好比我如今有1000個數據,須要 1000/0.75 = 1333 個坑位,又 1024 < 1333 < 2048,因此最好使用2048做爲初始容量。

  • 解決碰撞損失:使用高效的HashCode與loadFactor,這個…因爲JDK8的高性能出現,這兒問題也不大了。

6.5 HashMap與HashTable的主要區別

在不少的Java基礎書上都已經說過了,他們的主要區別其實就是Table全局加了線程同步保護

  • HashTable線程更加安全,代價就是由於它粗暴的添加了同步鎖,因此會有性能損失。
  • 其實有更好的concurrentHashMap能夠替代HashTable,一個是方法級,一個是Class級。

6.6 在Android中使用SparseArray代替HashMap

官方推薦使用SparseArray([spɑ:s][ə’reɪ],稀疏的數組)或者LongSparseArray代替HashMap。官方總結有一下幾點好處:

  • SparseArray使用基本類型(Primitive)中的int做爲Key,不須要Pair

總結

「The Algorithm Design Manual」一書中提到,雅虎的 Chief Scientist ,Udi Manber 曾說過,在 yahoo 所應用的算法中,最重要的三個是:Hash,Hash 和 Hash。其實從上文中所舉的git用sha1判斷文件更改,密碼用MD5生成摘要後加鹽等等對Hash的應用可看出,Hash的在計算機世界扮演着多麼重要的角色。另書中還舉了一個頗有趣的顯示中例子:

一場拍賣會中,物品是價高者得,若是每一個人只有一次出價機會,同時提交本身的價格後,最後一塊兒公佈,出價最高則勝出。這種形式存在做弊的可能,若是有出價者能 hack 進後臺,而後將本身的價格改成最高價 +1,則能以最低的代價得到勝利。如何杜絕這種做弊呢?

答案很簡單,參與者都提交自身出價的 hash 值就能夠了,即便有人能黑進後臺也沒法得知明文價格,等到公佈之時,再對比原出價與 hash 值是否對應便可。是否是很巧妙?

是的,上面的作法,與上文提到的網站上儲存密碼用MD5 值而非明文,是同一種思想,異曲同工。

能夠看到不管是密碼學、數據結構、現實生活中的應用,處處能夠看到Hash的影子,經過這篇文章的介紹,相信你不只知其名,也能懂其意。

相關文章
相關標籤/搜索