您的位置:首页 > 编程语言

如何安全的存储用户密码?(下)代码实现pbkdf2算法加密

2017-04-27 16:29 741 查看
这辈子没办法做太多事情,所以每一件都要做到精彩绝伦!
People can't do too many things in my life,so everything will be wonderful

本文参考博客:
http://wyait.blog.51cto.com/12674066/1918470
http://wyait.blog.51cto.com/12674066/1918474
参考资料:java API6.0中文版.chm
本文以java为例,进行实际加解密操作:

1 密码加盐hash

使用salt+password进行哈希算法加密!哈希算法选择:PBKDF2!

1.1 生成salt

使用随机函数java.security.SecureRandom生成24位随机数作为salt:
本文参考的依据是:JDK API 1.6.0 中文版,下载地址:http://down.51cto.com/data/2300228

1.2 PBKDF2代码详解

可以直接copy到代码中
package com.demo.encrypt;

/*
*Password Hashing With PBKDF2 (http://crackstation.net/hashing-security.htm).
*Copyright (c) 2013, Taylor Hornby
*All rights reserved.
*
*Redistribution and use in source and binary forms, with or without
*modification, are permitted provided that the following conditions are met:
*
* 1.Redistributions of source code must retain the above copyright notice,
*this list of conditions and the following disclaimer.
*
* 2.Redistributions in binary form must reproduce the above copyright notice,
*this list of conditions and the following disclaimer in the documentation
*and/or other materials provided with the distribution.
*
*THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "ASIS"
*AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
*IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
*ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
*LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
*CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
*SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
*INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
*CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
*ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
*POSSIBILITY OF SUCH DAMAGE.
*/

import java.math.BigInteger;
importjava.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;

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

