使用 Argon2 的 Java 密码散列

Argon2是 2015 年 7 月密码哈希竞赛的获胜者,这是一种有意占用资源(CPU、内存等)的单向哈希函数。在 Argon2 中,我们可以配置盐的长度、生成的哈希长度、迭代次数、内存成本和 CPU 成本,以控制哈希密码所需的资源。

Argon2 算法有三种变体:

  • Argon2d,最大限度地抵抗GPU破解攻击,适用于加密货币。
  • Argon2i,优化以抵抗侧信道攻击,适用于密码散列。
  • Argon2id,混合版本,如果不确定,选择这个。

Argon2 算法接受五个可配置参数:

  • 盐长度——随机盐的长度,推荐16字节。
  • 密钥长度——生成哈希的长度,推荐 16 字节,但大多数人更喜欢 32 字节。
  • 迭代——迭代次数,影响时间成本。
  • 内存——算法使用的内存量(以千字节为单位,1k = 1024 字节)会影响内存成本。
  • 并行度——算法使用的线程(或通道)数量,影响并行度。

阅读此Argon2 白皮书

为什么需要慢速密码散列?

现代硬件(CPU 和 GPU)越来越好,越来越便宜。消费级 CPU,如 AMD Ryzen Threadripper,每年都在增加内核,例如 AMD 3990X 有 64 个内核,128 个线程。

此外,由于加密货币挖矿的兴起 GPU 不断发展,FPGA或专用ASIC可以每秒执行数十亿次哈希计算

量子计算,现在说这个还为时过早,但迟早我们会进入量子计算时代,没有人知道量子计算机在哈希速度上能执行多快。

摩尔定律,快进十年,哈希速度将呈指数级增长,很有可能我们可以在一秒钟甚至更快的时间内解码一个MD5SHA1SHA-256,SHA-512BLAKE2密码(即使加盐也无济于事)。

简而言之,所有快速散列算法都不适用于密码散列,我们需要一些慢速散列和资源密集型算法,如BcryptScryptArgon2.

在 Java 中,我们可以使用以下库来执行 Argon2 密码散列。

1. Java Argon2 密码散列 - argon2-jvm

这个argon2-jvm内部使用Java Native Access (JNA)来调用Argon2 C library

1.1 这argon2-jvm在 Maven 中央存储库中可用。

pom.xml

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

1.2 默认Argon2Factory.create()返回一个argon2i变体,具有 16 字节的盐和 32 字节的哈希长度。


  // Argon2Types.ARGON2i
  // salt 16 bytes
  // Hash length 32 bytes
  Argon2 argon2 = Argon2Factory.create();

我们可以自定义 Argon2 变体、盐的长度以及生成的哈希的长度。


  // Argon2Types.ARGON2id
  // salt 32 bytes
  // Hash length 64 bytes
  Argon2 argon2 = Argon2Factory.create(
        Argon2Factory.Argon2Types.ARGON2id,
        32,
        64);

1.3 审查主哈希方法;它需要四个参数,迭代次数、内存、并行度和密码来散列。

Argon2.java

package de.mkammerer.argon2;

public interface Argon2 {

    /**
     * Hashes a password.
     * <p>
     * Uses UTF-8 encoding.
     *
     * @param iterations  Number of iterations
     * @param memory      Sets memory usage to x kibibytes
     * @param parallelism Number of threads and compute lanes
     * @param password    Password to hash
     * @return Hashed password.
     */
    String hash(int iterations, int memory, int parallelism, char[] password);

    //...

1.4 此 Java 示例提供以下输入来执行 Argon2 密码散列。

  1. 变体 = argon2i(默认)
  2. Salt = 16 字节,128 位(默认)
  3. 哈希长度 = 32 字节,256 位(默认)
  4. 迭代次数 = 10
  5. 内存 = 65536k, 64M
  6. 并行度 = 1

我们在测试计算机上运行以下代码四次,主要规格是 AMD 3900X 12 核,16M 内存,使用 Argon2 算法散列密码大约需要 450-500 毫秒。

PasswordArgon2Jvm.java

package com.mkyong.crypto.password;

import de.mkammerer.argon2.Argon2;
import de.mkammerer.argon2.Argon2Factory;

import java.time.Instant;
import java.time.temporal.ChronoUnit;

public class PasswordArgon2Jvm {

