每当您需要在应用程序中实现密码哈希或散列时,您应该牢记一些最佳实践。

  1. 永远不要自己实现密码哈希算法——改用经过严格审查的开发人员库!密码学是一个复杂的领域,如果你尝试自己实现一个流行的算法,就会出现很多问题。
  2. 随着计算机每年变得越来越强大,密码哈希算法(及其参数)需要调整!现代密码哈希算法依赖于您(作为开发人员)来指定他们在计算哈希时应该使用多少资源,并且随着时间的推移,您将需要更新这些参数。掌握此类情况的唯一方法是紧跟最新的安全新闻和趋势。

在决定如何存储敏感密码时,你应该考虑的第一件事是你是否想自己处理认证和授权问题。

有许多服务可以为你解决这个问题。使用认证和授权服务可以消除你在应用程序中安全存储密码的负担。

有许多商业服务,如Auth0和Okta,使之变得简单。只要你选择的供应商支持OpenID Connect等开放标准,你就能轻松地将它们集成到你的Java应用中(例如Spring Boot)。

此外,你的供应商可能也能支持MFA(多因素认证),以进一步提高你的终端用户的安全性。

如果您 确实 需要自己在 Java 应用程序中存储密码,那么本文适合您。

如果您在应用程序中存储用户名和密码,无论您做什么, 都不要 以纯文本形式存储 密码 

密码哈希算法可将纯文本密码转换为一串数据,您可以安全地存储在数据库中,并且永远不会被反转(您可以将纯文本密码转换为哈希,但不可能将哈希转换为密码,因此命名为“单向”函数)。

密码哈希算法是专为处理密码而设计的单向函数。

您需要一个 强大的密码散列算法 来处理您的用户密码。一个好的密码散列算法会消耗大量的计算能力和内存。在谈论密码哈希算法时:他们使用的计算资源越多越好! 

Argon2id

目前(2022 年)使用的最佳算法是 Argon2id。  Argon2 是一个密钥推导函数,被选为  密码哈希竞赛 的获胜者。它有三个不同的版本:

  • Argon2d: 最大限度地抵抗 GPU 破解攻击 
  • Argon2i: 针对侧信道攻击进行了优化
  • Argon2id: 混合版本

建议使用 Argon2id 版本的密码,因为它可以平衡侧通道和基于 GPU 的攻击。您可以通过提供三个参数来配置 Argon2id 算法:内存大小 (m)、迭代次数 (t) 和并行度 (p)。

目前,建议使用 Argon2id,内存至少为 15 MiB,迭代次数为 2,并行度为 1。该算法已经包括为每个用户自动加盐以防止预先计算的攻击,所以这里不需要做任何特别的事情。

将 Spring Security Crypto 与 Bouncy Castle 一起使用

在 Java 应用程序中实现 Arong2id 的最简单方法是使用 spring-security-crypto 库。尽管它是 Spring 框架的一部分,但您不必使用 Spring 框架的其余部分。

spring-security-crypto 库有一个 Argon2PasswordEncoder 您可以使用的。我个人认为命名有点偏离,因为这在技术上不是编码器而是哈希器。

Spring 库 bouncycastle 用作包含 Argon2 算法的基于 Java 的完整实现的依赖项。

spring-security-crypto 库可以被认为是一个直观的界面 bouncycastle,使其更易于使用。

<dependency>
   <groupId>org.springframework.security</groupId>
   <artifactId>spring-security-crypto</artifactId>
   <version>5.6.2</version>
</dependency>
<dependency>
   <groupId>commons-logging</groupId>
   <artifactId>commons-logging</artifactId>
   <version>1.2</version>
</dependency>
<dependency>
   <groupId>org.bouncycastle</groupId>
   <artifactId>bcpkix-jdk15on</artifactId>
   <version>1.70</version>
</dependency>

默认情况下 Argon2PasswordEncoder 使用 Argon2id 版本,m=4MiB、t=3 和 p=1。尽管迭代次数高于最小值,但您可能仍要考虑更高的内存消耗。

在下面的 Java 示例中,我使用了 m=15Mib、t=2 和 p=1 的最低要求。我使用与盐和哈希长度的默认值相同的值。


Argon2PasswordEncoder encoder = <b>new</b> Argon2PasswordEncoder(32,64,1,15*1024,2);
<b>var</b> myPassword = <font>"ThisIsMyPassword"</font><font>;

<b>var</b> encodedPassword = encoder.encode(myPassword);
System.out.println(encodedPassword);

<b>var</b> validPassword = encoder.matches(myPassword, encodedPassword);
System.out.println(validPassword);
</font>


代码的输出:

