摘要算法簡介
摘要算法又稱哈希算法、散列算法。它通過一個函數,把任意長度的數據轉換為一個長度固定的數據串(通常用16進制的字符串表示)。
Python的hashlib提供了常見的摘要算法,如 MD5,SHA1,SHA512 等等。
提示:
要注意摘要算法不是加密算法,不能用於加密(因為無法通過摘要反推明文),只能用於防篡改,但是它的單向計算特性決定了可以在不存儲明文口令的情況下驗證用戶口令。
如果原文內容是一個字符串'I am Jason',經MD5計算內容摘要是'2d3ec0dd5d4b99a2c5f1eb47656637e0'。如果有人篡改了你的文章,並發表為'I am Ross',篡改后的字符串計算出的摘要('6845af67ef35bfe261f6fed5a66ff3ab')不同於原文的摘要,你就知道該內容被串改了。
摘要算法之所以能指出數據是否被篡改過,就是因為摘要函數是一個單向函數,計算f(data)很容易,但通過digest反推data卻非常困難。而且,對原始數據做一個bit的修改,都會導致計算出的摘要完全不同。摘要算法就是通過摘要函數f()對任意長度的數據data計算出固定長度的摘要digest。
MD5 摘要算法示例
MD5是最常見的摘要算法,速度很快,生成結果是固定的128 bit字節,通常用一個32位的16進制字符串表示。
數據量小的時候可以一次性求出摘要
import hashlib
md5 = hashlib.md5()
# update中的數據需要是bytes類型的數據據。
md5.update(bytes('I am Jason', encoding='utf-8'))
# digest()返回的是二進制的字符串表達形式
data1 = md5.digest()
# hexdigest()返回的是十六進制的字符串表達形式
data2 = md5.hexdigest()
print(data1) # b'->\xc0\xdd]K\x99\xa2\xc5\xf1\xebGef7\xe0'
print(data2) # 2d3ec0dd5d4b99a2c5f1eb47656637e0
數據量很大,可以分塊多次調用update(),最后計算的結果是一樣的:
import hashlib
md5 = hashlib.md5()
md5.update(bytes('I am ', encoding='utf-8'))
md5.update(bytes('Jason', encoding='utf-8'))
data = md5.hexdigest()
print(data) # 多次求值后返回的是十六進制的字符串表達形式還是 2d3ec0dd5d4b99a2c5f1eb47656637e0
SHA1 摘要算法示例
另一種常見的摘要算法是SHA1,調用SHA1和調用MD5完全類似, SHA1的結果是160 bit字節,通常用一個40位的16進制字符串表示。
SHA1 同樣可以將數據分多段計算摘要
import hashlib
md5 = hashlib.sha1()
md5.update(bytes('I am ', encoding='utf-8')) # update中的數據需要是bytes類型的數據據。
md5.update(bytes('Jason', encoding='utf-8')) # update中的數據需要是bytes類型的數據據。
data = md5.hexdigest()
print(data) # 返回的是十六進制的字符串表達形式 c0f965c41ef25423c2fbbcd05dfc767b04c9ba7f
算法SHA256和SHA512比SHA1更安全,不過越安全的算法不僅越慢,而且摘要長度更長。
碰撞
有沒有可能兩個不同的數據通過某個摘要算法得到了相同的摘要?完全有可能,因為任何摘要算法都是把無限多的數據集合映射到一個有限的集合中,這種情況稱為碰撞。
比如:一個字符串'do you have the same hash value with me?' 的 md5 摘要為 b9455cde6391d136c33541b7293ec394
, 很有可能另一個字符串的md5摘要也是這個值。
摘要算法應用
摘要算法一個常用場景就是數據庫存儲用戶密碼的存儲。
如果以明文保存用戶口令,如果數據庫泄露,所有用戶的口令就落入黑客的手里。此外,網站運維人員是可以訪問數據庫的,也就是能獲取到所有用戶的口令
正確的保存口令的方式是不存儲用戶的明文口令,而是存儲用戶口令的摘要,比如MD5:
username | password |
---|---|
user01 | e10adc3949ba59abbe56e057f20f883e |
user02 | 878ef96e8614558c38c87f0410ad1539 |
user03 | 99b1c2188db85afee403b1536010c2c9 |
當用戶登錄時,首先計算用戶輸入的明文口令的MD5,然后和數據庫存儲的MD5對比,如果一致,說明口令輸入正確,如果不一致,口令肯定錯誤。
下面是一個簡單的代碼演示
import hashlib
db = {
'user01': '202cb962ac59075b964b07152d234b70', # 123
'user02': '289dff07669d7a23de0ef88d2f7129e7', # 234
'user03': 'd81f9c1be2e08964bf9f24b15f0e4900', # 345
}
def check():
while 1:
username = input('username: >>>').strip()
passwd = input('password: >>>').strip()
md5 = hashlib.md5()
md5.update(passwd.encode(encoding='utf-8'))
pwd_md5 = md5.hexdigest()
if username in db and pwd_md5 == db[username]:
print('OK')
else:
print('BAD')
check()
MD5 安全性
采用MD5存儲口令也不一定安全。因為如果一個黑客已經拿到了存儲MD5口令的數據庫,可以通過暴力破解的方式反推用戶的明文口令(當然真正的黑客不會這么傻)。黑客可以事先計算出一些常用口令的MD5值,得到一個反推表(彩虹表),然后逐個碰撞即可(不一定所有的都可以成功)。
md5值 | 密碼 |
---|---|
202cb962ac59075b964b07152d234b70 | 123 |
289dff07669d7a23de0ef88d2f7129e7 | 234 |
d81f9c1be2e08964bf9f24b15f0e4900 | 345 |
這樣,無需破解,只需要對比數據庫的MD5,黑客就獲得了使用常用口令的用戶賬號。
對於用戶來講,當然不建議使用過於簡單的口令。此外,我們還可以在程序設計上對簡單口令加強保護。這就是下文要提到的加鹽。
Salt 加鹽
由於常用口令的MD5值很容易被碰撞出來,所以,存儲的用戶口令不再是那些已經被計算出來的常用口令的MD5,我們可以通過對原始口令加一個復雜字符串來實現混淆,俗稱 "加鹽":
經過Salt處理的MD5口令,只要Salt不被黑客知道,即使用戶輸入簡單口令,也很難通過MD5反推明文口令。
加鹽可以有兩種方法:
方法1
salt = b'xxx'
md5 = hashlib.md5(salt)
md5.update('your password'.encode('utf-8'))
md5.hexdigest()
方法2
salt = b'xxx'
md5 = hashlib.md5()
md5.update(b'your password' + salt)
md5.hexdigest()
import hashlib
db = {
'user01': 'cb2921a386719d7467412b5573973529', # 123 salt=b'xxx';
'user02': '8d36de04ecc00c605caf4b2798328a59', # 234 salt=b'xxx';
'user03': '658d38b0b92c8e7e5eab2bef72b539c7', # 345 salt=b'xxx';
}
def check():
while 1:
username = input('username: >>>').strip()
passwd = input('password: >>>').strip()
md5 = hashlib.md5()
md5.update(passwd.encode(encoding='utf-8') + b'xxx')
pswdmd5 = md5.hexdigest()
if username in db and pswdmd5 == db[username]:
print('OK')
else:
print('BAD')
check()
關於相同口令的MD5值的問題
但是如果有兩個用戶都使用了相同的簡單口令比如123456,在數據庫中,將存儲兩條相同的MD5值,這說明這兩個用戶的口令是一樣的。
為了讓使用相同口令的用戶存儲不同的MD5,我們假定用戶無法修改登錄名,就可以通過把登錄名作為Salt的一部分來計算MD5,從而實現相同口令的用戶也存儲不同的MD5。
import hashlib
salt = 'xxx'
username = user01
password = 123
md5 = hashlib.md5()
md5.update(username + password + salt)
data = md5.hexdigest()
print(data)