深入学习Java Secure Hashing算法。安全密码哈希是施加于用户提供的密码一定的算法和操作,其通常非常弱,容易猜测之后获得的字符的加密序列。
Java中有许多这样的哈希算法可以证明对密码安全性非常有效。
请记住,一旦生成此密码哈希并将其存储在数据库中,您就无法将其转换回原始密码。
每次用户登录应用程序时,您都必须再次重新生成密码哈希并与存储在数据库中的哈希相匹配。因此,如果用户忘记了他/她的密码,您将不得不向他发送一个临时密码,并要求他用新密码更改密码。现在很常见,对吗?
目录
使用MD5算法的简单密码安全性
使用salt使MD5更安全
使用SHA算法的中密码安全性
使用PBKDF2WithHmacSHA1算法的高级密码安全性
使用bcrypt和scrypt算法进行更安全的密码哈希
最后的笔记
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会产生相当弱的哈希值。它的主要优点是速度快,易于实施。但这也意味着它容易受到 蛮力和字典攻击。
生成单词和散列的彩虹表允许非常快速地搜索已知散列并获得原始单词。
MD5 不是抗冲突的,这意味着不同的密码最终会导致相同的哈希。
今天,如果您在应用程序中使用MD5哈希,请考虑在安全性中添加一些盐。
请记住,添加盐不是MD5特定的。您也可以将其添加到其他算法中。因此,请关注它的应用方式,而不是它与MD5的关系。
维基百科将salt定义为随机数据,用作哈希密码或密码短语的单向函数的附加输入。更简单的说,salt是一些随机生成的文本,在获取哈希值之前会附加到密码中。
salting的最初意图主要是打败预先计算的彩虹表攻击,否则可以用来大大提高破解密码数据库的效率。现在更大的好处是减慢并行操作,将一次密码猜测的哈希值与多个密码哈希值进行比较。
重要提示:我们总是需要使用SecureRandom来创建好的盐,而在Java中,SecureRandom类支持“ SHA1PRNG ”伪随机数生成器算法,我们可以利用它。
如何为Hash生成Salt
让我们看看应该如何生成盐。
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值。因为当用户在系统中重新登录时,必须仅使用最初生成的salt再次创建哈希以匹配存储的哈希。如果使用不同的盐(我们生成随机盐),则生成的散列将不同。
此外,您可能听说过疯狂散列和腌制这个术语。它通常指创建自定义组合。
疯狂的哈希和腌制的例子
alt+password+salt => hash
不要练习这些疯狂的事情。无论如何,它们无助于使哈希更加安全。如果您想要更高的安全性,请选择更好的算法。
该SHA(安全散列算法)是加密散列函数族。它与MD5非常相似,只不过它会产生更强的哈希值。然而,这些散列并不总是唯一的,这意味着对于两个不同的输入,我们可以具有相等的散列。当发生这种情况时,它被称为“碰撞”。SHA中的碰撞几率小于MD5。但是,不要担心这些碰撞,因为它们真的非常罕见。
Java有4种SHA算法实现。与MD5(128位散列)相比,它们生成以下长度哈希值:
更长的哈希更难以打破。这是核心理念。
要获得算法的任何实现,请将其作为参数传递给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产生最强的哈希值。
到目前为止,我们学习了如何为密码创建安全哈希,并使用salt来使其更加安全。但是今天的问题是硬件变得如此之快以至于使用字典和彩虹表进行任何暴力攻击,任何密码都可以在更短的时间内被破解。
为了解决这个问题,一般的想法是使蛮力攻击更慢,以便可以最小化损害。我们的下一个算法适用于这个概念。目标是使哈希函数足够慢以阻止攻击,但仍然足够快以至于不会对用户造成明显的延迟。
此功能基本上是使用一些CPU密集型算法实现的,例如PBKDF2,Bcrypt或Scrypt。这些算法将工作因子(也称为安全因子)或迭代计数作为参数。此值确定散列函数的速度。当计算机明年变得更快时,我们可以增加工作因素来平衡它。
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
请注意参考上面代码示例中的函数。如果发现有任何困难,请下载教程末尾附带的源代码。
bcrypt背后的概念与PBKDF2中的先前概念类似。它恰好是java没有任何内置的bcrypt算法支持来使攻击更慢,但你仍然可以在源代码下载中找到一个这样的实现。
Java bcrypt with salt示例
我们来看一下示例用法代码(源代码中提供了BCrypt.java)。
public class BcryptHashingExample
{
public static void main(String[] args) throws NoSuchAlgorithmException
{
String originalPassword = "password";
String generatedSecuredPasswordHash = BCrypt.hashpw(originalPassword, BCrypt.gensalt(12));
System.out.println(generatedSecuredPasswordHash);
boolean matched = BCrypt.checkpw(originalPassword, generatedSecuredPasswordHash);
System.out.println(matched);
}
}
Output:
$2a$12$WXItscQ/FDbLKU4mO58jxu3Tx/mueaS8En3M6QOVZIZLaGdWrS.pK
true
|
与bcrypt类似,我从github下载了scrypt,并在源代码中添加了scrypt算法的源代码,以便在上一节下载。
Java以scrypt为例
让我们看看如何使用实现:
public class ScryptPasswordHashingDemo
{
public static void main(String[] args) {
String originalPassword = "password";
String generatedSecuredPasswordHash = SCryptUtil.scrypt(originalPassword, 16, 16, 16);
System.out.println(generatedSecuredPasswordHash);
boolean matched = SCryptUtil.check("password", generatedSecuredPasswordHash);
System.out.println(matched);
matched = SCryptUtil.check("passwordno", generatedSecuredPasswordHash);
System.out.println(matched);
}
}
Output:
$s0$41010$Gxbn9LQ4I+fZ/kt0glnZgQ==$X+dRy9oLJz1JaNm1xscUl7EmUFHIILT1ktYB5DQ3fZs=
true
false
要下载上述算法示例的源代码,请点击以下链接。
下载源代码
快乐学习!!
参考文献:
本文翻译自:https://howtodoinjava.com/security/how-to-generate-secure-password-hash-md5-sha-pbkdf2-bcrypt-examples/
作者 Lokesh Gupta