/*
*PBKDF2 salted password hashing.
*Author: havoc AT defuse.ca
*www: http://crackstation.net/hashing-security.htm */
public class HashEncrypt {
//PBKDF2.PBKDF2WithHmacSHA1, PBKDF2.PBKDF2WithHmacSHA224,PBKDF2.PBKDF2WithHmacSHA256,
//PBKDF2.PBKDF2WithHmacSHA384,//PBKDF2.PBKDF2WithHmacSHA512
//pbkdf2算法API链接:http://javadoc.iaik.tugraz.at/iaik_jce/current/iaik/pkcs/pkcs5/PBKDF2.html
publicstatic final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1";
//publicstatic final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA256";
//注意jdk1.8以下版本不支持SHA224以及SHA224以上算法,会报错:(项目环境为JDK1.7)
//ERROR:java.security.NoSuchAlgorithmException: PBKDF2WithHmacSHA224 SecretKeyFactorynot available
//The following constants may be changed without breaking existing hashes.
publicstatic final int SALT_BYTE_SIZE = 16;//=32/2,生成字符长度为32的salt值
publicstatic final int HASH_BYTE_SIZE = 24;
publicstatic final int PBKDF2_ITERATIONS = 1000;

publicstatic final int ITERATION_INDEX = 0;
publicstatic final int SALT_INDEX = 1;
publicstatic final int PBKDF2_INDEX = 2;

/**
* Returns a salted PBKDF2 hash of thepassword.(返回使用PBKDF2对password进行加盐hash生成的字符串)
*
* @param password the password to hash
* @return a salted PBKDF2 hash of thepassword
*/
publicstatic String createHash(String password)
throwsNoSuchAlgorithmException, InvalidKeySpecException {
returncreateHash(password.toCharArray());
}

/**
* Returns a salted PBKDF2 hash of thepassword.(返回使用PBKDF2对password字节数组进行加盐hash生成的字符串)
*
* @param password the password to hash
* @return a salted PBKDF2 hash of thepassword
*/
publicstatic String createHash(char[] password)
throwsNoSuchAlgorithmException, InvalidKeySpecException {
LongstartTime = System.currentTimeMillis();
//Generate a random salt:创建一个随机16位盐
SecureRandomrandom = new SecureRandom();
byte[]salt = new byte[SALT_BYTE_SIZE];
random.nextBytes(salt);
//Hash the password :生成可以编码的密钥
byte[]hash = pbkdf2(password, salt, PBKDF2_ITERATIONS, HASH_BYTE_SIZE);
LongendTime = System.currentTimeMillis();
System.out.println("生成加盐hash密码时间是:"+ String.valueOf(endTime - startTime));
//format iterations:salt:hash:对字节数组进行hash返回字符串,字符串结构:(迭代次数:salt盐:加盐hash密码)
returnPBKDF2_ITERATIONS + ":" + toHex(salt) + ":" + toHex(hash);
}

/**
* Validates a password using a hash.(校验密码是否正确)
*
* @param password the password tocheck
* @param correctHash the hash of thevalid password
* @return true if the password iscorrect, false if not
*/
publicstatic boolean validatePassword(String password, String correctHash)
throwsNoSuchAlgorithmException, InvalidKeySpecException {
returnvalidatePassword(password.toCharArray(), correctHash);
}

/**
* Validates a password using a hash.(校验密码是否正确实现)
*
* @param password the password tocheck
* @param correctHash the hash of thevalid password
* @return true if the password iscorrect, false if not:校验密码是否正确
*/
publicstatic boolean validatePassword(char[] password, String correctHash)
throwsNoSuchAlgorithmException, InvalidKeySpecException {
//correctHash字符串的结构为(迭代次数:salt盐:加盐hash密码)
//Decode the hash into its parameters:冒号分隔correctHash
String[]params = correctHash.split(":");
//params[0]:迭代次数
intiterations = Integer.parseInt(params[ITERATION_INDEX]);
//params[1]:salt盐 ,再转换为字节数组
byte[]salt = fromHex(params[SALT_INDEX]);
////params[2]:加盐hash密码字符串,再转换为字节数组
byte[]hash = fromHex(params[PBKDF2_INDEX]);
//Compute the hash of the provided password, using the same salt,
//iteration count, and hash length :使用password生成密钥
byte[]testHash = pbkdf2(password, salt, iterations, hash.length);
//Compare the hashes in constant time. The password is correct if
//both hashes match.:比较密钥是否相等
returnslowEquals(hash, testHash);
}

/**
* Compares two byte arrays in length-constanttime. This comparison method
* is used so that password hashes cannot beextracted from an on-line
* system using a timing attack and thenattacked off-line.
* 这段代码使用了异或(XOR)操作符”^”来比较整数是否相等,而没有使用”==”操作符。原因在于如果两个数完全一致,异或之后的值为零。
*
因为 0 XOR 0 = 0, 1 XOR1 = 0, 0 XOR 1 = 1, 1 XOR 0 = 1。
*
所以,第一行代码如果a.length等于b.length,变量diff等于0,否则的话diff就是一个非零的值。
*
然后,让a,b的每一个字节XOR之后再跟diff OR。这样,只有diff一开始是0,并且,a,b的每一个字节XOR的结果也是零,
*
最后循环完成后diff的值才是0,这种情况是a,b完全一样。否则最后diff是一个非零的值。
*
* @param a the first byte array
* @param b the second byte array
* @return true if both byte arrays are thesame, false if not:比较两个字节数组是否相等。0相等;1不等
*/
privatestatic boolean slowEquals(byte[] a, byte[] b) {
intdiff = a.length ^ b.length;
for(int i = 0; i < a.length && i < b.length; i++)
diff|= a[i] ^ b[i];
returndiff == 0;
}

/**
* Computes the PBKDF2 hash of a password.(使用pbkdf2算法生成加盐hash字节数组)
*
* @param password the password to hash.
* @param salt the salt
* @param iterations the iteration count(slowness factor)
* @param bytes the length of the hashto compute in bytes
* @return the PBDKF2 hash of the password:使用pbkdf2进行加盐hash生成密钥
*/
privatestatic byte[] pbkdf2(char[] password, byte[] salt, int iterations,
intbytes) throws NoSuchAlgorithmException, InvalidKeySpecException {
//PBEKeySpec:可随同基于密码的加密法 (PBE)
//使用的供用户选择的密码。可以将密码视为某种原始密钥内容,由此加密机制使用其导出一个密钥。
//带有生成可变密钥大小的 PBE 密码的 PBEKey 时使用的一个密码、salt、迭代计数以及导出密钥长度的构造方法。如果指定
//password 为 null,则使用一个空 char[]。
//注:在将 password 和 salt 存储进新的 PBEKeySpec 对象前将其复制。
//参数:
//password - 密码。
//salt - salt。
//iterationCount - 迭代计数。
//keyLength - 要导出的密钥长度。
PBEKeySpecspec = new PBEKeySpec(password, salt, iterations, bytes * 8);
//SecretKeyFactory此类表示秘密密钥的工厂。
//getInstance(String PBKDF2_ALGORITHM)返回转换指定算法的秘密密钥的SecretKeyFactory
//对象。
//PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1";
SecretKeyFactoryskf = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM);
//skf.generateSecret(spec)根据提供的密钥规范(密钥材料)生成 SecretKey 对象。
//SecretKey.getEncoded()返回基本编码格式的密钥,如果此密钥不支持编码,则返回 null。
returnskf.generateSecret(spec).getEncoded();
}

