背景
開發(fā)web應(yīng)用,很多時(shí)候會(huì)設(shè)計(jì)一個(gè)用戶系統(tǒng)。一旦涉及到用戶的隱私信息关霸,作為開發(fā)者必須慎重對(duì)待。經(jīng)常在網(wǎng)上能看到類似這樣的新聞:2011年中國網(wǎng)站用戶信息泄露事件杰扫。一旦發(fā)生這樣的事情队寇,總是會(huì)引起用戶的恐慌,所以章姓,用戶密碼的保護(hù)是這其中的重中之重备籽。
在工作過程中我也遇到了一些類似的問題嗡髓,在這里簡(jiǎn)單記錄自己的思考。主要的實(shí)踐語言是python,其他的語言道理相通坛悉。驗(yàn)證方式是最常見的user-password口令驗(yàn)證
存儲(chǔ)密碼
作為例子,使用mysql映跟,設(shè)計(jì)一個(gè)最簡(jiǎn)單的用戶表迹卢。
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`login_name` varchar(32) NOT NULL,
`encrypted_password` varchar(128) NOT NULL,
`create_time` bigint(20) NOT NULL,
`modify_time` bigint(20) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `login_name` (`login_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
login_name用于存儲(chǔ)用戶名,encrypted_password對(duì)應(yīng)加密后的密文银还。
加密方式
很明顯用戶的密碼不能明文存儲(chǔ)风宁。一旦數(shù)據(jù)庫被攻破,一切都完了见剩。
考慮到用戶注冊(cè)/登陸的場(chǎng)景杀糯。一個(gè)新用戶注冊(cè)進(jìn)來一般的流程如下:
- 用戶新建一個(gè)賬號(hào),輸入賬號(hào)和密碼苍苞,相關(guān)數(shù)據(jù)傳送到服務(wù)器固翰。
- 服務(wù)端接收賬號(hào)和密碼(明文),將用戶名加密后的明文存入數(shù)據(jù)庫羹呵。
- 用戶登陸時(shí)輸入賬號(hào)名和密碼骂际,上傳至服務(wù)端。
- 服務(wù)端用賬號(hào)和加密后的明文與數(shù)據(jù)庫中的數(shù)據(jù)進(jìn)行比對(duì)冈欢,如果相同則通過驗(yàn)證歉铝。
- 下次登陸重復(fù)過程3,4
在第4步的時(shí)候凑耻,如果無法匹配太示,不能提示用戶“密碼出現(xiàn)錯(cuò)誤”,或者“用戶名不存在”這樣的錯(cuò)誤香浩,這樣會(huì)讓攻擊者在破解的時(shí)候排除一些錯(cuò)誤選項(xiàng)类缤。而應(yīng)該提示“用戶名或密碼出現(xiàn)錯(cuò)誤”。
加密的算法實(shí)現(xiàn)比較成熟的有SHA256, SHA512, RipeMD, WHIRLPOOL邻吭,在選擇加密方式的時(shí)候直接用就行餐弱。
可能存在的風(fēng)險(xiǎn)
假設(shè)一個(gè)場(chǎng)景,數(shù)據(jù)庫被攻破了,黑客拿到了數(shù)據(jù)庫中的密文膏蚓,他/她如何通過這個(gè)密文得到用戶的密碼呢瓢谢?
字典攻擊和暴力破解
最簡(jiǎn)單的攻擊方式就是猜一個(gè)密碼,用這個(gè)密碼計(jì)算一個(gè)hash值驮瞧,然后和密文進(jìn)行比對(duì)氓扛。如果值一樣,就說明這個(gè)密碼是正確的剧董。這個(gè)最常用的辦法就是字典攻擊和暴力破解幢尚。
應(yīng)對(duì)的技術(shù)
實(shí)踐
前面廢話那么多,這里直接上代碼翅楼,再進(jìn)行說明尉剩。
import hashlib
import binascii
import base64
import os
dk_len = 24
salt_len = 24
password_encrypt_version = 'v1'
iterations = 1000
def pbkdf2(salt, password):
dk = hashlib.pbkdf2_hmac(hashlib.sha1().name, password.encode('utf8'), base64.b16decode(salt.upper()), iterations, dk_len)
return binascii.hexlify(dk)
def encrypt_pass(password):
salt = binascii.hexlify(os.urandom(salt_len))
encrypted_pass = pbkdf2(salt, password)
return '%s:%d:%s:%s' % (password_encrypt_version, iterations, salt, encrypted_pass)
不要使用自己定義的hash函數(shù)
加密方法已經(jīng)有了對(duì)應(yīng)的工業(yè)實(shí)現(xiàn)。自己造輪子在大多數(shù)時(shí)候都會(huì)引入風(fēng)險(xiǎn)毅臊,特別是涉及到敏感信息理茎,這種風(fēng)險(xiǎn)是非常大的。
使用慢hash算法
密碼的安全是由密碼的生命周期和破解周期決定管嬉,使用慢HASH算法皂林,目的是降低破解的速度。這里使用的是pbkdf2算法蚯撩,python中有對(duì)應(yīng)的函數(shù)础倍。
函數(shù)原型
hashlib.pbkdf2_hmac(name, password, salt, rounds, dklen=None)
rounds決定了算法的快慢。這個(gè)值要綜合多方面的考慮胎挎。太小沟启,破解難度降低;太大犹菇,消耗計(jì)算德迹,造成用戶登陸時(shí)等待驗(yàn)證時(shí)間過長(zhǎng),會(huì)降低用戶體驗(yàn)揭芍。這里折衷取1000胳搞。
dklen設(shè)置hash結(jié)果的長(zhǎng)度,這里設(shè)置為24称杨。
加鹽
- 使用偽隨機(jī)數(shù)產(chǎn)生器(CSPRNG)生成鹽值肌毅,這樣更加安全。具體到python姑原,應(yīng)該使用os.urandom這個(gè)方法悬而。
- 不要重復(fù)使用一個(gè)鹽值。每次都應(yīng)該聲稱一個(gè)新的鹽值页衙。
- 鹽值不能取得太短摊滔,過短的鹽值會(huì)降低安全性,這里取24店乐。
鹽值和密文一起存儲(chǔ)
最后密文的形式是
version:rounds:salt:hash_result
不要多次進(jìn)行hash
這毫無意義
驗(yàn)證
在認(rèn)證的時(shí)候艰躺,根據(jù)傳入的用戶名取出存儲(chǔ)的密文,從密文中取出rounds眨八,salt
對(duì)明文再次進(jìn)行計(jì)算腺兴,結(jié)果與存儲(chǔ)的hash結(jié)果進(jìn)行比對(duì),相同即通過驗(yàn)證廉侧。
其他的一些問題
針對(duì)用戶在登陸的時(shí)候页响,明文可能在網(wǎng)絡(luò)傳輸?shù)臅r(shí)候被監(jiān)聽到,可以在http通信時(shí)采用ssl加密的做法段誊。
或者采用其他身份驗(yàn)證方法闰蚕。
參考資料
在進(jìn)行實(shí)踐的時(shí)候參考了這篇文章,這篇文章深入淺出地講解了安全加密连舍,非常值得一讀没陡。