Bootstrap

现代安全密码哈希算法

在过去,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
    }
}
解释:
  • storedSaltStrstoredHashedPassword这两个全局变量来模拟数据库中存储的盐和哈希密码。在实际应用中,这些数据会存储在数据库中。
  • storePassword()方法生成一个随机盐,并使用PBKDF2算法将密码哈希。然后将盐(Base64编码)和哈希密码保存到全局变量中。
  • verifyPassword()方法从存储的盐(Base64解码)和哈希密码开始,重新计算输入密码的哈希值并与存储的哈希密码进行比较。

企业通常会选择 bcrypt、Argon2 或 PBKDF2 等算法来存储用户密码。安全性要求较高的企业倾向于使用 Argon2 或 bcrypt,因为它们具有防止硬件加速暴力破解攻击的能力。此外,企业还会结合 多方面认证来进一步提高安全性。

总结:

  • 对于大多数现代应用,如果不需要对硬件攻击进行特别强的防护,且注重简单易用,bcrypt 是一个不错的选择。它已经足够强大,能够抵御大部分常规的攻击,且具有广泛的支持。经过多年的广泛应用,已被认为非常安全。

  • 对于高安全性要求的应用,特别是需要防止并行硬件破解的情况,Argon2 是目前推荐的最安全的选择。它能够提供更强的防护,尤其是在针对高并发硬件的攻击时。

  • 对于需要兼容性和合规性的应用PBKDF2 可能是最适合的选择,特别是在要求符合某些法规(如 FIPS)的系统中。它已经是一个成熟且广泛支持的标准,但其安全性不如 Argon2 强。

其他安全思考:

盐的长度足够且随机性好,生成相同盐的概率极低,盐碰撞且密码相同的联合事件的发生概率在实际应用中几乎为零,是一个非常低的风险。上面的加密算法通常使用了强随机数生成器,几乎没有重复盐的风险。即使数据库中多个用户有相同的哈希密码也没什么,且有重复的微乎其微。

;