如何在Java中生成SALT以进行加盐哈希?

49

我一直在寻找最接近的答案是:如何生成随机字母数字字符串?

我想按照这个CrackStation教程的工作流程:

存储密码

  1. 使用CSPRNG生成一个长的随机盐。

  2. 将盐前置到密码中,并使用标准加密哈希函数(例如SHA256)进行哈希处理。

  3. 将盐和哈希值都保存在用户数据库记录中。

验证密码

  1. 从数据库中检索用户的盐和哈希值。

  2. 将盐前置到给定的密码中,并使用相同的哈希函数进行哈希处理。

  3. 将给定密码的哈希值与数据库中的哈希值进行比较。如果它们匹配,则密码正确。否则,密码不正确。

我不知道如何生成盐。我想出了如何使用MessageDigest生成哈希值。我尝试使用SecureRandom,但nextByte方法生成乱码。

编辑:我不知道该选择哪个答案,它们对我来说太复杂了,我已经决定使用jBCrypt; jBCript易于使用,背后完成所有复杂的工作。因此,我会让社区投票选出最佳答案。


2
如果你想在Java中轻松加密密码,可以看看 jBcrypt :) - NiziL
@Louie 不用谢 :) 我真的很喜欢这个库,简单而高效,而且 Blowfish 算法非常适合密码加密 :D - NiziL
4
+1 支持使用 jBcrypt。它是众所周知且备受尊重的。除非你确实知道自己在做什么,否则不要编写自己的加密算法,因为很容易出错。请勿改变原意,使内容更通俗易懂。 - Syon
1
jBcrypt看起来是一个不错的库,但对@NiziL有一点纠正: "密码哈希"不是“加密”。加密是指您可以使用密钥将其反转并获取数据(例如数据库或硬盘加密)。 jBcrypt是哈希而不是加密,请注意不要增加这些术语的混淆。 - Mike Ounsworth
1
@MikeOunsworth 我一直不太擅长用词...和安全:P 谢谢你指出这个错误! - NiziL
显示剩余3条评论
4个回答

60

受到这篇帖子那篇帖子的启发,我使用这段代码生成和验证散列盐密码。它只使用了JDK提供的类,没有外部依赖。

流程如下:

  • 使用 getNextSalt 创建一个盐
  • 要验证用户的身份,您需要获取他的密码并使用 hash 方法来生成加盐和哈希过的密码。该方法返回一个 byte[],您可以将其与盐一起保存在数据库中
  • 要认证用户,您需要获取他的密码,从数据库中检索盐和哈希密码,并使用 isExpectedPassword 方法来检查详细信息是否匹配
/**
 * A utility class to hash passwords and check passwords vs hashed values. It uses a combination of hashing and unique
 * salt. The algorithm used is PBKDF2WithHmacSHA1 which, although not the best for hashing password (vs. bcrypt) is
 * still considered robust and <a href="https://security.stackexchange.com/a/6415/12614"> recommended by NIST </a>.
 * The hashed value has 256 bits.
 */
public class Passwords {

  private static final Random RANDOM = new SecureRandom();
  private static final int ITERATIONS = 10000;
  private static final int KEY_LENGTH = 256;

  /**
   * static utility class
   */
  private Passwords() { }

  /**
   * Returns a random salt to be used to hash a password.
   *
   * @return a 16 bytes random salt
   */
  public static byte[] getNextSalt() {
    byte[] salt = new byte[16];
    RANDOM.nextBytes(salt);
    return salt;
  }

