原创

Java –生成安全哈希(Java安全散列-MD5,SHA256,SHA512,PBKDF2,BCrypt,SCrypt)

温馨提示:
本文最后更新于 2020年04月21日,已超过 1,632 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我

深入学习Java安全哈希算法。安全密码哈希是施加于用户提供的密码一定的算法和操作,其通常非常弱,容易猜测之后获得的字符的加密序列。

Java中有许多这样的哈希算法,它们可以证明确实对密码安全有效

请记住,一旦生成此密码哈希并将其存储在数据库中,就无法将其转换回原始密码。

每次用户登录到应用程序时,您都必须再次重新生成密码哈希,并与存储在数据库中的哈希匹配。因此,如果用户忘记了他/她的密码,则您将不得不向他发送一个临时密码,并要求他使用新密码进行更改。如今很常见吧?

目录

使用MD5算法简单密码的安全性
使MD5更安全,使用盐
使用SHA算法中的密码安全
使用PBKDF2WithHmacSHA1算法高级密码安全
使用BCrypt和SCrypt算法更安全的密码散列
最后说明

使用MD5算法的简单密码安全性

MD5消息摘要算法是一种广泛使用的密码散列函数,其产生一个128位(16字节)的散列值。这非常简单直接。基本思想是将可变长度的数据集映射到数据集的固定长度的

为此,将输入消息拆分为512位块的块。将填充添加到末尾,以便可以将其长度除以512。现在,这些块由MD5算法处理,该算法在128位状态下运行,结果将是128位哈希值。应用MD5后,生成的哈希通常是32位十六进制数字。

在此,通常将要编码的密码称为“ 消息 ”,并将生成的哈希值称为消息摘要或简称为“ 摘要”

Java MD5哈希示例

public class SimpleMD5Example
{
    public static void main(String[] args)
    {
        String passwordToHash = "password";
        String generatedPassword = null;
        try {
            // Create MessageDigest instance for MD5
            MessageDigest md = MessageDigest.getInstance("MD5");
            //Add password bytes to digest
            md.update(passwordToHash.getBytes());
            //Get the hash's bytes
            byte[] bytes = md.digest();
            //This bytes[] has bytes in decimal format;
            //Convert it to hexadecimal format
            StringBuilder sb = new StringBuilder();
            for(int i=0; i< bytes.length ;i++)
            {
                sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));
            }
            //Get complete hashed password in hex format
            generatedPassword = sb.toString();
        }
        catch (NoSuchAlgorithmException e)
        {
            e.printStackTrace();
        }
        System.out.println(generatedPassword);
    }
}
 
Console output:
 
5f4dcc3b5aa765d61d8327deb882cf99

尽管MD5是一种广泛使用的散列算法,但远非安全性,但MD5会生成相当弱的散列。它的主要优点是快速,易于实施。但这也意味着它容易受到 暴力攻击字典攻击

带有单词和哈希的 Rainbow表可以非常快速地搜索已知的哈希并获取原始单词。

MD5 不是抗冲突的,这意味着不同的密码最终可能导致相同的哈希。

今天,如果您在应用程序中使用MD5哈希,则可以考虑在安全性方面加些盐

使用盐使MD5更安全

请记住,加盐不是MD5特有的。您也可以将其添加到其他算法中。因此,请重点关注其应用方式,而不是其与MD5的关系。

Wikipedia将salt定义为随机数据,用作哈希密码或密码短语的单向函数的附加输入用更简单的话来说,salt是一些随机生成的文本,在获取哈希之前将其附加到密码上。

撒盐的最初目的主要是为了克服预先计算的彩虹表攻击,否则该攻击可用于极大地提高破解哈希密码数据库的效率。现在,更大的好处是放慢了并行操作,该并行操作可以一次将密码猜测的哈希值与许多密码哈希值进行比较。

重要提示:我们始终需要使用SecureRandom来创建良好的盐,并且在Java中,SecureRandom类支持“ SHA1PRNG ”伪随机数生成器算法,并且我们可以利用它。