$argon2id$v=19$m=15360,t=2,p=1$YpRuuQhW1dHOimAnWD5TRU6Sebitu+fIrmIrenr+YOM$hkEXhhHpu2NUcPwhV4IUQelQdf4I8V+iyFsFiC8BYEisE3oWFv96zYeNA1i/awhaDo1XHz6Pp/1r55SS/I4AIA
True

为 JVM 使用 Argon2 绑定

JVM 库的  Argon2 Binding 是 Spring 库的替代方案。该库由 Moritz Halbritter 创建,使用 JNA 调用原生 C 库。这种方法的优点是它使用了 Argon2 作者的原始实现,并且可能包括性能优势。因此,您需要能够访问系统上的底层 C 库。  

您可以选择使用包管理器自己安装此 C 库,或者依赖包含一组预编译 Argon2 库的库。推荐第一个,原因很明显,它更小。

没有预编译库:


<dependency>
    <groupId>de.mkammerer</groupId>
    <artifactId>argon2-jvm-nolibs</artifactId>
    <version>2.11</version>
</dependency>


使用预编译库:

<dependency>
    <groupId>de.mkammerer</groupId>
    <artifactId>argon2-jvm</artifactId>
    <version>2.11</version>
</dependency>


这个库的使用非常简单。在下面的代码示例中,您可以看到一个与 Spring 示例等效的示例,使用相同的最小参数。


Argon2 argon2 = Argon2Factory.create(Argon2Factory.Argon2Types.ARGON2id, 32, 64);
<b>var</b> myPassword = <font>"ThisIsMyPassword"</font><font>;

<b>var</b> hash = argon2.hash(2,15*1024,1, myPassword.toCharArray());
System.out.println(hash);

<b>var</b> validPassword = argon2.verify(hash, myPassword.toCharArray());
System.out.println(validPassword);
</font>


scrypt

如果你因为任何原因不能使用Argon2id,  scrypt 是一个不错的第二选择。与argon2id类似,scrypt是另一种强大的密码散列算法,它允许你配置各种成本参数来增加它所消耗的资源。

根据  OWASP Password cheat sheet , ,你应该使用一个最小的CPU/内存成本参数(N),一个最小的块大小(r),以及一个并行化参数(p)。下面的选项被认为是你应该使用的最小值。


N=2^16(64 MiB),r=8(1024字节),p=1
N=2^15 (32 MiB), r=8 (1024字节), p=2
N=2^14 (16 MiB), r=8 (1024 字节), p=4
N=2^13 (8 MiB), r=8 (1024 字节), p=8
N=2^12 (4 MiB), r=8 (1024字节), p=15


请注意,以上的scrypt参数列表都会产生同样强大的密码哈希值。上面的选项只是展示了通过为N、r和p参数指定不同的值,你可以让scrypt算法使用更多的CPU或内存资源,这取决于你的偏好。我下面的建议展示了真实世界的用法,你可以简单地复制和粘贴,以达到简便的目的。

spring-security-crypto库支持许多算法,包括SCrypt。你可以按照我们使用Argon2Encoder的方式使用SCryptEncoder。在下面的例子中,我使用了中间的建议,在CPU和内存分配之间提供了一个良好的平衡。


SCryptPasswordEncoder encoder = <b>new</b> SCryptPasswordEncoder(16384, 8, 4, 32, 64);
<b>var</b> myPassword = <font>"ThisIsMyPassword"</font><font>;

<b>var</b> encodedPassword = encoder.encode(myPassword);
System.out.println(encodedPassword);

<b>var</b> validPassword = encoder.matches(myPassword, encodedPassword);
System.out.println(validPassword);
</font>


输出:


e0804$8FQ4x/ntwEz2ZNu8QRyIQJlAXR+gQkiG3WulLMEq/kioVtaFiKE7sZDGgtmqUmwB8OE+f7Eagux9QXG478unLw==$RS1Bz5Uf30dWGxc+vtlkjj7tPnPdgq8YD1V8odhPW4A=
<b>true</b>


使用BCRYPT

如果Argon2id和scrypt不可用,另一个强有力的选择是BCrypt。如果你决定使用  BCrypt ,工作系数参数应该设置为不低于10。

你可以以类似于以前的方式使用spring-security-crypto库。默认的工作系数被设置为10,但我们在下面的例子中把它设置为14(在2022年是一个合理的数字)。


BCryptPasswordEncoder encoder = <b>new</b> BCryptPasswordEncoder(14)。

你设置的工作系数越高,哈希值就越强,但它也会花费更多的CPU资源(和时间!)来完成运行。在我的本地机器上,设置运行BCrypt的工作负荷为10,平均花费约89ms。对于14的工作量,平均是1033ms。这几乎是12倍的时间。这绝不是一个准确的基准,但它确实显示了增加工作因素的影响。