譯者 | baron
審校 | 孫淑娟 梁策
網(wǎng)絡(luò)安全問題日益嚴(yán)重,即使是大型知名企業(yè)也面臨敏感用戶數(shù)據(jù)泄露的問題。這些問題可能包括對(duì)數(shù)據(jù)庫的未經(jīng)授權(quán)的訪問以及日志的泄露等等。此外,我們也經(jīng)常遇到零日漏洞攻擊(Zero-Day Vulnerabilities),所有這些都對(duì)用戶自身安全和企業(yè)聲譽(yù)產(chǎn)生了負(fù)面影響。本文將介紹如何使用密碼認(rèn)證來實(shí)現(xiàn)用戶認(rèn)證的數(shù)據(jù)存儲(chǔ)。
一、身份驗(yàn)證
身份驗(yàn)證是用戶確認(rèn)他是所提供標(biāo)識(shí)符的所有者的過程。最明顯和人們最熟悉的身份驗(yàn)證過程是密碼身份驗(yàn)證。用戶進(jìn)入登錄頁面,輸入用戶名和密碼,然后登錄。下文將展示如何在服務(wù)器上實(shí)現(xiàn)身份驗(yàn)證。
認(rèn)證過程可以用一張圖表示:
服務(wù)端收到請(qǐng)求后,服務(wù)器將使用存儲(chǔ)在數(shù)據(jù)庫中的值(在注冊(cè)期間保存的)檢查用戶的數(shù)據(jù),并判斷用戶是否可以通過身份驗(yàn)證。如果檢查成功,通常會(huì)在服務(wù)器上創(chuàng)建一個(gè)會(huì)話,并將其標(biāo)識(shí)符作為 Cookie 在響應(yīng)中返回。
那么,用戶注冊(cè)時(shí)如何保存認(rèn)證數(shù)據(jù)呢?
1.將密碼存儲(chǔ)為純文本
在這種情況下,數(shù)據(jù)庫中的數(shù)據(jù)將作為開放數(shù)據(jù)存儲(chǔ)。任何有權(quán)訪問數(shù)據(jù)庫的人都可以獲取用戶的密碼,比如數(shù)據(jù)庫管理員、支持人員或開發(fā)人員。此外,系統(tǒng)中始終存在漏洞風(fēng)險(xiǎn),可能允許入侵者訪問數(shù)據(jù)庫且進(jìn)行下載和轉(zhuǎn)存。
理想情況下,每項(xiàng)服務(wù)都應(yīng)有其唯一的密碼,這樣就可以避免在服務(wù)中泄露身份驗(yàn)證數(shù)據(jù)的風(fēng)險(xiǎn)。但由于我們使用的服務(wù)如此之多,記住所有密碼是不可能的。一種解決方案是密碼管理器,但使用的人很少,并且用戶傾向于能隨處使用的一個(gè)或多個(gè)密碼。當(dāng)一項(xiàng)服務(wù)的數(shù)據(jù)泄露,使用該密碼的其他服務(wù)也會(huì)受到影響,因此強(qiáng)烈建議不要以純文本形式保存密碼,從而保護(hù)用戶免受此類問題影響。
2.密碼哈希
哈希算法是根據(jù)用戶密碼計(jì)算數(shù)字摘要的特定函數(shù)。該函數(shù)的工作原理是可以足夠快地從密碼中獲取哈希值,而無法在足夠的時(shí)間內(nèi)完成反向轉(zhuǎn)換。哈希函數(shù)有MD5、SHA-1、SHA-256 、SH3-512等。使用這些函數(shù),我們保存到數(shù)據(jù)庫中的不是密碼本身,而是使用哈希函數(shù)從密碼中計(jì)算出來的數(shù)值摘要值。例如,在 Java 中,使用如下所示操作獲取密碼的哈希值:
String password = "pa$$word";
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(password.getBytes());
String hash = new BigInteger(1, digest).toString(16);
作為轉(zhuǎn)換的結(jié)果,我們將得到以下值,可以將其保存在數(shù)據(jù)庫中:
6a158d9847a80e99511b2a7866233e404b305fdb7c953a30deb65300a57a0655
這個(gè)變體已經(jīng)好很多了,但它仍有缺點(diǎn)。其中之一是具有相同密碼的用戶將具有相同的哈希值。如果入侵者獲得對(duì)數(shù)據(jù)庫的訪問權(quán),他就可以根據(jù)自己的目的使用數(shù)據(jù),同時(shí)暴力破解密碼的可能性也相當(dāng)危險(xiǎn)。你可以使用流行的密碼和哈希來創(chuàng)建數(shù)據(jù)庫(或使用現(xiàn)有數(shù)據(jù)庫),因此可以快速恢復(fù)用戶密碼的值。這也是不推薦這一選項(xiàng)的原因。
3.使用唯一鹽(Salt)的密碼哈希
針對(duì)前一個(gè)解決方案的痛點(diǎn),我們可以使用每個(gè)用戶唯一的鹽。鹽是與密碼連接的隨機(jī)值,并從結(jié)果中獲取哈希函數(shù)。
String password = "pa$$word";
String salt = "b0f57dccf7133f7ef3acb09641e5f7a3";
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest((password + salt).getBytes());
String hash = new BigInteger(1, digest).toString(16)
隨機(jī)鹽可以像這樣生成:
Random random = new SecureRandom();
byte[] saltBytes = new byte[16];
random.nextBytes(saltBytes);
String salt = new BigInteger(1, saltBytes).toString(16);
這樣,我們一次解決了幾個(gè)問題。首先,具有相同密碼的用戶將具有不同的鹽值,因此哈希函數(shù)的值也不同。因?yàn)闊o法應(yīng)用預(yù)先計(jì)算的哈希表,入侵者獲取密碼將更加困難。
4.特殊算法 PBKDF2、BCrypt、SCrypt
最好的選擇是使用為散列密碼開發(fā)的特殊算法。這些算法是自適應(yīng)的,可以有意讓計(jì)算時(shí)間放慢,以使暴力攻擊更加困難。
我們以 BCrypt 算法(實(shí)現(xiàn)是 Spring Security 的一部分)為例:
String password = "pa$$word";
String salt = BCrypt.gensalt();
String hash = BCrypt.hashpw(password, salt);
結(jié)果是以下哈希值:
$2a$10$alXdzX7lkEp52xiKS7YfuelpoFqz6AsvyBwIEz/BbWghdkmwGqYoy
$2a$ - the hash algorithm identifier
10 - number of hashing rounds (2^10 = 1024)
alXdzX7lkEp52xiKS7Yfue - salt
lpoFqz6AsvyBwIEz/BbWghdkmwGqYoy - hash
為了計(jì)算這個(gè)函數(shù),我們使用了1024輪哈希。隨著時(shí)間的推移和計(jì)算能力的增長,我們可以增加這個(gè)值來保持計(jì)算的復(fù)雜性。
在對(duì)用戶進(jìn)行身份驗(yàn)證時(shí),只需調(diào)用檢查發(fā)送的密碼的方法以及存儲(chǔ)在數(shù)據(jù)庫中的哈希值:
//plaintext - sent by the user
//hash - loaded from DB
boolean checkpw = BCrypt.checkpw(plaintext, hash);
5.使用 PAKE
通過 PAKE(密碼驗(yàn)證密鑰協(xié)議)系列協(xié)議,可以在不傳輸密碼的情況下驗(yàn)證用戶是否知道密碼。該算法是專門設(shè)計(jì)的,因此密碼本身不被傳輸,而是使用密碼的一些計(jì)算的值被傳輸。如果攻擊者獲得了數(shù)據(jù)庫的訪問權(quán)限,哪怕可以竊聽客戶端和服務(wù)器之間的通道,他也無法恢復(fù)原始密碼值。我們以 PAKE 的 SRP-6a 為例。
登錄過程分兩步進(jìn)行,經(jīng)過數(shù)學(xué)計(jì)算,可以證明客戶端輸入的密碼,而無需傳輸密碼。該協(xié)議規(guī)范在RFC5054中有詳細(xì)描述。為了保存認(rèn)證數(shù)據(jù),需要計(jì)算v和s的值,其中v是verifier,s是salt 。計(jì)算salt的方法我們已經(jīng)知道,計(jì)算verifier的方法則是:
x = H(salt, password)
v = g^x (mod N)
H- 哈希函數(shù)(SHA-1、SHA256 等)。
g, N- 可以從RFC5054.Appendix A中選擇的常量。需要注意的是,選擇的常量和哈希函數(shù)在服務(wù)器和客戶端上必須相同。
salt 和verifier 值可以在客戶端和服務(wù)器上計(jì)算。如果這些值是在客戶端計(jì)算的,我們根本不會(huì)在通信通道上傳輸密碼,但我們也無法檢查服務(wù)器上的密碼策略(長度、通配符數(shù)量等)。因此,這些檢查也需要傳輸?shù)娇蛻舳恕?/p>
例如,你可以使用Nimbus SRP 庫:
String password = "pa$$word";
SRP6CryptoParams config = SRP6CryptoParams.getInstance(256, "SHA-1");
SRP6VerifierGenerator verifierGenerator = new SRP6VerifierGenerator(config);
byte[] saltBytes = verifierGenerator.generateRandomSalt(16);
String verifier = verifierGenerator.generateVerifier(saltBytes, password.getBytes()).toString(16);
String salt = new BigInteger(saltBytes).toString(16);
結(jié)果:
salt: 6bb9db1c839bdc59ecbcd0ee12488462
verifier: f28aed4372b1312ccdd6e281c7270be503bac99bff845c41da8189eadf9e4497
這些值必須保存在數(shù)據(jù)庫中,并在以后的客戶端身份驗(yàn)證過程中使用。該協(xié)議最大的優(yōu)點(diǎn)是密碼不會(huì)以任何方式傳輸?shù)椒?wù)器,并且無法從verifier值中恢復(fù)原始密碼。此外,verifier僅在注冊(cè)期間傳輸(如果在客戶端計(jì)算)并且僅用于身份驗(yàn)證期間的計(jì)算。該協(xié)議本身可以抵抗 MITM 攻擊,這意味著如果有人意外啟用了服務(wù)器上所有用戶請(qǐng)求的日志記錄,并且這些日志隨后被泄露,密碼也不會(huì)泄露。此數(shù)據(jù)在每個(gè)會(huì)話中計(jì)算,不能用于重新輸入。
二、結(jié)論
正確使用現(xiàn)代用戶身份驗(yàn)證方法可以大大降低身份驗(yàn)證數(shù)據(jù)泄露的可能,但身份驗(yàn)證只是網(wǎng)絡(luò)安全領(lǐng)域的一個(gè)側(cè)面。除此之外,日志請(qǐng)求和日志存儲(chǔ)也是值得人們關(guān)注的問題。
原文鏈接:https://dzone.com/articles/password-authentication-how-to-do-it-correctly
譯者介紹
baron,51CTO社區(qū)編輯,具有九年手機(jī)安全/SOC底層安全開發(fā)經(jīng)驗(yàn),擅長TrustZone/TEE安全產(chǎn)品的設(shè)計(jì)和開發(fā)。