    public static void main(String[] args) {

        // default argon2i, salt 16 bytes, hash length 32 bytes.
        Argon2 argon2 = Argon2Factory.create();

        char[] password = "Hello World".toCharArray();

        Instant start = Instant.now();  // start timer

        try {
            // iterations = 10
            // memory = 64m
            // parallelism = 1
            String hash = argon2.hash(22, 65536, 1, password);
            System.out.println(hash);

            // argon2 verify hash
            /*if (argon2.verify(hash, password)) {
                System.out.println("Hash matches password.");
            }*/

            //int iterations = Argon2Helper.findIterations(argon2, 1000, 65536, 1);
            //System.out.println(iterations);

        } finally {
            // Wipe confidential data
            argon2.wipeArray(password);
        }

        Instant end = Instant.now();    // end timer

        System.out.println(String.format(
                "Hashing took %s ms",
                ChronoUnit.MILLIS.between(start, end)
        ));

    }

}

输出。输出是 base64 编码的,由 Argon2 变体、Argon2 版本$v、内存成本$m、迭代$t、并行度(通道)$p、16 字节盐和 Argon2 生成的哈希组成。

终端

# 1st time
$argon2i$v=19$m=65536,t=10,p=1$cGjkgKPK111PPp7t2VEQrA$eowEcB27XAH9wSC1oUjaGuW0jA1iQSmaL4cs7W2Vd0k
Hashing took 500 ms

# 2nd time
$argon2i$v=19$m=65536,t=10,p=1$YkDFUQRhJGa0KjEWusHYQQ$t8IPgKRRFCMkb84cU1PB8JlS3aa+hTQfzFmmbz5omnk
Hashing took 489 ms

# 3rd time
$argon2i$v=19$m=65536,t=10,p=1$h2X+NkgqWtnpnfoQq2NDaw$xWqR9NaL7t6SaJJLiXeFtrGFNp4j08FJJIuTXe0oRiI
Hashing took 457 ms

# 4th time
$argon2i$v=19$m=65536,t=10,p=1$/bY1iOq+C8sRpIGcrwb2fQ$d1Ed4lLAeVrxBjFxINEbC4wNl0FZ2lZ2JsNkJjJkODA
Hashing took 507 ms

1.5 我们可以提供不同的输入来增加密码哈希的时间,假设我们希望哈希函数最多需要1秒,给64m和一个线程,我们可以Argon2Helper.findIterations用来找出优化的迭代。


  // 1000 = Time in ms, we want this 1 second
  // 65536 = Memory cost, 64Mb
  // 1 = parallelism
  int iterations = Argon2Helper.findIterations(argon2, 1000, 65536, 1);
  System.out.println(iterations);

输出


22

使用建议的 22 次迭代重新运行程序。现在,Argon2 密码散列大约需要 1 秒。

PasswordArgon2Jvm.java

  String hash = argon2.hash(22, 65536, 1, password);
  System.out.println(hash);

输出

终端

# 1st
$argon2i$v=19$m=65536,t=22,p=1$LqszYnhGhlM6AW3ehXhXmA$hgFiUxZUbgdodrIOUHhUzPdiWecYYFmHdFPQEf6beBc
Hashing took 1004 ms

# 2nd
$argon2i$v=19$m=65536,t=22,p=1$kySDkzqRkEr748trey63Dg$OsKcqvoK/Y5pywATXw0P8RmKeMAzurNsgbGlmnw8Svs
Hashing took 991 ms

2. Java Argon2 密码散列 - Spring Security

2.1 在 Spring Security 中,我们可以使用Argon2PasswordEncoderArgon2 进行密码散列。的实现Argon2PasswordEncoder需要BouncyCastle

pom.xml

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-crypto</artifactId>
        <version>5.3.2.RELEASE</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.65</version>
    </dependency>

2.2 Argon2PasswordEncoder, 内部使用和之BouncyCastle类的 API来提供 Argon2 散列。Argon2ParametersArgon2BytesGenerator

Argon2PasswordEncoder.java

package org.springframework.security.crypto.argon2;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
import org.bouncycastle.crypto.params.Argon2Parameters;
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
import org.springframework.security.crypto.keygen.KeyGenerators;
import org.springframework.security.crypto.password.PasswordEncoder;

public class Argon2PasswordEncoder implements PasswordEncoder {

