PBKDF2算法Java实现

1.加盐处理

       在密码学中,是指通过在密码任意固定位置插入特定的字符串,让散列后的结果和使用原始密码的散列结果不相符,这种过程称之为“加盐”(比如可以在密码中混入一段“随机”的字符串再进行哈希加密,这个被字符串被称作盐值)。

 

2.为什么要加盐

      涉及身份验证的系统都需要存储用户的认证信息,常用的用户认证方式主要为用户名和密码的方式,为了安全起见,用户输入的密码需要保存为密文形式,可采用已公开的不可逆的hash加密算法,比如SHA256, SHA512, SHA3等,对于同一密码,同一加密算法会产生相同的hash值,这样,当用户进行身份验证时,也可对用户输入的明文密码应用相同的hash加密算法,得出一个hash值,然后使用该hash值和之前存储好的密文值进行对照,如果两个值相同,则密码认证成功,否则密码认证失败。

     由于密码是由用户设定的,在实际应用中,用户设置的密码复杂度可能不够高,同时不同的用户极有可能会使用相同的密码,那么这些用户对应的密文也会相同,这样,当存储用户密码的数据库泄露后,攻击者会很容易便能找到相同密码的用户,从而也降低了破解密码的难度,因此,在对用户密码进行加密时,需要考虑对密码进行掩饰,即使是相同的密码,也应该要保存为不同的密文,即使用户输入的是弱密码,也需要考虑进行增强,从而增加密码被攻破的难度,而使用带盐的加密hash值便能满足该需求。

 

3.常用密码攻击方式

 常用的密码攻击方式有字典攻击、暴力破解、查表法、反向查表法、彩虹表等。

     对字典攻击和暴力破解,攻击者均采用逐密码尝试的方式,目前没有很好的手段来阻止字典攻击和暴力破解攻击,只能是想办法让这两种攻击方式变得相对低效一些,而相同的密码产生不同的hash值便能让攻击者针对每一个hash值都需要从头进行尝试,从而使攻击变得更加低效。

      对查表法、反向查表法和彩虹表攻击方式,攻击者需要提前准备好包含密码和密码hash值的密码表,然后根据该表和用户密码数据库进行批量匹配,从而达到攻破密码的目的;而如果我们在加密时,给每个密码附加了不同的随机值,这样每个密码对应的hash值也会不同,这样攻击者在准备密码表时,就必须要将最基本的密码和用户密码数据库中的盐值进行笛卡尔积后再计算hash值,盐值越多,用户需要准备的表量越大,这样对于攻击而言,就变得有些得不偿失了。

 

4. 让密码更难破解(慢哈希函数)

       加盐使攻击者无法采用特定的查询表和彩虹表快速破解大量哈希值,但是却不能阻止他们使用字典攻击或暴力攻击。高端的显卡(GPU)和定制的硬件可以每秒进行数十亿次哈希计算,因此这类攻击依然可以很高效。为了降低攻击者的效率,我们可以使用一种叫做密钥扩展的技术。

      这种技术的思想就是把哈希函数变得很慢,于是即使有着超高性能的GPU或定制硬件,字典攻击和暴力攻击也会慢得让攻击者无法接受。最终的目标是把哈希函数的速度降到足以让攻击者望而却步,但造成的延迟又不至于引起用户的注意。

     密钥扩展的实现是依靠一种CPU密集型哈希函数。不要尝试自己发明简单的迭代哈希加密,如果迭代不够多,是可以被高效的硬件快速并行计算出来的,就和普通哈希一样。应该使用标准的算法,比如PBKDF2或者bcrypt。这里使用PBKDF2实现

      这类算法使用一个安全因子或迭代次数作为参数,这个值决定了哈希函数会有多慢。对于桌面软件或者手机软件,获取参数最好的办法就是执行一个简短的性能基准测试,找到使哈希函数大约耗费0.5秒的值。这样,你的程序就可以尽可能保证安全,而又不影响到用户体验。

 

5.实现代码 

package com.hwj.util;

