ChatGPT解决这个技术问题 Extra ChatGPT

如何在 Java 中散列密码?

我需要散列密码以存储在数据库中。我怎样才能在 Java 中做到这一点?

我希望获取纯文本密码,添加随机盐,然后将盐和散列密码存储在数据库中。

然后当用户想要登录时,我可以获取他们提交的密码,从他们的帐户信息中添加随机盐,对其进行哈希处理,看看它是否等同于存储的哈希密码和他们的帐户信息。

@YGL 这实际上不是现在的重组,因为 GPU 攻击如此便宜,SHA 系列实际上是密码散列(太快)的一个非常糟糕的选择,即使使用盐也是如此。使用 bcrypt、scrypt 或 PBKDF2
为什么这个问题被关闭了?这是一个真正的工程问题,答案是无价的。 OP 不是要图书馆,而是要问如何解决工程问题。
太棒了。这个问题有 52 人赞成,有人决定将其作为“离题”关闭。
是的,我之前曾在 Meta 上发布过关于关闭问题的帖子,不过被打得很惨。
这个问题应该重新打开。这是一个关于如何编写程序来解决所描述的问题(密码验证)的问题,并带有简短的代码解决方案。看到触发词“图书馆”并不能证明反射性地结束一个问题。他不是在要求图书馆推荐,而是在问如何对密码进行哈希处理。编辑:在那里,修复它。

e
erickson

您实际上可以使用 Java 运行时内置的工具来执行此操作。 Java 6 中的 SunJCE 支持 PBKDF2,这是一个很好的密码散列算法。

byte[] salt = new byte[16];
random.nextBytes(salt);
KeySpec spec = new PBEKeySpec("password".toCharArray(), salt, 65536, 128);
SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
byte[] hash = f.generateSecret(spec).getEncoded();
Base64.Encoder enc = Base64.getEncoder();
System.out.printf("salt: %s%n", enc.encodeToString(salt));
System.out.printf("hash: %s%n", enc.encodeToString(hash));

这是一个可用于 PBKDF2 密码身份验证的实用程序类:

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

/**
 * Hash passwords for storage, and test passwords against password tokens.
 * 
 * Instances of this class can be used concurrently by multiple threads.
 *  
 * @author erickson
 * @see <a href="http://stackoverflow.com/a/2861125/3474">StackOverflow</a>
 */
public final class PasswordAuthentication
{

  /**
   * Each token produced by this class uses this identifier as a prefix.
   */
  public static final String ID = "$31$";

  /**
   * The minimum recommended cost, used by default
   */
  public static final int DEFAULT_COST = 16;

  private static final String ALGORITHM = "PBKDF2WithHmacSHA1";

  private static final int SIZE = 128;

  private static final Pattern layout = Pattern.compile("\\$31\\$(\\d\\d?)\\$(.{43})");

  private final SecureRandom random;

  private final int cost;

  public PasswordAuthentication()
  {
    this(DEFAULT_COST);
  }

  /**
   * Create a password manager with a specified cost
   * 
   * @param cost the exponential computational cost of hashing a password, 0 to 30
   */
  public PasswordAuthentication(int cost)
  {
    iterations(cost); /* Validate cost */
    this.cost = cost;
    this.random = new SecureRandom();
  }

  private static int iterations(int cost)
  {
    if ((cost < 0) || (cost > 30))
      throw new IllegalArgumentException("cost: " + cost);
    return 1 << cost;
  }

  /**
   * Hash a password for storage.
   * 
   * @return a secure authentication token to be stored for later authentication 
   */
  public String hash(char[] password)
  {
    byte[] salt = new byte[SIZE / 8];
    random.nextBytes(salt);
    byte[] dk = pbkdf2(password, salt, 1 << cost);
    byte[] hash = new byte[salt.length + dk.length];
    System.arraycopy(salt, 0, hash, 0, salt.length);
    System.arraycopy(dk, 0, hash, salt.length, dk.length);
    Base64.Encoder enc = Base64.getUrlEncoder().withoutPadding();
    return ID + cost + '$' + enc.encodeToString(hash);
  }

  /**
   * Authenticate with a password and a stored password token.
   * 
   * @return true if the password and token match
   */
  public boolean authenticate(char[] password, String token)
  {
    Matcher m = layout.matcher(token);
    if (!m.matches())
      throw new IllegalArgumentException("Invalid token format");
    int iterations = iterations(Integer.parseInt(m.group(1)));
    byte[] hash = Base64.getUrlDecoder().decode(m.group(2));
    byte[] salt = Arrays.copyOfRange(hash, 0, SIZE / 8);
    byte[] check = pbkdf2(password, salt, iterations);
    int zero = 0;
    for (int idx = 0; idx < check.length; ++idx)
      zero |= hash[salt.length + idx] ^ check[idx];
    return zero == 0;
  }