/**
* Converts a string of hexadecimal charactersinto a byte array.(将十六进制字符串转换为字节数组)
*
* @param hex the hex string
* @return the hex string decoded into abyte array:字符串转换为字节数组
*/
privatestatic 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);
}
returnbinary;
}

/**
* Converts a byte array into a hexadecimalstring.(将字节数组转换为十六进制字符串)
*
* @param array the byte array toconvert
* @return a length*2 character stringencoding the byte array::字节数组转换为字符串
*/
privatestatic String toHex(byte[] array) {
BigIntegerbi = new BigInteger(1, array);
Stringhex = bi.toString(16);
intpaddingLength = (array.length * 2) - hex.length();
if(paddingLength > 0)
returnString.format("%0" + paddingLength + "d", 0) + hex;
else
returnhex;
}

/**
* Tests the basic functionality of thePasswordHash class
*
* @param args ignored
*/
publicstatic void main(String[] args) {
try{
//Print out 10 hashes
for(int i = 0; i < 10; i++) {
//System.out.println(HashEncrypt.createHash("p\\r\\nassw0Rd!"));
System.out.println("生成加盐hash后密钥:第" +i + "次:hashPWd:"
+HashEncrypt.createHash("p\\r\\nassw0Rd!"));
}

//Test password validation
booleanfailure = false;
System.out.println("Runningtests...");
for(int i = 0; i < 2; i++) {
Stringpassword = "" + i;
Stringhash = createHash(password);
System.out.println("password第一次加盐hash:"+ hash);
StringsecondHash = createHash(password);
System.out.println("password第二次加盐hash:"+ secondHash);
if(hash.equals(secondHash)) {
//System.out.println("FAILURE: TWO HASHES ARE EQUAL!");
System.out.println("password="+ i
+",加盐hash后生成的两个密钥相等!就不可用!");
failure= true;
}
StringwrongPassword = "" + (i + 1);
if(validatePassword(wrongPassword, hash)) {
//System.out.println("FAILURE: WRONG PASSWORD ACCEPTED!");
System.out.println("hash="+ hash + ",wrongPassword="
+wrongPassword + ",校验密钥匹配!就不可用!");
failure= true;
}
if(!validatePassword(password, hash)) {
System.out.println("FAILURE:GOOD PASSWORD NOT ACCEPTED!");
System.out.println("hash=" +hash + ",password=" + password
+",校验密钥不匹配,hash就是password生成的!就不可用!");
failure= true;
}
}
if(failure)
System.out.println("TESTSFAILED!");
else
System.out.println("TESTSPASSED!");
}catch (Exception ex) {
System.out.println("ERROR:" + ex);
}
}

}

1.3 PBKDF2项目实践

1, 用户注册,使用PBKDF2对密码进行加盐hash加密;
2, 将salt盐和生成的hash密码存入数据库中;
3, 用户登录对密码进行加盐hash;
4, 校验密码;
5, 用户每次登录,重复3/4步骤。
如果想保存32位固定长度密码,可以在后面再进行一次MD5加密

工具类:
package com. common.utils;

import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
importjava.security.spec.InvalidKeySpecException;

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

