Java Secure Hashing - MD5,SHA256,SHA512,PBKDF2,BCrypt,SCrypt
深入學習Java Secure Hashing算法。安全密碼哈希是施加於用戶提供的密碼一定的算法和操作,其通常非常弱,容易猜測之後獲得的字符的加密序列。
Java中有許多這樣的哈希算法可以證明對密碼安全性非常有效。
請記住,一旦生成此密碼哈希並將其存儲在數據庫中,您就無法將其轉換回原始密碼。
每次用戶登錄應用程序時,您都必須再次重新生成密碼哈希並與存儲在數據庫中的哈希相匹配。因此,如果用戶忘記了他/她的密碼,您將不得不向他發送一個臨時密碼,並要求他用新密碼更改密碼。現在很常見,對嗎?
目錄
使用PBKDF2WithHmacSHA1算法的高級密碼安全性
使用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會產生相當弱的哈希值。它的主要優點是速度快,易於實施。但這也意味着它容易受到 蠻力和字典攻擊。
生成單詞和散列的彩虹表允許非常快速地搜索已知散列並獲得原始單詞。
MD5 不是抗衝突的,這意味着不同的密碼最終會導致相同的哈希。
今天,如果您在應用程序中使用MD5哈希,請考慮在安全性中添加一些鹽。
使用salt使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算法的中密碼安全性
該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來使其更加安全。但是今天的問題是硬件變得如此之快以至於使用字典和彩虹表進行任何暴力攻擊,任何密碼都可以在更短的時間內被破解。
爲了解決這個問題,一般的想法是使蠻力攻擊更慢,以便可以最小化損害。我們的下一個算法適用於這個概念。目標是使哈希函數足夠慢以阻止攻擊,但仍然足夠快以至於不會對用戶造成明顯的延遲。
此功能基本上是使用一些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和scrypt算法進行更安全的密碼哈希
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
最後的筆記
- 使用散列存儲文本密碼對於當今的應用程序安全性來說是最危險的。
- MD5提供用於生成安全密碼哈希的基本哈希。加鹽使其更加強大。
- MD5生成128位哈希。爲了使ti更安全,使用SHA算法生成160位到512位長的哈希值。512位是最強的。
- 即使SHA散列安全密碼也能夠通過當今的快速硬件進行破解。要想達到這個目標,您將需要能夠使暴力攻擊更慢並最小化影響的算法。這些算法是PBKDF2,BCrypt和SCrypt。
- 在應用適當的安全算法之前,請仔細考慮。
要下載上述算法示例的源代碼,請點擊以下鏈接。
快樂學習!!
參考文獻:
- https://en.wikipedia.org/wiki/MD5
- https://en.wikipedia.org/wiki/Secure_Hash_Algorithm
- http://en.wikipedia.org/wiki/Bcrypt
- http://en.wikipedia.org/wiki/Scrypt
- http://en.wikipedia.org/wiki/PBKDF2
- https://github.com/wg/scrypt
- http://www.mindrot.org/projects/jBCrypt/
作者 Lokesh Gupta