如何为哈希生成盐

让我们看看如何产生盐。

private static byte[] getSalt() throws NoSuchAlgorithmException
{
    //Always use a SecureRandom generator
    SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
    //Create array for salt
    byte[] salt = new byte[16];
    //Get a random salt
    sr.nextBytes(salt);
    //return salt
    return salt;
}

SHA1PRNG算法被用作基于SHA-1消息摘要算法的加密强伪随机数生成器。请注意,如果未提供种子,它将从真正的随机数生成器(TRNG生成种子

带盐的Java MD5示例

现在,让我们看一下修改后的MD5哈希示例:

public class SaltedMD5Example
{
    public static void main(String[] args) throws NoSuchAlgorithmException, NoSuchProviderException
    {
        String passwordToHash = "password";
        byte[] salt = getSalt();
         
        String securePassword = getSecurePassword(passwordToHash, salt);
        System.out.println(securePassword); //Prints 83ee5baeea20b6c21635e4ea67847f66
         
        String regeneratedPassowrdToVerify = getSecurePassword(passwordToHash, salt);
        System.out.println(regeneratedPassowrdToVerify); //Prints 83ee5baeea20b6c21635e4ea67847f66
    }
     
    private static String getSecurePassword(String passwordToHash, byte[] salt)
    {
        String generatedPassword = null;
        try {
            // Create MessageDigest instance for MD5
            MessageDigest md = MessageDigest.getInstance("MD5");
            //Add password bytes to digest
            md.update(salt);
            //Get the hash's bytes
            byte[] bytes = md.digest(passwordToHash.getBytes());
            //This bytes[] has bytes in decimal format;
            //Convert it to hexadecimal format
            StringBuilder sb = new StringBuilder();
            for(int i=0; i< bytes.length ;i++)
            {
                sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));
            }
            //Get complete hashed password in hex format
            generatedPassword = sb.toString();
        }
        catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return generatedPassword;
    }
     
    //Add salt
    private static byte[] getSalt() throws NoSuchAlgorithmException, NoSuchProviderException
    {
        //Always use a SecureRandom generator
        SecureRandom sr = SecureRandom.getInstance("SHA1PRNG", "SUN");
        //Create array for salt
        byte[] salt = new byte[16];
        //Get a random salt
        sr.nextBytes(salt);
        //return salt
        return salt;
    }
}

重要:请注意,现在您必须为散列的每个密码存储此salt值因为当用户重新登录系统时,您必须仅使用原始生成的盐重新创建散列以与存储的散列匹配。如果使用其他盐(我们正在生成随机盐),则生成的哈希将有所不同。

另外,您可能听说过术语“ 疯狂的哈希和盐腌”它通常指创建自定义组合。

疯狂的哈希和加盐示例

alt+password+salt => hash

不要练习这些疯狂的事情。无论如何,它们无助于使哈希进一步安全。如果需要更高的安全性,请选择更好的算法。

使用SHA算法的中等密码安全性

SHA(安全散列算法)是加密散列函数族。它与MD5非常相似,只不过它会产生更强的哈希值但是,这些哈希值并不总是唯一的,这意味着对于两个不同的输入,我们可以具有相等的哈希值。发生这种情况时称为“冲突”。SHA中发生冲突的可能性小于MD5。但是,不必担心这些碰撞,因为它们确实非常罕见。

Java有4种SHA算法的实现。与MD5(128位哈希)相比,它们生成以下长度的哈希:

  • SHA-1(最简单的一位-160位哈希)
  • SHA-256(比SHA-1强-256位哈希)
  • SHA-384(比SHA-256强-384位哈希)
  • SHA-512(比SHA-384更强大– 512位哈希)

较长的哈希值更难破解。那是核心思想。

要获得算法的任何实现,请将其作为参数传递给MessageDigest例如