/**
*
* @项目名称:common
* @类名称:HashEncrypt
* @类描述:使用PBKDF2对密码进行加密处理
* @创建人:wyait
* @创建时间:2017年4月24日 上午9:53:15
*@version:
*/
public class HashEncrypt {
//pbkdf2 SHA1算法(由于JDK1.8以下只能使用SHA1,so....这将会是个历史遗留问题)
publicstatic final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1";
//=32/2,生成字符长度为32的salt值
publicstatic final int SALT_BYTE_SIZE = 16;
//48/2,生成字符长度为48的hash密码
publicstatic final int HASH_BYTE_SIZE = 24;
//加密迭代次数
publicstatic final int PBKDF2_ITERATIONS = 1000;

/**
*
* @描述:返回使用SecureRandom生成salt盐
* @创建人:wyait
* @创建时间:2017年4月24日 上午10:40:14
* @return
*/
publicstatic String createSalt() {
//Generate a random salt:创建一个随机16位盐
SecureRandomrandom = new SecureRandom();
byte[]salt = new byte[SALT_BYTE_SIZE];
random.nextBytes(salt);
//转换为十六进制字符串,字符长度=32
returntoHex(salt);
}

/**
*
* @描述:返回使用PBKDF2对password进行加盐hash生成的字符串(字符长度48)
* @创建人:wyait
* @创建时间:2017年4月24日 上午9:55:37
* @param salt 盐
* @param password 原始密码
* @return
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
*/
publicstatic String createHash(String salt, String password)
throwsNoSuchAlgorithmException, InvalidKeySpecException {
returncreateHash(fromHex(salt), password.toCharArray());
}

/**
*
* @描述:返回使用PBKDF2对password字节数组进行加盐hash生成的字符串(字符长度48)
* @创建人:wyait
* @创建时间:2017年4月24日 上午9:57:35
* @param salt 盐转换为字节数组
* @param password 密码转换为字符数组
* @return
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
*/
publicstatic String createHash(byte[] salt, char[] password)
throwsNoSuchAlgorithmException, InvalidKeySpecException {
//Hash the password :hash生成字节数组
byte[]hash = pbkdf2(password, salt, PBKDF2_ITERATIONS, HASH_BYTE_SIZE);
//对字节数组进行hash返回字符串(加盐hash密码)
System.out.println("hash密钥字符串长度:"+ toHex(hash).length());
returntoHex(hash);
}

/**
*
* @描述:校验密码是否正确
* @创建人:wyait
* @创建时间:2017年4月24日 上午10:02:49
* @param salt 盐
* @param password 密码
* @param correctHash 加盐hash密码
* @return
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
*/
publicstatic boolean validatePassword(String salt, String password,
StringcorrectHash) throws NoSuchAlgorithmException,
InvalidKeySpecException{
returnvalidatePassword(fromHex(salt), password.toCharArray(),
correctHash);
}

/**
*
* @描述:校验密码是否正确实现
* @创建人:wyait
* @创建时间:2017年4月24日 上午10:07:14
* @param salt 盐
* @param password 密码
* @param correctHash 加盐hash密码
* @return
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
*/
publicstatic boolean validatePassword(byte[] salt, char[] password,
StringcorrectHash) throws NoSuchAlgorithmException,
InvalidKeySpecException{
//将correctHash转换为字节数组
byte[]hash = fromHex(correctHash);
//对passwrod进行:加盐hash生成密码字符串,再比对correctHash
byte[]testHash = pbkdf2(password, salt, PBKDF2_ITERATIONS, hash.length);
//both hashes match.:比较密钥是否相等
returnslowEquals(hash, testHash);
}

/**
*
* @描述:比较两个字节数组的值是否相等
* @创建人:wyait
* @创建时间:2017年4月24日 上午10:07:46
* @param a
* @param b
* @return
*/
privatestatic boolean slowEquals(byte[] a, byte[] b) {
intdiff = a.length ^ b.length;
for(int i = 0; i < a.length && i < b.length; i++)
diff|= a[i] ^ b[i];
returndiff == 0;
}

/**
*
* @描述:使用pbkdf2算法生成加盐hash字节数组
* @创建人:wyait
* @创建时间:2017年4月24日 上午10:11:02
* @param password
* @param salt
* @param iterations
* @param bytes
* @return
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
*/
privatestatic byte[] pbkdf2(char[] password, byte[] salt, int iterations,
intbytes) throws NoSuchAlgorithmException, InvalidKeySpecException {
//PBEKeySpec:
//带有生成可变密钥大小的 PBE 密码的 PBEKey 时使用的一个密码、salt、迭代计数以及导出密钥长度的构造方法。如果指定
//password 为 null,则使用一个空 char[]。
//注:在将 password 和 salt 存储进新的 PBEKeySpec 对象前将其复制。
//参数:
//password - 密码。
//salt - salt。
//iterationCount - 迭代计数。
//keyLength - 要导出的密钥长度。
PBEKeySpecspec = new PBEKeySpec(password, salt, iterations, bytes * 8);
//SecretKeyFactory此类表示秘密密钥的工厂。
SecretKeyFactoryskf = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM);
//skf.generateSecret(spec)根据提供的密钥规范(密钥材料)生成 SecretKey 对象。
//SecretKey.getEncoded()返回基本编码格式的密钥,如果此密钥不支持编码,则返回 null。返回字节数组
returnskf.generateSecret(spec).getEncoded();
}