  /**
   * Returns a salted and hashed password using the provided hash.<br>
   * Note - side effect: the password is destroyed (the char[] is filled with zeros)
   *
   * @param password the password to be hashed
   * @param salt     a 16 bytes salt, ideally obtained with the getNextSalt method
   *
   * @return the hashed password with a pinch of salt
   */
  public static byte[] hash(char[] password, byte[] salt) {
    PBEKeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_LENGTH);
    Arrays.fill(password, Character.MIN_VALUE);
    try {
      SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
      return skf.generateSecret(spec).getEncoded();
    } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
      throw new AssertionError("Error while hashing a password: " + e.getMessage(), e);
    } finally {
      spec.clearPassword();
    }
  }

  /**
   * Returns true if the given password and salt match the hashed value, false otherwise.<br>
   * Note - side effect: the password is destroyed (the char[] is filled with zeros)
   *
   * @param password     the password to check
   * @param salt         the salt used to hash the password
   * @param expectedHash the expected hashed value of the password
   *
   * @return true if the given password and salt match the hashed value, false otherwise
   */
  public static boolean isExpectedPassword(char[] password, byte[] salt, byte[] expectedHash) {
    byte[] pwdHash = hash(password, salt);
    Arrays.fill(password, Character.MIN_VALUE);
    if (pwdHash.length != expectedHash.length) return false;
    for (int i = 0; i < pwdHash.length; i++) {
      if (pwdHash[i] != expectedHash[i]) return false;
    }
    return true;
  }

  /**
   * Generates a random password of a given length, using letters and digits.
   *
   * @param length the length of the password
   *
   * @return a random password
   */
  public static String generateRandomPassword(int length) {
    StringBuilder sb = new StringBuilder(length);
    for (int i = 0; i < length; i++) {
      int c = RANDOM.nextInt(62);
      if (c <= 9) {
        sb.append(String.valueOf(c));
      } else if (c < 36) {
        sb.append((char) ('a' + c - 10));
      } else {
        sb.append((char) ('A' + c - 36));
      }
    }
    return sb.toString();
  }
}

我应该将字节存储为 SQL 中的 BLOB 吗? - Louis Hong
@Louie 我想这取决于你使用的数据库,但BLOB似乎是合适的。如果你需要建议,你可以就这个具体问题提出一个单独的问题。 - assylias
6
@assylias,首先感谢你!有两个问题:1:2017年这些技术(libsodium / Argon2)还有用吗?2:为什么世上没有一个apache commons库来处理由开发人员常犯的认证问题以及每个需要任何形式身份验证的应用程序呢? :) - Eran Medan
1
@FarhadAbdolhosseini 是的,那是可行的。关于时序攻击,我不确定是否存在时序攻击可能性(是的,使用了哈希值,但我不知道哈希算法是否在恒定时间内运行)。 - assylias
1
@assylias 我认为由于哈希算法的迭代次数很少,所以哈希算法的时间应该可以忽略不计。我的担心是isExpectedPassword在给定返回语句的情况下,对于不同的输出会在不同的时间返回。例如,如果我尝试多个输入并发现它后来返回一个错误,那么我可能能够假设这个输入符合长度条件但在for循环中失败了(例如,如果我知道/假设代码),然后我就可以利用它。但由于它们被哈希了,我只能了解到哈希值,而不能获得有关实际值的信息,因此应该没问题。 - Farhad Abdolhosseini
显示剩余2条评论

14

关于生成salt,您的方法是正确的,即使用随机数。对于这种情况,它可以保护您的系统免受可能的字典攻击。现在,针对第二个问题,您可以考虑使用Base64而非UTF-8编码。以下是一个生成哈希值的示例。我正在使用Apache Common Codecs进行base64编码,您也可以选择其他编码。

public byte[] generateSalt() {
        SecureRandom random = new SecureRandom();
        byte bytes[] = new byte[20];
        random.nextBytes(bytes);
        return bytes;
    }

public String bytetoString(byte[] input) {
        return org.apache.commons.codec.binary.Base64.encodeBase64String(input);
    }

public byte[] getHashWithSalt(String input, HashingTechqniue technique, byte[] salt) throws NoSuchAlgorithmException {
        MessageDigest digest = MessageDigest.getInstance(technique.value);
        digest.reset();
        digest.update(salt);
        byte[] hashedBytes = digest.digest(stringToByte(input));
        return hashedBytes;
    }
