Argon2是 2015 年 7 月密码哈希竞赛的获胜者,这是一种有意占用资源(CPU、内存等)的单向哈希函数。在 Argon2 中,我们可以配置盐的长度、生成的哈希长度、迭代次数、内存成本和 CPU 成本,以控制哈希密码所需的资源。
Argon2 算法有三种变体:
Argon2d,最大限度地抵抗GPU破解攻击,适用于加密货币。Argon2i,优化以抵抗侧信道攻击,适用于密码散列。Argon2id,混合版本,如果不确定,选择这个。Argon2 算法接受五个可配置参数:
阅读此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内部使用Java Native Access (JNA)来调用Argon2 C library。
1.1 这argon2-jvm在 Maven 中央存储库中可用。
-
- <dependency>
- <groupId>de.mkammerergroupId>
- <artifactId>argon2-jvmartifactId>
- <version>2.7version>
- 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.
- *
- * 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(默认)我们在测试计算机上运行以下代码四次,主要规格是 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.1 在 Spring Security 中,我们可以使用Argon2PasswordEncoderArgon2 进行密码散列。的实现Argon2PasswordEncoder需要BouncyCastle。
-
- <dependency>
- <groupId>org.springframework.securitygroupId>
- <artifactId>spring-security-cryptoartifactId>
- <version>5.3.2.RELEASEversion>
- dependency>
-
- <dependency>
- <groupId>commons-logginggroupId>
- <artifactId>commons-loggingartifactId>
- <version>1.2version>
- dependency>
-
- <dependency>
- <groupId>org.bouncycastlegroupId>
- <artifactId>bcpkix-jdk15onartifactId>
- <version>1.65version>
- dependency>
2.2 Argon2PasswordEncoder, 内部使用和之BouncyCastle类的 API来提供 Argon2 散列。Argon2ParametersArgon2BytesGenerator
-
- 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-
- 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);
argon2idArgon2PasswordEncoder不允许更改 Argon2 的变体。
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