      private static final int DEFAULT_SALT_LENGTH = 16;
      private static final int DEFAULT_HASH_LENGTH = 32;
      private static final int DEFAULT_PARALLELISM = 1;
      private static final int DEFAULT_MEMORY = 1 << 12;
      private static final int DEFAULT_ITERATIONS = 3;

      //...

      public Argon2PasswordEncoder(int saltLength, int hashLength,
        int parallelism, int memory, int iterations) {

    		this.hashLength = hashLength;
    		this.parallelism = parallelism;
    		this.memory = memory;
    		this.iterations = iterations;

    		this.saltGenerator = KeyGenerators.secureRandom(saltLength);
    	}

    	public Argon2PasswordEncoder() {
    		this(DEFAULT_SALT_LENGTH, DEFAULT_HASH_LENGTH,
          DEFAULT_PARALLELISM, DEFAULT_MEMORY, DEFAULT_ITERATIONS);
    	}

      @Override
    	public String encode(CharSequence rawPassword) {
    		byte[] salt = saltGenerator.generateKey();
    		byte[] hash = new byte[hashLength];

    		Argon2Parameters params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id).
    				withSalt(salt).
    				withParallelism(parallelism).
    				withMemoryAsKB(memory).
    				withIterations(iterations).
    				build();
    		Argon2BytesGenerator generator = new Argon2BytesGenerator();
    		generator.init(params);
    		generator.generateBytes(rawPassword.toString().toCharArray(), hash);

    		return Argon2EncodingUtils.encode(hash, params);
    	}

      //...

2.3 此 Java 示例用于Argon2PasswordEncoder执行 Argon2 密码散列,使用默认输入:

  1. 变体 =argon2id
  2. Salt = 16 字节,128 位
  3. 哈希长度 = 32 字节,256 位
  4. 迭代 = 3
  5. 内存 = 1 << 12, 或 2 ^ 12, 4096k
  6. 并行度 = 1
PasswordArgon2SpringSecurity.java

package com.mkyong.crypto.password;

import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;

import java.time.Instant;
import java.time.temporal.ChronoUnit;

public class PasswordArgon2SpringSecurity {

    public static void main(String[] args) {

        Argon2PasswordEncoder encoder = new Argon2PasswordEncoder();

        String password = "Hello World";

        Instant start = Instant.now();  // start timer

        String hash = encoder.encode(password);
        System.out.println(hash);

        // argon2 verify hash
        /*if (encoder.matches("Hello World", hash)) {
            System.out.println("match");
        }*/

        Instant end = Instant.now();    // end timer

        System.out.println(String.format(
                "Hashing took %s ms",
                ChronoUnit.MILLIS.between(start, end)
        ));

    }

}

输出

终端

# 1st
$argon2id$v=19$m=4096,t=3,p=1$iurr6y6xk2X7X/YVOEQXBg$ti9/be9VgbXtJWpm1hoYyLm8V0wBGr+dxu9X+PFbpZI
Hashing took 176 ms

# 2nd
$argon2id$v=19$m=4096,t=3,p=1$vnOEfUC3oZ3sVBj/yKG/4g$LdVFmw9N5D49tuJiYT0LGZ8YOqYetqz5UzDyku+7PRs
Hashing took 156 ms

2.4 输入是可配置的。

Argon2PasswordEncoder.java

    public Argon2PasswordEncoder(int saltLength, int hashLength, int parallelism, int memory, int iterations) {
  		this.hashLength = hashLength;
  		this.parallelism = parallelism;
  		this.memory = memory;
  		this.iterations = iterations;

  		this.saltGenerator = KeyGenerators.secureRandom(saltLength);
  	}

例如,


  // int saltLength, int hashLength, int parallelism, int memory, int iterations
  Argon2PasswordEncoder encoder = new Argon2PasswordEncoder(16, 32, 1, 65536, 10);
  1. 变体 =argon2id
  2. Salt = 16 字节,128 位
  3. 哈希长度 = 32 字节,256 位
  4. 迭代 = 10
  5. 内存 = 1 << 16,或 2 ^ 16、65536k、64M
  6. 并行度 = 1

Argon2PasswordEncoder不允许更改 Argon2 的变体。

3. 常见问题

3.1 Argon2推荐参数是什么?

除了 16 字节盐和 32 字节密钥长度外,其余参数取决于服务器容量。在生产服务器上运行 Argon2 密码散列,并微调迭代、线程、内存和每次调用可以承受的时间。一般来说,Argon2 认证需要 0.5ms 到 1 秒。

下载源代码

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值