/**
*
* @描述:将十六进制字符串转换为字节数组
* @创建人:wyait
* @创建时间:2017年4月24日 上午10:11:15
* @param hex
* @return
*/
privatestatic 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);
}
returnbinary;
}

/**
*
* @描述:将字节数组转换为十六进制字符串
* @创建人:wyait
* @创建时间:2017年4月24日 上午10:11:22
* @param array
* @return
*/
privatestatic String toHex(byte[] array) {
BigIntegerbi = new BigInteger(1, array);
Stringhex = bi.toString(16);
intpaddingLength = (array.length * 2) - hex.length();
if(paddingLength > 0)
returnString.format("%0" + paddingLength + "d", 0) + hex;
else
returnhex;
}

/**
*
* @描述:main方法测试
* @创建人:wyait
* @创建时间:2017年4月24日 上午10:23:53
* @param args
*/
publicstatic void main(String[] args) {
try{
//Print out 10 hashes
for(int i = 0; i < 10; i++) {
Longs = System.currentTimeMillis();
String salt = HashEncrypt.createSalt();
//System.out.println(HashEncrypt.createHash("p\\r\\nassw0Rd!"));
Longe = System.currentTimeMillis();
System.out.println("生成加盐hash后密钥:第" +i + "次:hashPWd:"
+HashEncrypt.createHash(salt, "12003p\\r\\nassw0Rd!")
+",生成密码所用毫秒值:" + (String.valueOf(e - s)));
}
//Test password validation
booleanfailure = false;
System.out.println("Runningtests...");
for(int i = 0; i < 200; i++) {
Longs = System.currentTimeMillis();
Stringsalt = HashEncrypt.createSalt();
Stringpassword = "" + i;
Stringhash = createHash(salt, password);
Stringsalt1 = HashEncrypt.createSalt();
Longe = System.currentTimeMillis();
System.out.println("password第一次加盐hash:"+ hash + ",生成密码所用毫秒值:"
+(String.valueOf(e - s)));
StringsecondHash = createHash(salt1, password);
System.out.println("password第二次加盐hash:"+ secondHash);
//两个不同盐的hash密码比较
if(hash.equals(secondHash)) {
//System.out.println("FAILURE: TWO HASHES ARE EQUAL!");
System.out.println("password="+ i
+",加盐hash后生成的两个密钥相等!就不可用!");
failure= true;
}
StringwrongPassword = "" + (i + 1);
//使用其他密码是hash比对
if(validatePassword(salt1, wrongPassword, hash)) {
//System.out.println("FAILURE: WRONG PASSWORD ACCEPTED!");
System.out.println("hash="+ hash + ",wrongPassword="
+wrongPassword + ",校验密钥匹配!就不可用!");
failure= true;
}
//用salt+password和使用它们生成的hash比对是否相等
if(!validatePassword(salt, password, hash)) {
System.out.println("FAILURE:GOOD PASSWORD NOT ACCEPTED!");
System.out.println("hash="+ hash + ",password=" + password
+",校验密钥不匹配,hash就是password生成的!就不可用!");
failure= true;
}
}
if(failure)
System.out.println("TESTSFAILED!");
else
System.out.println("TESTSPASSED!");
}catch (Exception ex) {
System.out.println("ERROR:" + ex);
}
}

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息