MessageDigest md = MessageDigest.getInstance("SHA-1");
 
//OR
 
MessageDigest md = MessageDigest.getInstance("SHA-256");

Java SHA哈希示例

让我们创建一个测试程序,以便演示其用法:

package com.howtodoinjava.hashing.password.demo.sha;
 
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
 
public class SHAExample {
     
    public static void main(String[] args) throws NoSuchAlgorithmException
    {
        String passwordToHash = "password";
        byte[] salt = getSalt();
         
        String securePassword = get_SHA_1_SecurePassword(passwordToHash, salt);
        System.out.println(securePassword);
         
        securePassword = get_SHA_256_SecurePassword(passwordToHash, salt);
        System.out.println(securePassword);
         
        securePassword = get_SHA_384_SecurePassword(passwordToHash, salt);
        System.out.println(securePassword);
         
        securePassword = get_SHA_512_SecurePassword(passwordToHash, salt);
        System.out.println(securePassword);
    }
 
    private static String get_SHA_1_SecurePassword(String passwordToHash, byte[] salt)
    {
        String generatedPassword = null;
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            md.update(salt);
            byte[] bytes = md.digest(passwordToHash.getBytes());
            StringBuilder sb = new StringBuilder();
            for(int i=0; i< bytes.length ;i++)
            {
                sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));
            }
            generatedPassword = sb.toString();
        }
        catch (NoSuchAlgorithmException e)
        {
            e.printStackTrace();
        }
        return generatedPassword;
    }
     
    private static String get_SHA_256_SecurePassword(String passwordToHash, byte[] salt)
    {
        //Use MessageDigest md = MessageDigest.getInstance("SHA-256");
    }
     
    private static String get_SHA_384_SecurePassword(String passwordToHash, byte[] salt)
    {
        //Use MessageDigest md = MessageDigest.getInstance("SHA-384");
    }
     
    private static String get_SHA_512_SecurePassword(String passwordToHash, byte[] salt)
    {
        //Use MessageDigest md = MessageDigest.getInstance("SHA-512");
    }
     
    //Add salt
    private static byte[] getSalt() throws NoSuchAlgorithmException
    {
        SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
        byte[] salt = new byte[16];
        sr.nextBytes(salt);
        return salt;
    }
}
 
Output:
 
e4c53afeaa7a08b1f27022abd443688c37981bc4
 
87adfd14a7a89b201bf6d99105b417287db6581d8aee989076bb7f86154e8f32
 
bc5914fe3896ae8a2c43a4513f2a0d716974cc305733847e3d49e1ea52d1ca50e2a9d0ac192acd43facfb422bb5ace88
 
529211542985b8f7af61994670d03d25d55cc9cd1cff8d57bb799c4b586891e112b197530c76744bcd7ef135b58d47d65a0bec221eb5d77793956cf2709dd012

我们可以很容易地说SHA-512产生最强的哈希值。

使用PBKDF2WithHmacSHA1算法的高级密码安全性

到目前为止,我们了解了如何为密码创建安全的哈希,并使用salt使其更加安全。但是今天的问题是,硬件已经变得如此之快,以至于使用字典和Rainbow表进行的任何暴力攻击,都可能在更少或更长时间内破解任何密码。

为了解决这个问题,一般的想法是使暴力攻击变慢,以使破坏最小化。我们的下一个算法就是基于这个概念。目的是使散列函数足够慢以阻止攻击,但又要足够快以至于不会对用户造成明显的延迟。

此功能实质上是使用某些CPU密集型算法(例如PBKDF2,BcryptScrypt)实现的这些算法将工作因子(也称为安全因子)或迭代计数作为参数。此值确定哈希函数的速度。明年计算机变得更快时,我们可以增加工作系数来平衡它。

Java已将“ PBKDF2 ”算法实现为“ PBKDF2WithHmacSHA1 ”。

Java PBKDF2WithHmacSHA1哈希示例