public byte[] stringToByte(String input) {
        if (Base64.isBase64(input)) {
            return Base64.decodeBase64(input);

        } else {
            return Base64.encodeBase64(input.getBytes());
        }
    }

这里是一些额外的参考资料,介绍了密码哈希的标准实践直接来自OWASP


你从哪里得到了Base64类? - Louis Hong
你可以从这里下载jar包:http://commons.apache.org/proper/commons-codec/download_codec.cgi - Raunak Agarwal
如果您担心使用外部库,可以使用Java的内部Base64Encoder http://docs.oracle.com/cd/E23507_01/Platform.20073/apidoc/atg/security/Base64Encoder.html - Raunak Agarwal
@Raunak Agarwal:FYI,盐并不能防止字典攻击(使用单词字典猜测密码),它可以防止使用彩虹表的攻击,并且可以防止所有相同的密码同时被破解。此外,Base64和UTF-8并不完全可比。UTF-8是一种字符串编码方案,它是一种用于表示人类可读文本的二进制格式。Base64是一种字节编码方案,它是一种用于表示二进制数据的文本格式。 - Syon
@Raunak Agarwal:恕我直言,两点都不正确。字典攻击可能是预先计算的,也可能不是。彩虹表是一种专门用于预先计算哈希值的存储系统,它可能包含通过字典和/或暴力计算的哈希值,但不一定包含字典数据。盐只能防止预先计算攻击,而字典攻击并不完全属于这一类别。 - Syon
显示剩余7条评论

4

另一个使用SHA-3的版本,我正在使用bouncycastle:

接口:

public interface IPasswords {

    /**
     * Generates a random salt.
     *
     * @return a byte array with a 64 byte length salt.
     */
    byte[] getSalt64();

    /**
     * Generates a random salt
     *
     * @return a byte array with a 32 byte length salt.
     */
    byte[] getSalt32();

    /**
     * Generates a new salt, minimum must be 32 bytes long, 64 bytes even better.
     *
     * @param size the size of the salt
     * @return a random salt.
     */
    byte[] getSalt(final int size);

    /**
     * Generates a new hashed password
     *
     * @param password to be hashed
     * @param salt the randomly generated salt
     * @return a hashed password
     */
    byte[] hash(final String password, final byte[] salt);

    /**
     * Expected password
     *
     * @param password to be verified
     * @param salt the generated salt (coming from database)
     * @param hash the generated hash (coming from database)
     * @return true if password matches, false otherwise
     */
    boolean isExpectedPassword(final String password, final byte[] salt, final byte[] hash);

    /**
     * Generates a random password
     *
     * @param length desired password length
     * @return a random password
     */
    String generateRandomPassword(final int length);
}

实现方式如下:
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.Validate;
import org.apache.log4j.Logger;
import org.bouncycastle.jcajce.provider.digest.SHA3;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

public final class Passwords implements IPasswords, Serializable {

    /*serialVersionUID*/
    private static final long serialVersionUID = 8036397974428641579L;
    private static final Logger LOGGER = Logger.getLogger(Passwords.class);
    private static final Random RANDOM = new SecureRandom();
    private static final int DEFAULT_SIZE = 64;
    private static final char[] symbols;

    static {
            final StringBuilder tmp = new StringBuilder();
            for (char ch = '0'; ch <= '9'; ++ch) {
                    tmp.append(ch);
            }
            for (char ch = 'a'; ch <= 'z'; ++ch) {
                    tmp.append(ch);
            }
            symbols = tmp.toString().toCharArray();
    }

    @Override public byte[] getSalt64() {
            return getSalt(DEFAULT_SIZE);
    }

    @Override public byte[] getSalt32() {
            return getSalt(32);
    }

