一、哈希碰撞是什么?
所謂哈希(hash),就是將不同的輸入映射成獨一無二的、固定長度的值(又稱"哈希值")。它是最常見的軟件運算之一。
如果不同的輸入得到了同一個哈希值,就發生了"哈希碰撞"(collision)。
舉例來說,很多網絡服務會使用哈希函數,產生一個 token,標識用戶的身份和權限。
AFGG2piXh0ht6dmXUxqv4nA1PU120r0yMAQhuc13i8
上面這個字符串就是一個哈希值。如果兩個不同的用戶,得到了同樣的 token,就發生了哈希碰撞。服務器將把這兩個用戶視為同一個人,這意味着,用戶 B 可以讀取和更改用戶 A 的信息,這無疑帶來了很大的安全隱患。
黑客攻擊的一種方法,就是設法制造"哈希碰撞",然后入侵系統,竊取信息。
二、如何防止哈希碰撞?
防止哈希碰撞的最有效方法,就是擴大哈希值的取值空間。
16個二進制位的哈希值,產生碰撞的可能性是 65536 分之一。也就是說,如果有65537個用戶,就一定會產生碰撞。哈希值的長度擴大到32個二進制位,碰撞的可能性就會下降到 4,294,967,296 分之一。
更長的哈希值意味着更大的存儲空間、更多的計算,將影響性能和成本。開發者必須做出抉擇,在安全與成本之間找到平衡。
下面就介紹,如何在滿足安全要求的前提下,找出哈希值的最短長度。
三、生日攻擊
哈希碰撞的概率取決於兩個因素(假設哈希函數是可靠的,每個值的生成概率都相同)。
- 取值空間的大小(即哈希值的長度)
- 整個生命周期中,哈希值的計算次數
這個問題在數學上早有原型,叫做"生日問題"(birthday problem):一個班級需要有多少人,才能保證每個同學的生日都不一樣?
答案很出人意料。如果至少兩個同學生日相同的概率不超過5%,那么這個班只能有7個人。事實上,一個23人的班級有50%的概率,至少兩個同學生日相同;50人班級有97%的概率,70人的班級則是99.9%的概率(計算方法見后文)。
這意味着,如果哈希值的取值空間是365,只要計算23個哈希值,就有50%的可能產生碰撞。也就是說,哈希碰撞的可能性,遠比想象的高。實際上,有一個近似的公式。
上面公式可以算出,50% 的哈希碰撞概率所需要的計算次數,N 表示哈希的取值空間。生日問題的 N 就是365,算出來是 23.9。這個公式告訴我們,哈希碰撞所需耗費的計算次數,跟取值空間的平方根是一個數量級。
這種利用哈希空間不足夠大,而制造碰撞的攻擊方法,就被稱為生日攻擊(birthday attack)。
四、數學推導
這一節給出生日攻擊的數學推導。
至少兩個人生日相同的概率,可以先算出所有人生日互不相同的概率,再用 1 減去這個概率。
我們把這個問題設想成,每個人排隊依次進入一個房間。第一個進入房間的人,與房間里已有的人(0人),生日都不相同的概率是365/365
;第二個進入房間的人,生日獨一無二的概率是364/365
;第三個人是363/365
,以此類推。
因此,所有人的生日都不相同的概率,就是下面的公式。
上面公式的 n 表示進入房間的人數。可以看出,進入房間的人越多,生日互不相同的概率就越小。
這個公式可以推導成下面的形式。
那么,至少有兩個人生日相同的概率,就是 1 減去上面的公式。
五、哈希碰撞的公式
上面的公式,可以進一步推導成一般性的、便於計算的形式。
根據泰勒公式,指數函數 ex 可以用多項式展開。
如果 x 是一個極小的值,那么上面的公式近似等於下面的形式。
現在把生日問題的1/365
代入。
因此,生日問題的概率公式,變成下面這樣。
假設 d 為取值空間(生日問題里是 365),就得到了一般化公式。
上面就是哈希碰撞概率的公式。
六、應用
上面的公式寫成函數。
const calculate = (d, n) => { const exponent = (-n * (n - 1)) / (2 * d) return 1 - Math.E ** exponent; } calculate(365, 23) // 0.5000017521827107 calculate(365, 50) // 0.9651312540863107 calculate(365, 70) // 0.9986618113807388
一般來說,哈希值由大小寫字母和阿拉伯數字構成,一共62個字符(10 + 26 + 26)。如果哈希值只有三個字符的長度(比如abc
),取值空間就是 62 ^ 3 = 238,328
,那么10000次計算導致的哈希碰撞概率是100%。
calculate(62 ** 3, 10000) // 1
哈希值的長度增加到5個字符(比如abcde
),碰撞的概率就下降到5.3%。
calculate(62 ** 5, 10000) // 0.05310946204730993
現在有一家公司,它的 API 每秒會收到100萬個請求,每個請求都會生成一個哈希值,假定這個 API 會使用10年。那么,大約一共會計算300萬億次哈希。能夠接受的哈希碰撞概率是1000億分之一(即每天發生一次哈希碰撞),請問哈希字符串最少需要多少個字符?
根據上面的公式倒推,就會知道哈希值的最短長度是22個字符(比如BwQ1W6soXkA1PU120r0yMA
),計算過程略。
22個字符的哈希值,就能保證300萬億次計算里面,只有1000億分之一的概率發生碰撞。常用的 SHA256 哈希函數產生的是64個字符的哈希值,每個字符的取值范圍是0~9和a~f,發生碰撞的概率還要低得多。
七、參考鏈接
- How Long Should I Make My API Key?, by Sam Corcos
- Birthday problem, by Wikipedia
- Birthday attack, by Wikipedia
(完)