import org.springframework.stereotype.Component;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;

@Component
public class PBKDF2Util {

    public static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1";


    public static final int SALT_BYTE_SIZE = 32 / 2;         //盐的长度
    public static final int HASH_BIT_SIZE = 128 * 4;         //生成密文的长度
    public static final int PBKDF2_ITERATIONS = 1000;        //迭代次数



    /**
     * @auther: Ragty
     * @describe: 对输入的密码进行验证
     * @param: [attemptedPassword(待验证密码), encryptedPassword(密文), salt(盐值)]
     * @return: boolean
     * @date: 2018/11/2
     */
    public boolean authenticate(String attemptedPassword, String encryptedPassword, String salt)
            throws NoSuchAlgorithmException, InvalidKeySpecException {
        // 用相同的盐值对用户输入的密码进行加密
        String encryptedAttemptedPassword = getEncryptedPassword(attemptedPassword, salt);
        // 把加密后的密文和原密文进行比较,相同则验证成功,否则失败
        return encryptedAttemptedPassword.equals(encryptedPassword);
    }



    /**
     * @auther: Ragty
     * @describe: 生成密文
     * @param: [password(明文密码), salt(盐值)]
     * @return: java.lang.String
     * @date: 2018/11/2
     */
    public String getEncryptedPassword(String password, String salt) throws NoSuchAlgorithmException,
            InvalidKeySpecException {

        KeySpec spec = new PBEKeySpec(password.toCharArray(), fromHex(salt), PBKDF2_ITERATIONS, HASH_BIT_SIZE);
        SecretKeyFactory f = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM);
        return toHex(f.generateSecret(spec).getEncoded());
    }




    /**
     * @auther: Ragty
     * @describe: 通过加密的强随机数生成盐(最后转换为16进制)
     * @param: []
     * @return: java.lang.String
     * @date: 2018/11/2
     */
    public String generateSalt() throws NoSuchAlgorithmException {
        SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
        byte[] salt = new byte[SALT_BYTE_SIZE];
        random.nextBytes(salt);

        return toHex(salt);
    }




    /**
     * @auther: Ragty
     * @describe: 十六进制字符串转二进制字符串
     * @param: [hex]
     * @return: byte[]
     * @date: 2018/11/2
     */
    private static byte[] fromHex(String hex) {
        byte[] binary = new byte[hex.length() / 2];
        for (int i = 0; i < binary.length; i++) {
            binary[i] = (byte) Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16);
        }
        return binary;
    }





    /**
     * @auther: Ragty
     * @describe: 二进制字符串转十六进制字符串
     * @param: [array]
     * @return: java.lang.String
     * @date: 2018/11/2
     */
    private static String toHex(byte[] array) {
        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;
    }
}

 

6.单元测试

package com.hwj;

import com.hwj.util.MD5Util;
import com.hwj.util.PBKDF2Util;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import sun.security.util.Password;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SaltTest {

    @Autowired
    private PBKDF2Util pbkdf2Util;
    @Autowired
    private MD5Util md5Util;


    @Test
    public void mailSendTest() throws Exception{

        String password = "123456";

        String salt = pbkdf2Util.generateSalt();
        String pbkdf2 = pbkdf2Util.getEncryptedPassword(password,salt);
        String md5 = md5Util.digest(password);

        System.out.println("原始密码:"+password);
        System.out.println("MD5加密后的密码:"+md5);
        System.out.println("盐值:"+salt);
        System.out.println("PBKDF2加盐后的密码:"+pbkdf2);
        System.out.println("Test success");

    }


}

 

7.测试结果

原始密码:123456
MD5加密后的密码:e10adc3949ba59abbe56e057f20f883e
盐值:fb8681ed06d293f602596206089cc961
PBKDF2加盐后的密码:ac9bb962cc0e9b8a8c9887e0df41982b98001456afa191123fcd67ece84d3f715ecc5be82a704ec001e8bec1b53fe01f8adacffbfa3bc425f10b962c3690a904
Test success 

 

  • 17
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值