  private static byte[] pbkdf2(char[] password, byte[] salt, int iterations)
  {
    KeySpec spec = new PBEKeySpec(password, salt, iterations, SIZE);
    try {
      SecretKeyFactory f = SecretKeyFactory.getInstance(ALGORITHM);
      return f.generateSecret(spec).getEncoded();
    }
    catch (NoSuchAlgorithmException ex) {
      throw new IllegalStateException("Missing algorithm: " + ALGORITHM, ex);
    }
    catch (InvalidKeySpecException ex) {
      throw new IllegalStateException("Invalid SecretKeyFactory", ex);
    }
  }

  /**
   * Hash a password in an immutable {@code String}. 
   * 
   * <p>Passwords should be stored in a {@code char[]} so that it can be filled 
   * with zeros after use instead of lingering on the heap and elsewhere.
   * 
   * @deprecated Use {@link #hash(char[])} instead
   */
  @Deprecated
  public String hash(String password)
  {
    return hash(password.toCharArray());
  }

  /**
   * Authenticate with a password in an immutable {@code String} and a stored 
   * password token. 
   * 
   * @deprecated Use {@link #authenticate(char[],String)} instead.
   * @see #hash(String)
   */
  @Deprecated
  public boolean authenticate(String password, String token)
  {
    return authenticate(password.toCharArray(), token);
  }

}

您可能需要对使用 BigInteger 的字节到十六进制转换有点警惕:前导零被删除。这对于快速调试来说是可以的,但是由于这种影响,我在生产代码中看到了错误。
@thomas-pornin 强调了为什么我们需要一个库,而不是一个几乎就在那里的代码块。可怕的是,接受的答案没有回答关于如此重要主题的问题。
使用从 Java 8 开始的算法 PBKDF2WithHmacSHA512。它有点强。
Note, existing algs are not deleted in later versions: java_4:PBEWithMD5AndDES,DESede,DES java_5/6/7:PBKDF2WithHmacSHA1,PBE (only in Java 5),PBEWithSHA1AndRC2_40,PBEWithSHA1And,PBEWithMD5AndTriple java_8:PBEWithHmacSHA224AndAES_128, PBEWithHmacSHA384AndAES_128, PBEWithHmacSHA512AndAES_128, RC4_40, PBKDF2WithHmacSHA256 , PBEWithHmacSHA1AndAES_128, RC4_128, PBKDF2WithHmacSHA224, PBEWithHmacSHA256AndAES_256, RC2_128, PBEWithHmacSHA224AndAES_256, PBEWithHmacSHA384AndAES_256,PBEWithHmacSHA512AndAES_256,PBKDF2WithHmacSHA512,PBEWithHmacSHA256AndAES_128, PBKDF2WithHmacSHA384,PBEWithHmacSHA1AndAES_256
@TheTosters 是的,incorrect 密码的执行时间会更长;更具体地说,错误密码与正确密码所用的时间相同。它阻止了 timing attacks,,尽管我承认在这种情况下我想不出一种实用的方法来利用这种漏洞。但你不会偷工减料。仅仅因为我看不到它,并不意味着更狡猾的头脑不会。
M
Michael Borgwardt

BCrypt 是一个非常好的库,其中有一个Java port


B
BuZZ-dEE

您可以使用 Spring Security Crypto(只有 2 optional compile dependencies),它支持 PBKDF2BCryptSCryptArgon2 密码加密。

Argon2PasswordEncoder argon2PasswordEncoder = new Argon2PasswordEncoder();
String aCryptedPassword = argon2PasswordEncoder.encode("password");
boolean passwordIsValid = argon2PasswordEncoder.matches("password", aCryptedPassword);
SCryptPasswordEncoder sCryptPasswordEncoder = new SCryptPasswordEncoder();
String sCryptedPassword = sCryptPasswordEncoder.encode("password");
boolean passwordIsValid = sCryptPasswordEncoder.matches("password", sCryptedPassword);
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String bCryptedPassword = bCryptPasswordEncoder.encode("password");
boolean passwordIsValid = bCryptPasswordEncoder.matches("password", bCryptedPassword);
Pbkdf2PasswordEncoder pbkdf2PasswordEncoder = new Pbkdf2PasswordEncoder();
String pbkdf2CryptedPassword = pbkdf2PasswordEncoder.encode("password");
boolean passwordIsValid = pbkdf2PasswordEncoder.matches("password", pbkdf2CryptedPassword);