    @Override public byte[] getSalt(int size) {
            final byte[] salt;
            if (size < 32) {
                    final String message = String.format("Size < 32, using default of: %d", DEFAULT_SIZE);
                    LOGGER.warn(message);
                    salt = new byte[DEFAULT_SIZE];
            } else {
                    salt = new byte[size];
            }
            RANDOM.nextBytes(salt);
            return salt;
    }

    @Override public byte[] hash(String password, byte[] salt) {

            Validate.notNull(password, "Password must not be null");
            Validate.notNull(salt, "Salt must not be null");

            try {
                    final byte[] passwordBytes = password.getBytes("UTF-8");
                    final byte[] all = ArrayUtils.addAll(passwordBytes, salt);
                    SHA3.DigestSHA3 md = new SHA3.Digest512();
                    md.update(all);
                    return md.digest();
            } catch (UnsupportedEncodingException e) {
                    final String message = String
                            .format("Caught UnsupportedEncodingException e: <%s>", e.getMessage());
                    LOGGER.error(message);
            }
            return new byte[0];
    }

    @Override public boolean isExpectedPassword(final String password, final byte[] salt, final byte[] hash) {

            Validate.notNull(password, "Password must not be null");
            Validate.notNull(salt, "Salt must not be null");
            Validate.notNull(hash, "Hash must not be null");

            try {
                    final byte[] passwordBytes = password.getBytes("UTF-8");
                    final byte[] all = ArrayUtils.addAll(passwordBytes, salt);

                    SHA3.DigestSHA3 md = new SHA3.Digest512();
                    md.update(all);
                    final byte[] digest = md.digest();
                    return Arrays.equals(digest, hash);
            }catch(UnsupportedEncodingException e){
                    final String message =
                            String.format("Caught UnsupportedEncodingException e: <%s>", e.getMessage());
                    LOGGER.error(message);
            }
            return false;


    }

    @Override public String generateRandomPassword(final int length) {

            if (length < 1) {
                    throw new IllegalArgumentException("length must be greater than 0");
            }

            final char[] buf = new char[length];
            for (int idx = 0; idx < buf.length; ++idx) {
                    buf[idx] = symbols[RANDOM.nextInt(symbols.length)];
            }
            return shuffle(new String(buf));
    }


    private String shuffle(final String input){
            final List<Character> characters = new ArrayList<Character>();
            for(char c:input.toCharArray()){
                    characters.add(c);
            }
            final StringBuilder output = new StringBuilder(input.length());
            while(characters.size()!=0){
                    int randPicker = (int)(Math.random()*characters.size());
                    output.append(characters.remove(randPicker));
            }
            return output.toString();
    }
}

测试用例:

测试用例:

public class PasswordsTest {

    private static final Logger LOGGER = Logger.getLogger(PasswordsTest.class);

    @Before
    public void setup(){
            BasicConfigurator.configure();
    }

    @Test
    public void testGeSalt() throws Exception {

            IPasswords passwords = new Passwords();
            final byte[] bytes = passwords.getSalt(0);
            int arrayLength = bytes.length;

            assertThat("Expected length is", arrayLength, is(64));
    }

    @Test
    public void testGeSalt32() throws Exception {
            IPasswords passwords = new Passwords();
            final byte[] bytes = passwords.getSalt32();
            int arrayLength = bytes.length;
            assertThat("Expected length is", arrayLength, is(32));
    }

    @Test
    public void testGeSalt64() throws Exception {
            IPasswords passwords = new Passwords();
            final byte[] bytes = passwords.getSalt64();
            int arrayLength = bytes.length;
            assertThat("Expected length is", arrayLength, is(64));
    }

    @Test
    public void testHash() throws Exception {
            IPasswords passwords = new Passwords();
            final byte[] hash = passwords.hash("holacomoestas", passwords.getSalt64());
            assertThat("Array is not null", hash, Matchers.notNullValue());
    }


