编程

Java 哈希密码

278 2024-06-06 00:37:00

1. 概述

本教程中,我们将讨论密码哈希的重要性。

我们将快速了解它是什么,为什么它很重要,以及在 Java 中实现它的一些安全和不安全的方法。

2. 什么是哈希?

哈希(Hash)是使用称为加密哈希函数的数学函数从给定消息生成字符串或哈希的过程。

虽然有几种哈希函数,但那些为哈希密码量身定制的函数需要具有四个主要属性才能安全:

  1. 它应该是确定性的:由相同的散列函数处理的相同消息应该总是产生相同的散列
  2. 它是不可逆的:从其哈希生成消息是不切实际的
  3. 它具有高熵:对消息的微小更改应该会产生截然不同的散列
  4. 它可以防止冲突:两个不同的消息不应该产生相同的散列

具有所有四个属性的哈希函数是密码哈希的有力候选者,因为它们加在一起大大增加了从哈希中反向工程密码的难度。

不过,密码散列函数也应该很慢。一种快速算法将有助于暴力攻击,在这种攻击中,黑客将试图通过每秒哈希和比较数十亿(或数万亿)个潜在密码来猜测密码。

满足所有这些标准的一些很棒的散列函数是 PBKDF2、BCrypt 和 SCrypt。但首先,让我们看看一些较旧的算法,以及为什么不再推荐它们。

3. 不推荐: MD5

我们的第一个哈希函数是早在 1992 年开发的 MD5 消息摘要算法。

Java 的 MessageDigest 使其易于计算,并且在其他情况下仍然有用。

然而,在过去的几年里,MD5 被发现无法通过第四个密码哈希属性,因为它在计算上很容易产生冲突。最重要的是,MD5 是一种快速算法,因此对暴力攻击毫无用处。

因此,不建议使用 MD5

4. 不推荐: SHA-512

接下来,我们看看 SHA-512,它是安全哈希算法家族的一部分,该家族始于 1993 年的 SHA-0。

4.1. Why SHA-512?

随着计算机功率的增加,以及新漏洞的发现,研究人员就会推出新版本的 SHA。较新版本的长度越来越长,或者有时研究人员会发布基础算法的新版本。

SHA-512 表示第三代算法中最长的密钥。

虽然现在有更安全的  SHA 版本,但 SHA-512 是用 Java 实现的最强版本。

4.2. Java 中的实现

现在,让我们来看看在 Java 中 SHA-512 哈希算法的实现。

首先,我们必须理解盐的概念。简单地说,这是为每个新散列生成的随机序列

通过引入这种随机性,我们增加了哈希的熵,并保护我们的数据库免受被称为彩虹表(rainbow table)的预先编译的哈希列表的影响。

然后,我们的新哈希函数大致变为:

salt <- generate-salt;
hash <- salt + ':' + sha512(salt + password)

4.3. 生成盐

要引入盐,我们使用 java.security 中的 SecureRandom 类:

SecureRandom random = new SecureRandom();
byte[] salt = new byte[16];
random.nextBytes(salt);

然后,使用 MessageDigest 类来使用盐配置 SHA-512 哈希函数:

MessageDigest md = MessageDigest.getInstance("SHA-512");
md.update(salt);

添加了这些内容后,我们现在可以使用 digetst 方法生成哈希密码:

byte[] hashedPassword = md.digest(passwordToHash.getBytes(StandardCharsets.UTF_8));

4.4. 为什么不推荐?

当与盐一起使用时,SHA-512 仍然是一个恰当选择,但也有更强、更慢的选择

此外,我们将介绍的其余选项还有一个重要功能:可配置强度。.

5. PBKDF2, BCrypt 及 SCrypt

PBKDF2、BCrypt 和 SCrypt 是三个推荐的算法。

5.1. 为什么推荐这些算法?

这些算法每一个都很慢,而且每个都有一个出色的功能,即具有可配置的强度。
这意味着,随着计算机强度的增加,我们可以通过改变输入来减慢算法的速度

5.2. Java 中实现 PBKDF2

现在,盐是密码哈希的基本原理,PBKDF2 同样也需要盐:

SecureRandom random = new SecureRandom();
byte[] salt = new byte[16];
random.nextBytes(salt);

接下来,我们将使用 PBKDF2WithHmacSHA1 算法创建 PBEKeySpec 和 SecretKeyFactory

KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 65536, 128);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");

第三个参数(65536)实际上是强度参数。它指示此算法运行的迭代次数,从而增加生成哈希所需的时间。

最后,我们使用 SecretKeyFactory 生成哈希:

byte[] hash = factory.generateSecret(spec).getEncoded();

5.3. Java 中实现 BCrypt 和 SCrypt

BCrypt 和 SCrypt 支持还没有随 Java 一起提供,尽管如此有些 Java 库支持它们。

其中一个库是 Spring Security。

6. 使用 Spring Security 进行密码哈希

尽管 Java 原生支持 PBKDF2 和 SHA 哈希算法,但它不支持 BCrypt 和 SCrypt 算法。

幸运的是,Spring Security 通过 PasswordEncoder 接口提供了对所有这些推荐算法的支持:

  • Pbkdf2PasswordEncoder 提供 PBKDF2
  • BCryptPasswordEncoder 提供 BCrypt 
  • SCryptPasswordEncoder 提供 SCrypt

PBKDF2、BCrypt和SCrypt的密码编码器都支持配置所需的密码哈希强度。

即使没有基于 Spring Security 的应用,我们也可以直接使用这些编码器。或者,如果我们使用 Spring Security 保护我们的网站,那么我们可以通过其 DSL 或依赖注入来配置我们想要的密码编码器。

而且,与我们上面的例子不同,这些加密算法将在内部为我们生成 salt。该算法将 salt 存储在输出哈希中,以供以后验证密码时使用。

7. 结论

因此,我们深入研究了密码哈希;探讨概念及其用途。

在用 Java 进行编码之前,我们已经了解了一些历史哈希函数以及一些当前实现的哈希函数。

最后,我们看到 Spring Security 附带了其密码加密类,实现了一系列不同的哈希函数。