概述
編程開發(fā)中拥诡,像用戶登錄注冊這種功能很常見触趴,那么對于用戶密碼處理氮发,我們該選擇什么樣的加密算法呢?在這種場景下冗懦,算法需要滿足下面兩個條件:
- 算法需不可逆爽冕,這樣才能有效防止密碼泄露。
- 算法需相對慢披蕉,可以動態(tài)調(diào)整計算成本颈畸,緩慢是應對暴力破解有效方式。
目前來看有這么幾個算法 PBKDF2没讲、 BCrypt 和 SCrypt 可以滿足眯娱。我們先看下舊的密碼加密方式。
舊的加密
過去密碼加密常用MD5或者SHA爬凑。MD5是早期設計的加密哈希徙缴,它生成哈希速度很快,隨著計算機能力的增強嘁信,出現(xiàn)了被破解的情況于样,所以又有了一些長度增大的哈希函數(shù),如:SHA-1潘靖,SHA-256等穿剖。下面是它們的一些比較:
MD5:速度快生成短哈希(16 字節(jié))。意外碰撞的概率約為:
卦溢。
SHA1:比 md5 慢 20%糊余,生成的哈希比 MD5 長一點(20 字節(jié))。意外碰撞的概率約為:
单寂。
SHA256:最慢贬芥,通常比 md5 慢 60%,并且生成的哈希長(32 字節(jié))凄贩。意外碰撞的概率約為:
誓军。
為了確保安全你可能會選擇目前長度最長的哈希SHA-512,但硬件能力在增強疲扎,或許有一天又會發(fā)現(xiàn)新的漏洞昵时,研究人員又推出較新的版本,新版本的長度也會越來越長椒丧,而且他們也可能會發(fā)布底層算法壹甥,所以我們應該另外尋找更合適的算法。
加鹽操作
密碼安全壶熏,除了要選擇足夠可靠的加密算法外句柠,輸入數(shù)據(jù)的強度也要提升,因為密碼是人設置的,其字符長度組合強度不可能一致溯职,如果直接進行哈希存儲往往會提升爆破的概率精盅,這時我們需要加鹽。
加鹽是密碼學中經(jīng)常提到的概念谜酒,其實就是隨機數(shù)據(jù)叹俏。下面是一個 java 中生成鹽的例子:
public static byte[] generateSalt() {
SecureRandom random = new SecureRandom();
byte[] salt = new byte[16];
random.nextBytes(salt);
return salt;
}
SHA-512 加鹽哈希密碼
public static String sha512(String rawPassword, byte[] salt) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-512");
// 加點鹽
md.update(salt);
return Hex.encodeHexString(md.digest(rawPassword.getBytes(StandardCharsets.UTF_8)));
} catch (GeneralSecurityException ex) {
throw new IllegalStateException("Could not create hash", ex);
}
}
PBKDF2
PBKDF1和PBKDF2是一個密鑰派生函數(shù),其作用就是根據(jù)指定的密碼短語生成加密密鑰僻族。之前在 常見加密算法 提到過粘驰。它雖然不是加密哈希函數(shù),但它仍然適用密碼存儲場景述么,因為它有足夠的安全性蝌数,PBKDF2 函數(shù)計算如下:
-
是偽隨機函數(shù)兩個參數(shù),輸出固定的長度(例如度秘,HMAC)顶伞;
-
是生成派生密鑰的主密碼;
-
是加密鹽剑梳;
-
是迭代次數(shù)枝哄,次數(shù)越多;
-
是派生密鑰的長度阻荒;
-
是生成的派生密鑰。
PRF(HMAC)大致迭代過程众羡,第一次時將 Password 作為密鑰和Salt傳入侨赡,然后再將輸出結(jié)果作為輸入重復完成后面迭代。
HMAC:基于哈希的消息認證碼粱侣,可以使用共享密鑰提供身份驗證羊壹。比如HMAC-SHA256,輸入需要認證的消息和密鑰進行計算齐婴,然后輸出sha256的哈希值油猫。
PBKDF2不同于MD和SHA哈希函數(shù),它通過增加迭代次數(shù)提升了破解難度柠偶,并且還可以根據(jù)情況進行配置情妖,這使得它具有滑動計算成本。
對于MD5和SHA诱担,攻擊者每秒可以猜測數(shù)十億個密碼毡证。而使用 PBKDF2,攻擊者每秒只能進行幾千次猜測(或更少蔫仙,取決于配置)料睛,所以它適用于抗擊暴力攻擊。
2021 年,OWASP 建議對 PBKDF2-HMAC-SHA256 使用 310000 次迭代恤煞,對 PBKDF2-HMAC-SHA512 使用 120000 次迭代
public static String pbkdf2Encode(String rawPassword, byte[] salt) {
try {
int iterations = 310000;
int hashWidth = 256;
PBEKeySpec spec = new PBEKeySpec(rawPassword.toCharArray(), salt, iterations, hashWidth);
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
return Base64.getEncoder().encodeToString(skf.generateSecret(spec).getEncoded());
} catch (GeneralSecurityException ex) {
throw new IllegalStateException("Could not create hash", ex);
}
}
Bcrypt
簡介
bcrypt 是基于 eksblowfish 算法設計的加密哈希函數(shù)屎勘,它最大的特點是:可以動態(tài)調(diào)整工作因子(迭代次數(shù))來調(diào)整計算速度,因此就算以后計算機能力不斷增加居扒,它仍然可以抵抗暴力攻擊概漱。
關(guān)于eksblowfish算法,它是采用分組加密模式苔货,并且支持動態(tài)設定密鑰計算成本(迭代次數(shù))犀概。算法的詳細介紹可查看下面文章:
結(jié)構(gòu)
bcrypt 函數(shù)輸入的密碼字符串不超過 72 個字節(jié)、包含算法標識符夜惭、一個計算成本和一個 16 字節(jié)(128 位)的鹽值姻灶。通過輸入計算得到 24字節(jié)(192位)哈希,最終輸出格式如下:
$2a$12$DQoa2eT/aXFPgIoGwfllHuj4wEA3F71WWT7E/Trez331HGDUSRvXi
\__/\/ \____________________/\_____________________________/
Alg Cost Salt Hash
-
$2a$
: bcrypt 算法標識符或叫版本诈茧; -
12
: 工作因子 (2^12 表示 4096 次迭代) -
DQoa2eT/aXFPgIoGwfllHu
: base64 的鹽值产喉; -
j4wEA3F71WWT7E/Trez331HGDUSRvXi
: 計算后的 Base64 哈希值(24 字節(jié))。
bcrypt 版本
-
$2a$
: 規(guī)定哈希字符串必須是 UTF-8 編碼敢会,必須包含空終止符曾沈。 -
$2y$
: 該版本為修復 2011年6月 PHP 在 bcrypt 實現(xiàn)中的一個錯誤。 -
$2b$
: 該版本為修復 2014年2月 OpenBSD 在 bcrypt 實現(xiàn)中的一個錯誤鸥昏。
2014年2月 在 OpenBSD 的 bcrypt 實現(xiàn)中發(fā)現(xiàn)塞俱,它使用一個無符號的 8 位值來保存密碼的長度。對于長度超過255字節(jié)的密碼吏垮,密碼將在72或長度模256 中的較小者處被截斷障涯,而不是被截斷為72字節(jié)。例如:260 字節(jié)的密碼將被截斷為4個字節(jié)膳汪,而不是截斷為 72 個字節(jié)唯蝶。
實踐
bcrypt 關(guān)鍵在于設定合適的工作因子,理想的工作因子沒有特定的法則遗嗽,主要還是取決于服務器的性能和應用程序上的用戶數(shù)量粘我,一般在安全性和應用性能之間權(quán)衡設定。
假如你的因子設置比較高痹换,雖然可以保證攻擊者難以破解哈希征字,但是登錄驗證也會變慢,嚴重影響用戶體驗晴音,而且也可能被攻擊者通過大量登錄嘗試耗盡服務器的 CPU 來執(zhí)行拒絕服務攻擊柔纵。一般來說計算哈希的時間不應該超過一秒。
我們使用 spring security BCryptPasswordEncoder
看下不同因子下產(chǎn)生哈希的時間锤躁,我電腦配置如下:
處理器:2.2 GHz 四核Intel Core i7
內(nèi)存:16 GB 1600 MHz DDR3
顯卡:Intel Iris Pro 1536 MB
Map<Integer, BCryptPasswordEncoder> encoderMap = new LinkedHashMap<>();
for (int i = 8; i <= 21; i++) {
encoderMap.put(i, new BCryptPasswordEncoder(i));
}
String plainTextPassword = "huhdfJ*!4";
for (int i : encoderMap.keySet()) {
BCryptPasswordEncoder encoder = encoderMap.get(i);
long start = System.currentTimeMillis();
encoder.encode(plainTextPassword);
long end = System.currentTimeMillis();
System.out.println(String.format("bcrypt | cost: %d, time : %dms", i, end - start));
}
bcrypt | cost: 8, time : 39ms
bcrypt | cost: 9, time : 45ms
bcrypt | cost: 10, time : 89ms
bcrypt | cost: 11, time : 195ms
bcrypt | cost: 12, time : 376ms
bcrypt | cost: 13, time : 720ms
bcrypt | cost: 14, time : 1430ms
bcrypt | cost: 15, time : 2809ms
bcrypt | cost: 16, time : 5351ms
bcrypt | cost: 17, time : 10737ms
bcrypt | cost: 18, time : 21417ms
bcrypt | cost: 19, time : 43789ms
bcrypt | cost: 20, time : 88723ms
bcrypt | cost: 21, time : 176704ms
擬合得到以下公式:
BCryptPasswordEncoder
因子范圍在 4-31 搁料,默認是 10或详,我們根據(jù)公式推導一下 31時需要多長時間。
/**
* @param strength the log rounds to use, between 4 and 31
*/
public BCryptPasswordEncoder(int strength) {
this(strength, null);
}
工作因子 31
時大概需要 284
天郭计,所以我們知道使用 bcrypt 可以很容易的擴展哈希計算過程以適應更快的硬件霸琴,為我們留出很大的回旋余地,以防止攻擊者從未來的技術(shù)改進中受益昭伸。
SCrypt
SCrypt 比上面提到的算法出來較晚梧乘,是Colin Percival于 2009 年 3 月創(chuàng)建的基于密碼的密鑰派生函數(shù)。關(guān)于該算法我們需要明白下面兩點:
- 該算法專門設計用于通過需要大量內(nèi)存來執(zhí)行大規(guī)模自定義硬件攻擊庐杨,成本高昂选调。
- 它屬于密鑰派生函數(shù)和上面提到 PBKDF2 屬于同一類別。
Spring security 也實現(xiàn)該算法 SCryptPasswordEncoder
灵份,輸入?yún)?shù)如下:
- CpuCost: 算法的 cpu 成本仁堪。 必須是大于 1 的 2 的冪。默認當前為 16,384 或 2^14)
- MemoryCost: 算法的內(nèi)存成本填渠。默認當前為 8弦聂。
- Parallelization: 算法的并行化當前默認為 1。請注意氛什,該實現(xiàn)當前不利用并行化莺葫。
- KeyLength: 算法的密鑰長度。 當前默認值為 32枪眉。
- SaltLength: 鹽長度捺檬。 當前默認值為 64。
不過也有人提到贸铜,并不建議在生產(chǎn)系統(tǒng)中使用它來存儲密碼欺冀,他的結(jié)論是首先 SCrypt 設計目的是密鑰派生函數(shù)而不是加密哈希,另外它實現(xiàn)上也并不那么完美萨脑。詳細可查看下面文章。
https://blog.ircmaxell.com/2014/03/why-i-dont-recommend-scrypt.html
結(jié)論
我會推薦使用 bcrypt饺饭。為什么是 bcrypt 呢渤早?
密碼存儲這種場景下,將密碼哈希處理是最好的方式瘫俊,第一它本身就是加密哈希函數(shù)鹊杖,其次按照摩爾定律的定義,集成系統(tǒng)上每平方英寸的晶體管數(shù)量大約每 18 個月翻一番扛芽。在 2 年內(nèi)骂蓖,我們可以增加它的工作因子以適應任何變化。
當然這并不是說其它算法不夠安全川尖,你仍然可以選擇其它算法登下。建議優(yōu)先使用 bcrypt,其次是密鑰派生類(PBKDF2 和 SCrypt),最后是哈希+鹽(SHA256(salt))被芳。