让我们看一下如何使用PBKDF2WithHmacSHA1算法的示例。

    public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeySpecException
    {
        String  originalPassword = "password";
        String generatedSecuredPasswordHash = generateStorngPasswordHash(originalPassword);
        System.out.println(generatedSecuredPasswordHash);
    }
    private static String generateStorngPasswordHash(String password) throws NoSuchAlgorithmException, InvalidKeySpecException
    {
        int iterations = 1000;
        char[] chars = password.toCharArray();
        byte[] salt = getSalt();
         
        PBEKeySpec spec = new PBEKeySpec(chars, salt, iterations, 64 * 8);
        SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        byte[] hash = skf.generateSecret(spec).getEncoded();
        return iterations + ":" + toHex(salt) + ":" + toHex(hash);
    }
     
    private static byte[] getSalt() throws NoSuchAlgorithmException
    {
        SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
        byte[] salt = new byte[16];
        sr.nextBytes(salt);
        return salt;
    }
     
    private static String toHex(byte[] array) throws NoSuchAlgorithmException
    {
        BigInteger bi = new BigInteger(1, array);
        String hex = bi.toString(16);
        int paddingLength = (array.length * 2) - hex.length();
        if(paddingLength > 0)
        {
            return String.format("%0"  +paddingLength + "d", 0) + hex;
        }else{
            return hex;
        }
    }
     
Output:
     
1000:5b4240333032306164:f38d165fce8ce42f59d366139ef5d9e1ca1247f0e06e503ee1a611dd9ec40876bb5edb8409f5abe5504aab6628e70cfb3d3a18e99d70357d295002c3d0a308a0

下一步是拥有一个功能,当用户再次登录并再次登录时,该功能可用于再次验证密码。

public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeySpecException
    {
        String  originalPassword = "password";
        String generatedSecuredPasswordHash = generateStorngPasswordHash(originalPassword);
        System.out.println(generatedSecuredPasswordHash);
         
        boolean matched = validatePassword("password", generatedSecuredPasswordHash);
        System.out.println(matched);
         
        matched = validatePassword("password1", generatedSecuredPasswordHash);
        System.out.println(matched);
    }
     
    private static boolean validatePassword(String originalPassword, String storedPassword) throws NoSuchAlgorithmException, InvalidKeySpecException
    {
        String[] parts = storedPassword.split(":");
        int iterations = Integer.parseInt(parts[0]);
        byte[] salt = fromHex(parts[1]);
        byte[] hash = fromHex(parts[2]);
         
        PBEKeySpec spec = new PBEKeySpec(originalPassword.toCharArray(), salt, iterations, hash.length * 8);
        SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        byte[] testHash = skf.generateSecret(spec).getEncoded();
         
        int diff = hash.length ^ testHash.length;
        for(int i = 0; i < hash.length && i < testHash.length; i++)
        {
            diff |= hash[i] ^ testHash[i];
        }
        return diff == 0;
    }
    private static byte[] fromHex(String hex) throws NoSuchAlgorithmException
    {
        byte[] bytes = new byte[hex.length() / 2];
        for(int i = 0; i<bytes.length ;i++)
        {
            bytes[i] = (byte)Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16);
        }
        return bytes;
    }

请注意引用上述代码示例中的函数。如果发现任何困难,请下载本教程末尾附带的源代码。

使用bcrypt和scrypt算法更安全的密码哈希

bcrypt背后的概念类似于PBKDF2中的先前概念。碰巧是Java没有对bcrypt算法提供任何内置支持,以使攻击速度变慢,但是您仍然可以在源代码下载中找到这样的实现。

带盐的Java bcrypt示例

让我们看一下示例用法代码(BCrypt.java在源代码中可用)。

public class BcryptHashingExample
{
    public static void main(String[] args) throws NoSuchAlgorithmException
    {
        String  originalPassword = "password";
        String generatedSecuredPasswordHash = BCrypt.hashpw(originalPassword, BCrypt.gensalt(