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可以每秒执行数十亿次哈希计算。
量子计算,现在说这个还为时过早,但迟早我们会进入量子计算时代,没有人知道量子计算机在哈希速度上能执行多快。
摩尔定律,快进十年,哈希速度将呈指数级增长,很有可能我们可以在一秒钟甚至更快的时间内解码一个MD5
, SHA1
, SHA-256
,SHA-512
或BLAKE2
密码(即使加盐也无济于事)。
简而言之,所有快速散列算法都不适用于密码散列,我们需要一些慢速散列和资源密集型算法,如Bcrypt、Scrypt或Argon2
.
在 Java 中,我们可以使用以下库来执行 Argon2 密码散列。
- argon2-jvm
- Spring Security Argon2PasswordEncoder
1. Java Argon2 密码散列 - argon2-jvm
这个argon2-jvm内部使用Java Native Access (JNA)来调用Argon2 C library。
1.1 这argon2-jvm
在 Maven 中央存储库中可用。
<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 审查主哈希方法;它需要四个参数,迭代次数、内存、并行度和密码来散列。
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 密码散列。
- 变体 =
argon2i
(默认) - Salt = 16 字节,128 位(默认)
- 哈希长度 = 32 字节,256 位(默认)
- 迭代次数 = 10
- 内存 = 65536k, 64M
- 并行度 = 1
我们在测试计算机上运行以下代码四次,主要规格是 AMD 3900X 12 核,16M 内存,使用 Argon2 算法散列密码大约需要 450-500 毫秒。
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 秒。
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 中,我们可以使用Argon2PasswordEncoder
Argon2 进行密码散列。的实现Argon2PasswordEncoder
需要BouncyCastle。
<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 散列。Argon2Parameters
Argon2BytesGenerator
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 密码散列,使用默认输入:
- 变体 =
argon2id
- Salt = 16 字节,128 位
- 哈希长度 = 32 字节,256 位
- 迭代 = 3
- 内存 = 1 << 12, 或 2 ^ 12, 4096k
- 并行度 = 1
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 输入是可配置的。
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);
- 变体 =
argon2id
- Salt = 16 字节,128 位
- 哈希长度 = 32 字节,256 位
- 迭代 = 10
- 内存 = 1 << 16,或 2 ^ 16、65536k、64M
- 并行度 = 1
Argon2PasswordEncoder
不允许更改 Argon2 的变体。
3. 常见问题
3.1 Argon2推荐参数是什么?
除了 16 字节盐和 32 字节密钥长度外,其余参数取决于服务器容量。在生产服务器上运行 Argon2 密码散列,并微调迭代、线程、内存和每次调用可以承受的时间。一般来说,Argon2 认证需要 0.5ms 到 1 秒。
下载源代码
$ git clone GitHub - mkyong/core-java: List of core Java source code
$ cd java-crypto