通常用戶登錄驗證的方式是用戶名和密碼,用戶和網站都擁有登陸名和密碼,這個時候,對于用戶密碼的存儲就變成了一個安全問題。
早期的驗證框架的實現是把密碼以明文的方式保存到數據庫,但是一旦數據庫被SQL injection攻擊,密碼信息泄露,用戶的信息就會赤裸裸的暴露在互聯網上面。典型的例子就是幾年前的CSDN。這種情況下,很難能找到一種合適的方式,讓我們有足夠的時間通知用戶來重新設置密碼修復安全問題。
為了給我們多爭取點時間,一種應對的策略是采用加密哈希函數(cryptographic hash function)加密用戶密碼后保存到數據庫,其哈希后的值也被稱為簽名、校驗或者哈希值。
這樣用戶在發送密碼到服務器時,服務器只需要計算下它的哈希值是否和表中的密碼值一致就可以判斷是否可以合法登陸了。
比如在MySQL中保存的密碼:
~> mysql -u root -e 'select PASSWORD("password");'
+-------------------------------------------+
| PASSWORD("password") |
+-------------------------------------------+
| *2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19 |
+-------------------------------------------+
用戶在登陸時,輸入密碼password
, 應用通過同樣的哈希函數計算密碼的哈希值并與數據庫中的密碼字段比較,匹配則登陸成功,反之則失敗。這樣如果密碼泄露,在黑客使用暴力破解或者字典攻擊破解密碼之前,我們也許可以有稍多點的時間來通知用戶修改密碼。
當然,黑客也可以加密哈希函數預先計算值并生成彩虹表(Rainbow Table)。了解到實現,黑客就可以以空間換時間的方式,先計算出密碼的哈希值,然后反查密碼,隨著這個表的增長,破解的難度可能會降低,時間也會減少。
比如MySQL中它是通過sha1加密的:
~> mysql -u root -e "select password('password'), concat('*', ucase(sha1(unhex(sha1('password')))));"
+-------------------------------------------+---------------------------------------------------+
| password('password') | concat('*', ucase(sha1(unhex(sha1('password'))))) |
+-------------------------------------------+---------------------------------------------------+
| *2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19 | *2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19 |
+-------------------------------------------+---------------------------------------------------+
如果黑客以同樣的算法,計算了password
的哈希值,那它就可以在O(1)的時間內知道密碼。
要增加破解的難度,讓我們在密碼泄露時多爭取點時間,其中的一種方式就是給密碼中加點鹽(salt),在生成密碼的哈希值時,加入一個隨機的字符串(salt),然后保存在數據庫中。這樣對于相同的密碼,在數據庫中的保存的記錄也是不同的。這樣對于使用彩虹表破解的黑客來說,破解的成本很高,因為他需要猜測混合salt的算法,同樣,暴力破解也變的不太可能。
在密碼比較時,計算用戶輸入的哈希值,然后和數據庫中去掉salt的部分記錄比較,就可以驗證是否是合法用戶了。我們以python下bcrypt
為例:
pip install bcrypt
>>> hashed_password = bcrypt.hashpw("password", bcrypt.gensalt())
>>> print hashed_password
$2b$12$K947InrSXM6XvNoErbAcj.K5YQ/OSVvJ802MxSWNgXdrjmru8Grs2
這是用Blowfish密碼生成的哈希值字符串再進行base64編碼的結果,共分為4個部分,$2b$
表明這是bcrypt
格式的哈希,第二個是成本(cost)值,默認是12,第三部分是22位的字符串,也就是salt的值,剩下的部分就是密碼哈希后的base64編碼的值。在比較的時候,只需要把哈希后的密碼當做salt傳進去就可以了。
>>> hashed_again = bcrypt.hashpw("password", hashed_password)
>>> hashed_again == hashed_password
True
hashpw
的源碼以及它調用的C類庫的bcrypt_hasspass
可以看出,在實際的比較中,它進行了版本的校驗以及截斷,重用了hashed_password
中的salt值,所以最后生成的字符串是一樣的。
Blowfish是一個symmetric-key塊分組密碼,對稱性key的意思用同樣的加密秘鑰去加密解密,就像以前諜戰片中用相同的密碼本去解密消息。塊分組的意思就是將明文分為固定長度的塊,用秘鑰分別加密后再拼起來,并且密文應該和明文的長度相同。Blowfish的分組塊大小為64bit,key的長度范圍從32bits到448bits,它使用的是16輪變換的Feistel cipher,以及基于PI生成的S-boxes,從wiki看內容描述,一直不知道為什么8bits的輸入最后產生的是32bits的輸出,后來看了偽碼后才發現它不是用的S-boxes描述的二維數組去取值的:(……。
進一步加強密碼的強度的方式是給密碼再撒一把胡椒(pepper),簡單來說就是服務器用戶的密碼在哈希之前加入額外的字符串(pepper),這樣可以變相的增強簡單密碼的強度,像下面這樣:
>>> bcrypt.hashpw("password*{abcd&", bcrypt.gensalt())
'$2b$12$2dVYv2o5vw6uMYe2IT9V9uWfIR2zdkpKDagNRZ8eFOpS4nyNHJuz.'
*{abcd&
就是pepper,它必須保存在服務器中,主要針對的場景數據庫暴露但是應用服務器安全,可以拖延字典攻擊的時間,讓用戶及早更改密碼。
PS. Coursera上面斯坦福大學的密碼學課程開始了,有興趣的可以去學習下,第二周的課程就在介紹塊加密.