在过去,MD5常常用于密码存储和加密,但由于其被发现容易受到碰撞攻击(即不同的输入可能产生相同的哈希值),现在已经不被推荐用于密码加密。当前,更加安全的密码存储方式使用了更为复杂和安全的哈希算法,并且通常结合了盐(salt)和迭代加密的技术来增强安全性。
目前最常用的安全密码哈希算法包括:
1. bcrypt
- 适用场景:非常适合大多数现代应用,尤其是对性能要求不是特别高的场景。
- 特点:
- 简单易用:bcrypt 提供了非常简洁的 API,易于集成。
- 自动盐生成:bcrypt 自动为每个哈希值生成一个盐(salt),确保相同的密码生成不同的哈希。
- 多次加密:通过设置迭代次数来增加密码哈希的计算复杂度,从而提高安全性。
- 优点:
- 经过多次加密(可调整的迭代次数)使其在面对暴力破解时具有较强的防御能力。
- 适合大多数中小型应用,性能开销相对较低。
- 缺点:
- 相较于 Argon2,它对内存的消耗较少,因此对于基于定制硬件(如 FPGA 或 GPU)进行的大规模并行暴力破解,防护能力相对较弱。
- 常见应用:很多企业和开源项目广泛使用,像是现代的 web 框架和系统(如 Node.js 的
bcrypt
包、Python 的bcrypt
库等)都有支持。
Maven 依赖
<dependency>
<groupId>org.mindrot</groupId>
<artifactId>jbcrypt</artifactId>
<version>0.4</version>
</dependency>
Java 代码实现
import org.mindrot.jbcrypt.BCrypt;
public class BCryptExample {
public static void main(String[] args) {
String password = "SecurePassword12345678";
// 生成盐并对密码进行加密
String hashedPassword = BCrypt.hashpw(password, BCrypt.gensalt(12)); // 12 是工作因子,越大越慢
System.out.println("Hashed password: " + hashedPassword);
// 验证密码
boolean isPasswordCorrect1 = BCrypt.checkpw(password, hashedPassword);
System.out.println("Password match 1: " + isPasswordCorrect1); // true
boolean isPasswordCorrect2 = BCrypt.checkpw("WrongPassword", hashedPassword);
System.out.println("Password match 2: " + isPasswordCorrect2); // false
}
}
解释:
BCrypt.gensalt()
生成一个包含盐的加密哈希。BCrypt.checkpw()
用来验证用户输入的密码是否与存储的哈希密码匹配。
2. Argon2
- 适用场景:非常适合对安全性要求极高的场景,尤其是需要防止大规模硬件破解(如 GPU、FPGA)的应用。
- 特点:
- 内存密集型:Argon2 允许通过配置内存消耗来增加密码哈希计算的复杂度,能够有效防止使用高并行度的硬件进行攻击。
- 可配置性:可以配置迭代次数、内存消耗和并行度,灵活调整安全性与性能之间的平衡。
- 最新标准:Argon2 是目前密码学界推荐的最安全的哈希算法之一,具有更强的抵御攻击的能力。它在 2015 年获得了密码哈希大赛的冠军。
- 优点:
- 高内存消耗使其对 GPU 和专用硬件的抗性更强。
- 可调的内存、迭代次数和并行度让用户可以根据需求选择最合适的配置。
- 缺点:
- 相较于 bcrypt,其性能稍差,尤其是在低端硬件上,内存和计算开销较大。
- 常见应用:越来越多的新系统开始采用 Argon2,尤其是在需要极高安全性的场景下(例如密码管理器、银行系统等)。
Maven 依赖
<dependency>
<groupId>de.mkammerer</groupId>
<artifactId>argon2-jvm</artifactId>
<version>2.11</version>
</dependency>
Java 代码实现
import de.mkammerer.argon2.Argon2;
import de.mkammerer.argon2.Argon2Factory;
public class Argon2Example {
public static void main(String[] args) {
String password = "SecurePassword12345678";
// 创建 Argon2 实例
Argon2 argon2 = Argon2Factory.create();
// 哈希密码
char[] passwordChars = password.toCharArray();
String hashedPassword = argon2.hash(2, 65536, 1, passwordChars); // 2是迭代次数,65536是内存使用,1是并发度
System.out.println("Hashed password: " + hashedPassword);
// 验证密码
boolean isPasswordCorrect1 = argon2.verify(hashedPassword, passwordChars);
System.out.println("Password match 1: " + isPasswordCorrect1); // true
boolean isPasswordCorrect2 = argon2.verify(hashedPassword, "WrongPassword".toCharArray());
System.out.println("Password match 2: " + isPasswordCorrect2); // false
// 清除密码
argon2.wipeArray(passwordChars);
}
}
解释:
argon2.hash()
生成带盐的加密哈希,参数中包括迭代次数、内存使用量和并发度。argon2.verify()
验证密码是否与生成的哈希匹配。
3. PBKDF2
- 适用场景:适合需要兼容性和合规性的系统,尤其是要求符合如 FIPS(美国联邦信息处理标准)等标准的场景。
- 特点:
- 广泛支持:PBKDF2 被广泛支持,包括 Java 标准库和许多操作系统和库中。它通常用于需要兼容性和标准化的系统中。
- 灵活性:支持迭代次数、密钥长度等配置选项,用户可以根据需求调整。
- 优点:
- 由于其标准化的广泛支持,适合要求兼容性的应用,尤其是在合规性要求较高的行业中。
- 可以与现有系统兼容,使用简单且在许多语言和平台中都有实现。
- 缺点:
- 比较简单,且主要基于 CPU 计算,不如 Argon2 在防御硬件破解(例如 GPU 和 FPGA)方面强大。
- 相较于 Argon2,它的内存使用和并行性支持较弱,虽然能通过迭代次数来增加安全性,但对于某些攻击场景(如大规模并行暴力破解),其防御能力较为有限。
- 常见应用:PBKDF2 在一些企业级系统和较老的系统中仍然被使用。
PBKDF2
是 Java 标准库中内置支持的,你可以直接使用 javax.crypto
来实现。
Java 代码实现
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.SecureRandom;
import java.util.Base64;
public class PBKDF2Example {
// 模拟数据库存储的盐和哈希密码
private static String storedSaltStr; // 存储的盐(Base64编码)
private static String storedHashedPassword; // 存储的哈希密码
// 生成盐
public static byte[] generateSalt() {
SecureRandom random = new SecureRandom();
byte[] salt = new byte[16]; // 16字节盐
random.nextBytes(salt);
return salt;
}
// 使用PBKDF2加密密码
public static String hashPassword(String password, byte[] salt) throws Exception {
int iterations = 10000; // 迭代次数
int keyLength = 256; // 哈希输出长度
PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, keyLength);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] hash = factory.generateSecret(spec).getEncoded();
return Base64.getEncoder().encodeToString(hash);
}
// 保存密码和盐到“数据库”
public static void storePassword(String password) throws Exception {
byte[] salt = generateSalt(); // 生成盐
storedSaltStr = Base64.getEncoder().encodeToString(salt); // 盐以Base64编码存储
storedHashedPassword = hashPassword(password, salt); // 哈希密码存储
}
// 验证密码
public static boolean verifyPassword(String password, String storedSaltStr, String storedHashedPassword) throws Exception {
byte[] salt = Base64.getDecoder().decode(storedSaltStr); // 从存储中提取盐
String hashedInputPassword = hashPassword(password, salt); // 使用相同盐和算法生成哈希值
return hashedInputPassword.equals(storedHashedPassword); // 比较哈希值
}
public static void main(String[] args) throws Exception {
// 1. 模拟存储密码到数据库
String password = "SecurePassword12345678";
storePassword(password); // 存储盐和哈希密码到全局变量(模拟数据库)
System.out.println("Salt (Base64): " + storedSaltStr);
System.out.println("Hashed Password: " + storedHashedPassword);
// 2. 验证密码
String inputPassword = "SecurePassword12345678"; // 用户输入的密码
boolean isPasswordCorrect = verifyPassword(inputPassword, storedSaltStr, storedHashedPassword);
System.out.println("Password is correct: " + isPasswordCorrect); // true
// 尝试使用错误密码验证
String incorrectPassword = "WrongPassword";
boolean isIncorrectPasswordCorrect = verifyPassword(incorrectPassword, storedSaltStr, storedHashedPassword);
System.out.println("Incorrect password is correct: " + isIncorrectPasswordCorrect); // false
}
}
解释:
- 用
storedSaltStr
和storedHashedPassword
这两个全局变量来模拟数据库中存储的盐和哈希密码。在实际应用中,这些数据会存储在数据库中。 storePassword()
方法生成一个随机盐,并使用PBKDF2算法将密码哈希。然后将盐(Base64编码)和哈希密码保存到全局变量中。verifyPassword()
方法从存储的盐(Base64解码)和哈希密码开始,重新计算输入密码的哈希值并与存储的哈希密码进行比较。
企业通常会选择 bcrypt、Argon2 或 PBKDF2 等算法来存储用户密码。安全性要求较高的企业倾向于使用 Argon2 或 bcrypt,因为它们具有防止硬件加速暴力破解攻击的能力。此外,企业还会结合 多方面认证来进一步提高安全性。
总结:
-
对于大多数现代应用,如果不需要对硬件攻击进行特别强的防护,且注重简单易用,bcrypt 是一个不错的选择。它已经足够强大,能够抵御大部分常规的攻击,且具有广泛的支持。经过多年的广泛应用,已被认为非常安全。
-
对于高安全性要求的应用,特别是需要防止并行硬件破解的情况,Argon2 是目前推荐的最安全的选择。它能够提供更强的防护,尤其是在针对高并发硬件的攻击时。
-
对于需要兼容性和合规性的应用,PBKDF2 可能是最适合的选择,特别是在要求符合某些法规(如 FIPS)的系统中。它已经是一个成熟且广泛支持的标准,但其安全性不如 Argon2 强。
其他安全思考:
盐的长度足够且随机性好,生成相同盐的概率极低,盐碰撞且密码相同的联合事件的发生概率在实际应用中几乎为零,是一个非常低的风险。上面的加密算法通常使用了强随机数生成器,几乎没有重复盐的风险。即使数据库中多个用户有相同的哈希密码也没什么,且有重复的微乎其微。