盐是密码学中常常提到的观点,实在便是随机数据。下面是一个 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是一个密钥派生函数,其浸染便是根据指定的密码短语天生加密密钥。之前在 口试:请说一下编程中有哪些加密算法? 提到过。它虽然不是加密哈希函数,但它仍旧适用密码存储场景,由于它有足够的安全性,PBKDF2 函数打算如下:

DK = PBKDF2(PRF, Password, Salt, Iterations, HashWidth)
PRF 是伪随机函数两个参数,输出固定的长度(例如,HMAC);Password 是天生派生密钥的主密码;Salt 是加密盐;Iterations 是迭代次数,次数越多;HashWidth 是派生密钥的长度;DK 是天生的派生密钥。PRF(HMAC)大致迭代过程,第一次时将 Password 作为密钥和Salt传入,然后再将输出结果作为输入重复完成后面迭代。
HMAC:基于哈希的认证码,可以利用共享密钥供应身份验证。比如HMAC-SHA256,输入须要认证的和密钥进行打算,然后输出sha256的哈希值。
PBKDF2不同于MD和SHA哈希函数,它通过增加迭代次数提升了破解难度,并且还可以根据情形进行配置,这使得它具有滑动打算本钱。
对付MD5和SHA,攻击者每秒可以预测数十亿个密码。而利用 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 算法设计的加密哈希函数,它最大的特点是:可以动态调度事情因子(迭代次数)来调度打算速率,因此就算往后打算机能力不断增加,它仍旧可以抵抗暴力攻击。
关于eksblowfish算法,它是采取分组加密模式,并且支持动态设定密钥打算本钱(迭代次数)。算法的详细先容可查看下面文章:
https://www.usenix.org/legacy/publications/library/proceedings/usenix99/full_papers/provos/provos_html/node4.html
构造bcrypt 函数输入的密码字符串不超过 72 个字节、包含算法标识符、一个打算本钱和一个 16 字节(128 位)的盐值。通过输入进行打算得到 24字节(192位)哈希,终极输出格式如下:
$2a$12$DQoa2eT/aXFPgIoGwfllHuj4wEA3F71WWT7E/Trez331HGDUSRvXi\__/\/ \____________________/\_____________________________/Alg Cost Salt Hash
$2a$: bcrypt 算法标识符或叫版本;12: 事情因子 (2^12 表示 4096 次迭代)DQoa2eT/aXFPgIoGwfllHu: base64 的盐值;j4wEA3F71WWT7E/Trez331HGDUSRvXi: 打算后的 Base64 哈希值(24 字节)。
bcrypt 版本
$2a$: 规定哈希字符串必须是 UTF-8 编码,必须包含空终止符。$2y$: 该版本为修复 2011年6月 PHP 在 bcrypt 实现中的一个缺点。$2b$: 该版本为修复 2014年2月 OpenBSD 在 bcrypt 实现中的一个缺点。2014年2月 在 OpenBSD 的 bcrypt 实现中创造,它利用一个无符号的 8 位值来保存密码的长度。对付长度超过255字节的密码,密码将在72或长度模256 中的较小者处被截断,而不是被截断为72字节。例如:260 字节的密码将被截断为4个字节,而不是截断为 72 个字节。
实践bcrypt 关键在于设定得当的事情因子,空想的事情因子没有特定的法则,紧张还是取决于做事器的性能和运用程序上的用户数量,一样平常在安全性和运用性能之间权衡设定。
如果你的因子设置比较高,虽然可以担保攻击者难以破解哈希,但是登录验证也会变慢,严重影响用户体验,而且也可能被攻击者通过大量登录考试测验耗尽做事器的 CPU 来实行谢绝做事攻击。一样平常来说打算哈希的韶光不应该超过一秒。
我们利用 spring security BCryptPasswordEncoder 看下不同因子下产生哈希的韶光,我电脑配置如下:
处理器:2.2 GHz 四核Intel Core i7内存: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 : 39msbcrypt | cost: 9, time : 45msbcrypt | cost: 10, time : 89msbcrypt | cost: 11, time : 195msbcrypt | cost: 12, time : 376msbcrypt | cost: 13, time : 720msbcrypt | cost: 14, time : 1430msbcrypt | cost: 15, time : 2809msbcrypt | cost: 16, time : 5351msbcrypt | cost: 17, time : 10737msbcrypt | cost: 18, time : 21417msbcrypt | cost: 19, time : 43789msbcrypt | cost: 20, time : 88723msbcrypt | cost: 21, time : 176704ms
拟合得到以下公式:
BCryptPasswordEncoder 因子范围在 4-31 ,默认是 10,我们根据公式推导一下 31时须要多永劫光。
/ @param strength the log rounds to use, between 4 and 31 / public BCryptPasswordEncoder(int strength) { this(strength, null); }
事情因子 31 时大概须要 284 天,以是我们知道利用 bcrypt 可以很随意马虎的扩展哈希打算过程以适应更快的硬件,为我们留出很大的回旋余地,以防止攻击者从未来的技能改进中受益。
SCryptSCrypt 比上面提到的算法出来较晚,是Colin Percival于 2009 年 3 月创建的基于密码的密钥派生函数。关于该算法我们须要明白下面两点:
该算法专门设计用于通过须要大量内存来实行大规模自定义硬件攻击,本钱高昂。它属于密钥派生函数和上面提到 PBKDF2 属于同一种别。Spring security 也实现该算法 SCryptPasswordEncoder ,输入参数如下:
CpuCost: 算法的 cpu 本钱。 必须是大于 1 的 2 的幂。默认当前为 16,384 或 2^14)MemoryCost: 算法的内存本钱。默认当前为 8。Parallelization: 算法的并行化当前默认为 1。请把稳,该实现当前不利用并行化。KeyLength: 算法的密钥长度。 当前默认值为 32。SaltLength: 盐长度。 当前默认值为 64。不过也有人提到,并不建议在生产系统中利用它来存储密码,他的结论是首先 SCrypt 设计目的是密钥派生函数而不是加密哈希,其余它实现上也并不那么完美。详细可查看下面文章。
https://blog.ircmaxell.com/2014/03/why-i-dont-recommend-scrypt.html
结论我会推举利用 bcrypt。为什么是 bcrypt 呢?
密码存储这种场景下,将密码哈希处理是最好的办法,第一它本身便是加密哈希函数,其次按照摩尔定律的定义,集成系统上每平方英寸的晶体管数量大约每 18 个月翻一番。在 2 年内,我们可以增加它的事情因子以适应任何变革。
当然这并不是说其它算法不足安全,你仍旧可以选择其它算法。建议优先利用 bcrypt,其次是密钥派生类(PBKDF2 和 SCrypt),末了是哈希+盐(SHA256(salt))。