B
Bozho

您可以使用 MessageDigest 计算散列值,但这在安全性方面是错误的。哈希不能用于存储密码,因为它们很容易被破解。

您应该使用另一种算法,例如 bcrypt、PBKDF2 和 scrypt 来存储您的密码。 See here


您如何在登录时散列密码而不将盐存储在数据库中?
使用用户名作为盐并不是一个致命的缺陷,但它远不如使用来自加密 RNG 的盐好。将盐存储在数据库中绝对没有问题。盐不是秘密。
用户名和电子邮件不会也存储在数据库中吗?
@ZZ Coder,@erickson 正确,我以某种方式假设它将是所有密码的一种盐,这将导致一个易于计算的彩虹表。
使用用户名(或其他 ID,如电子邮件)作为盐的一个问题是,如果不让用户也设置新密码,您就无法更改 ID。
l
laz

您可以使用 OWASP 所描述的 Shiro 库(以前称为 JSecurityimplementation

看起来 JASYPT 库也有一个 similar utility


这实际上是我正在使用的。但是因为我们决定不使用 Shiro,所以有人担心必须为一个包包含整个 Shiro 库效率低下。
我不知道仅由密码散列实用程序组成的库。如果依赖关系是一个问题,你可能最好自己滚动。埃里克森的答案对我来说看起来不错。或者,如果您希望以安全的方式使用 SHA,则只需从我引用的 OWASP 链接中复制代码。
C
Community

除了其他答案中提到的 bcrypt 和 PBKDF2 之外,我建议您查看 scrypt

不推荐使用 MD5 和 SHA-1,因为它们相对较快,因此使用“每小时租金”分布式计算(例如 EC2)或现代高端 GPU,可以使用暴力/字典攻击以相对较低的成本和合理的方式“破解”密码时间。

如果您必须使用它们,那么至少将算法迭代预定义的大量时间(1000+)。

请参阅此处了解更多信息:https://security.stackexchange.com/questions/211/how-to-securely-hash-passwords

在这里:http://codahale.com/how-to-safely-store-a-password/(批评 SHA 系列、MD5 等用于密码散列)

在这里:http://www.unlimitednovelty.com/2012/03/dont-use-bcrypt.html(批评bcrypt并推荐scrypt和PBKDF2)


C
Community

完全同意 Erickson 的观点,即 PBKDF2 就是答案。

如果您没有该选项,或者只需要使用哈希,则 Apache Commons DigestUtils 比正确获取 JCE 代码要容易得多:https://commons.apache.org/proper/commons-codec/apidocs/org/apache/commons/codec/digest/DigestUtils.html

如果您使用哈希,请使用 sha256 或 sha512。此页面对密码处理和散列有很好的建议(注意它不建议对密码处理进行散列处理):http://www.daemonology.net/blog/2009-06-11-cryptographic-right-answers.html


值得注意的是,SHA512 并不比 SHA256 好(为此),只是因为数字更大。
Q
Qw3ry

虽然已经提到了 NIST recommendation PBKDF2,但我想指出,有一个从 2013 年到 2015 年运行的公共 password hashing competition。最后,Argon2 被选为推荐的密码哈希函数。

对于您可以使用的原始(本机 C)库,有一个相当广泛采用的 Java binding

在一般用例中,我认为从安全角度来看,如果您选择 PBKDF2 而不是 Argon2,反之亦然。如果您有严格的安全要求,我建议您在评估中考虑 Argon2。

有关密码散列函数安全性的更多信息,请参阅 security.se


@zaph我将答案编辑为更客观。请注意,NIST 建议可能并不总是最佳选择(请参阅 here 示例) - 当然,对于其他地方推荐的任何内容也是如此。因此,我确实认为这个答案为这个问题提供了价值。
u
user1767316

截至 2020 年,Argon2id 或 Argon2i 是 Argon2id 或 Argon2i,但不是其 Spring 实现,在使用中最可靠的密码散列算法,最有可能在任何硬件的情况下优化其强度。

PBKDF2 标准包括块密码 BCRYPT 算法的 CPU 贪婪/计算成本高的特性,并添加了它的 stream cipher 功能。 PBKDF2 被内存指数贪婪的 SCRYPT 淹没,然后被抗侧信道攻击的 Argon2 淹没

Argon2 提供必要的校准工具,以在给定目标散列时间和使用的硬件的情况下找到优化的强度参数。

Argon2i 专门研究内存贪婪散列

Argon2d 专门用于 CPU 贪婪散列

Argon2id 使用这两种方法。

内存贪婪散列有助于防止 GPU 用于破解。

Spring security/Bouncy Castle 的实现没有优化,并且考虑到攻击者可以使用的相对一周。 cf: Spring 文档 Argon2Scrypt

当前的实现使用了 Bouncy castle,它没有利用密码破解者将使用的并行性/优化,因此攻击者和防御者之间存在不必要的不对称。

用于 java 的最可靠的实现是 mkammerer 的实现,

用 C 编写的 official native implementation 的包装 jar/库。

它写得很好,使用简单。

嵌入式版本为 Linux、windows 和 OSX 提供本地构建。

例如,摩根大通在其 tessera 安全项目中使用它来保护其以太坊加密货币实施 Quorum

这是一个例子:

    final char[] password = "a4e9y2tr0ngAnd7on6P১M°RD".toCharArray();
    byte[] salt = new byte[128];
    new SecureRandom().nextBytes(salt);
    final Argon2Advanced argon2 = Argon2Factory.createAdvanced(Argon2Factory.Argon2Types.ARGON2id);
    byte[] hash = argon2.rawHash(10, 1048576, 4, password, salt);

(见tessera

在你的 POM 中声明这个库:

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

或使用 gradle:

compile 'de.mkammerer:argon2-jvm:2.7'

校准可以使用 de.mkammerer.argon2.Argon2Helper#findIterations

SCRYPT 和 Pbkdf2 算法也可以通过编写一些简单的基准来校准,但当前的最小安全迭代值将需要更高的哈希时间。


我只想说,这写得很好,就像你描述的那样开箱即用。谢谢你。
f
fospathi

这里有两个 MD5 散列和其他散列方法的链接:

Javadoc API:https://docs.oracle.com/javase/1.5.0/docs/api/java/security/MessageDigest.html

教程:http://www.twmacinta.com/myjava/fast_md5.php


请记住,对于密码哈希,越慢越好。您应该使用哈希函数的数千次迭代作为“密钥强化”技术。此外,盐是必不可少的。
我的印象是,质量哈希算法的多次迭代会产生与一次迭代大致相同的安全性,因为字节长度仍然相同?
@erickson 最好明确地减慢攻击者的速度。
关于密钥强化:存在盐以使预先计算的哈希不可用。但攻击者不必预先计算。攻击者可以“即时”散列字符串+盐,直到找到正确的。但是如果你为你的哈希迭代数千次,他们将不得不做同样的事情。您的服务器不会受到 10k 次迭代的太大影响,因为它不会经常发生。攻击者将需要 10k 倍的计算能力。
@Simon 今天 MD5 被认为对密码散列毫无用处,因为它可以使用 GPU 蛮力/字典攻击在几秒钟内被破解。见这里:codahale.com/how-to-safely-store-a-password
Z
ZZ Coder

在所有标准哈希方案中,LDAP ssha 是最安全的一种,

http://www.openldap.org/faq/data/cache/347.html

我会按照那里指定的算法并使用 MessageDigest 进行哈希处理。

您需要按照您的建议将盐存储在数据库中。


因为 SSHA 不迭代哈希函数,所以速度太快了。这允许攻击者更快地尝试密码。 Bcrypt、PBBKDF1 和 PBKDF2 等更好的算法使用“密钥强化”技术将攻击者的速度减慢到密码应该过期的程度,然后他们才能暴力破解甚至是 8 个字母的密码空间。
所有这些机制的问题在于您没有获得客户支持。散列密码的问题是您不能支持使用其他算法散列的密码。使用 ssha,至少所有 LDAP 客户端都支持它。
它不是“最安全的”,它只是“非常兼容”。 bcrypt/scrypt 的资源密集度更高。
س
سحنون المالكى

我从 udemy 上的视频中学习并编辑为更强大的随机密码

}

private String pass() {
        String passswet="1234567890zxcvbbnmasdfghjklop[iuytrtewq@#$%^&*" ;

        char icon1;
        char[] t=new char[20];

         int rand1=(int)(Math.random()*6)+38;//to make a random within the range of special characters

            icon1=passswet.charAt(rand1);//will produce char with a special character

        int i=0;
        while( i <11) {

             int rand=(int)(Math.random()*passswet.length());
             //notice (int) as the original value of Math>random() is double

             t[i] =passswet.charAt(rand);

             i++;
                t[10]=icon1;
//to replace the specified item with icon1
         }
        return new String(t);
}






}

我愿意被纠正,但我认为你不应该在散列时使用随机数。这样您的哈希函数就可以保持确定性;也就是说,如果您多次散列一个字符串,您将始终为该字符串返回相同的散列值。