安全密码哈希是施加于用户提供的密码一定的算法和操作,其通常非常弱,容易猜测之后获得的字符的加密序列。
Java中有许多这样的哈希算法,它们可以证明确实对密码安全有效。
请记住,一旦生成此密码哈希并将其存储在数据库中,就无法将其转换回原始密码。
每次用户登录到应用程序时,您都必须再次重新生成密码哈希,并与存储在数据库中的哈希匹配。因此,如果用户忘记了他/她的密码,则您将不得不向他发送一个临时密码,并要求他使用新密码进行更改。如今很常见吧?
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的关系。
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(安全散列算法)是加密散列函数族。它与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,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<bytes.length ;i++) { bytes[i] = (byte)Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16); } return bytes; } |
请注意引用上述代码示例中的函数。如果发现任何困难,请下载教程末尾随附的源代码。
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(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 |
- 对于应用程序安全而言,使用哈希存储文本密码是最危险的事情。
- MD5提供了用于生成安全密码哈希的基本哈希。加盐使其更坚固。
- MD5生成128位哈希。为了使ti更安全,请使用SHA算法,该算法生成从160位到512位长的哈希。512位最强。
- 甚至SHA散列的安全密码也可以通过当今的快速硬件来破解。为了克服这一点,您将需要可以使蛮力攻击速度变慢并使影响最小化的算法。这样的算法是PBKDF2,BCrypt和SCrypt。
- 在应用适当的安全算法之前,请仔细考虑。