    @Test
    public void testSHA3() throws UnsupportedEncodingException {
            SHA3.DigestSHA3 md = new SHA3.Digest256();
            md.update("holasa".getBytes("UTF-8"));
            final byte[] digest = md.digest();
             assertThat("expected digest is:",digest,Matchers.notNullValue());
    }

    @Test
    public void testIsExpectedPasswordIncorrect() throws Exception {

            String password = "givemebeer";
            IPasswords passwords = new Passwords();

            final byte[] salt64 = passwords.getSalt64();
            final byte[] hash = passwords.hash(password, salt64);
            //The salt and the hash go to database.

            final boolean isPasswordCorrect = passwords.isExpectedPassword("jfjdsjfsd", salt64, hash);

            assertThat("Password is not correct", isPasswordCorrect, is(false));

    }

    @Test
    public void testIsExpectedPasswordCorrect() throws Exception {
            String password = "givemebeer";
            IPasswords passwords = new Passwords();
            final byte[] salt64 = passwords.getSalt64();
            final byte[] hash = passwords.hash(password, salt64);
            //The salt and the hash go to database.
            final boolean isPasswordCorrect = passwords.isExpectedPassword("givemebeer", salt64, hash);
            assertThat("Password is correct", isPasswordCorrect, is(true));
    }

    @Test
    public void testGenerateRandomPassword() throws Exception {
            IPasswords passwords = new Passwords();
            final String randomPassword = passwords.generateRandomPassword(10);
            LOGGER.info(randomPassword);
            assertThat("Random password is not null", randomPassword, Matchers.notNullValue());
    }
}

pom.xml(仅依赖项):

<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testng</groupId>
        <artifactId>testng</artifactId>
        <version>6.1.1</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.hamcrest</groupId>
        <artifactId>hamcrest-all</artifactId>
        <version>1.3</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>

    <dependency>
        <groupId>org.bouncycastle</groupId>
        <artifactId>bcprov-jdk15on</artifactId>
        <version>1.51</version>
        <type>jar</type>
    </dependency>


    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.3.2</version>
    </dependency>


</dependencies>

0
这是我的解决方案,我很想听听大家的意见,对于初学者来说很简单。
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Base64;
import java.util.Base64.Encoder;
import java.util.Scanner;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

public class Cryptography {

    public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeySpecException {
        Encoder encoder = Base64.getUrlEncoder().withoutPadding();
        System.out.print("Password: ");
        String strPassword = new Scanner(System.in).nextLine();
        byte[] bSalt = Salt();
        String strSalt = encoder.encodeToString(bSalt); // Byte to String
        System.out.println("Salt: " + strSalt);
        System.out.println("String to be hashed: " + strPassword + strSalt);
        String strHash = encoder.encodeToString(Hash(strPassword, bSalt)); // Byte to String
        System.out.println("Hashed value (Password + Salt value): " + strHash);
    }

    private static byte[] Salt() {
        SecureRandom random = new SecureRandom();
        byte salt[] = new byte[6];
        random.nextBytes(salt);
        return salt;
    }

    private static byte[] Hash(String password, byte[] salt) throws NoSuchAlgorithmException, InvalidKeySpecException {
        KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 65536, 128);
        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        byte[] hash = factory.generateSecret(spec).getEncoded();
        return hash;
    }

}

您可以通过解码strSalt并使用相同的hash方法进行验证:

public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeySpecException {
        Encoder encoder = Base64.getUrlEncoder().withoutPadding();
        Decoder decoder = Base64.getUrlDecoder();
        System.out.print("Password: ");
        String strPassword = new Scanner(System.in).nextLine();
        String strSalt = "Your Salt String Here";
        byte[] bSalt = decoder.decode(strSalt); // String to Byte
        System.out.println("Salt: " + strSalt);
        System.out.println("String to be hashed: " + strPassword + strSalt);
        String strHash = encoder.encodeToString(Hash(strPassword, bSalt)); // Byte to String
        System.out.println("Hashed value (Password + Salt value): " + strHash);
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接