如何生成安全的密碼 Hash:MD5, SHA, PBKDF2, BCrypt 示例


密碼 Hash 值的產生是將用戶所提供的密碼通過使用一定的算法計算后得到的加密字符序列。在 Java 中提供很多被證明能有效保證密碼安全的 Hash 算法實現,我將在這篇文章中討論其中的部分算法。

需要注意的是,一旦生成密碼的 Hash 值並存儲在數據庫中后,你將不可能再把它轉換回密碼明文。只能每次用戶在登錄到應用程序時,須重新生成 Hash 值與數據庫中的 Hash 值匹配來完成密碼的校驗。

 

簡單的密碼安全實現使用 MD5 算法

MD5消息摘要算法(MD5 Message-Digest Algorithm)是一種廣泛使用的加密 Hash 函數,主要用於生成一個128bit(16byte) Hash 值。它的實現思路非常簡單且易懂,其最基本的思路是將可變長度的數據集映射為固定長度的數據集。為了做到這一點,它將輸入消息分割成 512-bit 的數據塊。通過填補消息到末尾以確保其長度能除以512。現在這些塊將通過 MD5 算法處理,其結果將是一個128位的散列值。使用MD5后,生成的散列值通常是32位16進制的數字

在該文中,對於被加密的密碼明文被稱為“消息(message)”,其加密后所生成的 Hash 值稱為 “消息摘要(message digest)” 或簡稱 "摘要(digest)"。以下是 MD5 產生 Hash 值的代碼示例:

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:

f4dcc3b5aa765d61d8327deb882cf99 

雖然 MD5 是一個廣為流傳的 Hash 算法, 但它並不安全且所生成的 Hash 值也是相當的薄弱。它主要的優點在於生成速度快且易於實現。但是,這也意味着它是容易被暴力攻擊和字典攻擊。例如使用明文和 Hash 生成的彩虹表可以快速地搜索已知 Hash 對應的原數據。

此外,MD5 並沒有避免 Hash 碰撞:這意味不同的密碼會導致生成相同的 Hash 值。

不過,如果你仍然需要使用 MD5,可以考慮為其加 salt 來進一步保證它的安全性。

使用 salt 讓生成的 MD5 更加安全

這里需要注意的是,加 salt 並不是 MD5 所特有的, 你同樣可以把它應用在其它算法中。所以,在這里你只需關注它是如何應用而不是它與 MD5 的聯系。

在 Wikipedia 上對 salt 的定義是通過一個單向函數獲取隨機數據來為密碼或口令添加一些額外的數據。更簡單的說法則是通過生成一些隨機的文本將其附加到密碼上來生成 Hash

為 Hash 加 salt 的主要目的是用來防止預先被計算好的彩虹表攻擊。現在加 slat 好處是將原本一次比較變為多次比較從而減慢對密碼 Hash 值的猜測,否則對 Hash 密碼庫的破解效率將是非常之高。

重要的是:在 Java 中,我們總是需要使用 SecureRandom 來生成一個好的 salt 值,因此可以利用 SecureRandom 類所提供 “SHA1PRNG” 算法來生成偽隨機數。代碼如下所示:

private static String 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.toString();
}

SHA1PRNG 算法是基於 SHA-1 算法實現且保密性較強的偽隨機數生成器。要注意的是,如果不為它提供隨機數種子,它將會通過真隨機數來生成一個種子(TRNG)。 譯注:關於 TRGN 和 PRGN。

接下來,看看為 MD5 Hash 加 slat 的代碼示例:

public class SaltedMD5Example
{
    public static void main(String[] args) throws NoSuchAlgorithmException, NoSuchProviderException
    {
        String passwordToHash = "password";
        String 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, String salt)
    {
        String generatedPassword = null;
        try {
            // Create MessageDigest instance for MD5
            MessageDigest md = MessageDigest.getInstance("MD5");
            //Add password bytes to digest
            md.update(salt.getBytes());
            //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 String 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.toString();
    }
}

重要提示:請注意,現在你必須每一個密碼 Hash 存儲它 slat 值。因為當用戶登錄系統,你必須使用最初生成的 slat 來再次生成 Hash 與存儲的 Hash 進行匹配。如果使用不同的 slat(生成隨機 slat)那么生成的 Hash 將不同。

此外,你可能聽說過多次 Hash 和加 salt。通常就是指創建自定義的組合,如:

salt+password+salt => hash

實際上你沒必要這樣去做,因為這並不能幫助你進一步鞏固 Hash 的安全性。如果你需要更高的安全性,正確的做法應該去選擇一個更好的算法。

中等的密碼安全實現使用SHA算法

SHA(安全散列算法)同樣是作為加密 Hash 函數家族中的一員。除了它生成的 Hash 安全性比 MD5 更強之外其它都與 MD5 非常類似。然而,它們生成的 Hash 並不總是唯一的,這意味着輸入兩個不同的值所獲的 Hash 卻是相同的。通常這種情況的發生我們稱之為“碰撞”。 不過,SHA 碰撞的幾率小於 MD5。你甚至無需擔心碰撞的發生,因為這種情況非常罕見。

在 Java 中有提供有4種 SHA 算法的實現,相對 MD5(128 bit hash) 它提供了以下長度的 Hash:

  • SHA-1 (簡單實現 – 160 bits Hash)
  • SHA-256 (強於 SHA-1 – 256 bits Hash)
  • SHA-384 (強於 SHA-256 – 384 bits Hash)
  • SHA-512 (強於 SHA-384 – 512 bits Hash)

通常越長的 Hash 越難破解,這是核心思想。

要獲取相應算法實現,可通過參數的方式傳給 MessageDigest 來獲得實例。如下所示:

MessageDigest md = MessageDigest.getInstance("SHA-1");
//OR
MessageDigest md = MessageDigest.getInstance("SHA-256");

下面通過測試程序來看 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";
        String 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, String salt)
    {
        String generatedPassword = null;
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            md.update(salt.getBytes());
            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, String salt)
    {
        //Use MessageDigest md = MessageDigest.getInstance("SHA-256");
    }

    private static String get_SHA_384_SecurePassword(String passwordToHash, String salt)
    {
        //Use MessageDigest md = MessageDigest.getInstance("SHA-384");
    }

    private static String get_SHA_512_SecurePassword(String passwordToHash, String salt)
    {
        //Use MessageDigest md = MessageDigest.getInstance("SHA-512");
    }

    //Add salt
    private static String getSalt() throws NoSuchAlgorithmException
    {
        SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
        byte[] salt = new byte[16];
        sr.nextBytes(salt);
        return salt.toString();
    }
}

Output:
// SHA-1
e4c53afeaa7a08b1f27022abd443688c37981bc4
// SHA-256
adfd14a7a89b201bf6d99105b417287db6581d8aee989076bb7f86154e8f32
// SHA-384
bc5914fe3896ae8a2c43a4513f2a0d716974cc305733847e3d49e1ea52d1ca50e2a9d0ac192acd43facfb422bb5ace88
// SAH-512
b8f7af61994670d03d25d55cc9cd1cff8d57bb799c4b586891e112b197530c76744bcd7ef135b58d47d65a0bec221eb5d77793956cf2709dd012

如上代碼所示,在使用 SHA 的同時同樣可以為其加 salt 來加強它的安全性。

較高密碼安全實現使用 PBKDF2WithHmacSHA1 算法

到目前為止,我們已經了解如何為密碼生成安全的 Hash 值以及通過利用 salt 來加強它的安全性。但今天的問題是,硬件的速度已經遠遠超過任何使用字典或彩虹表進行的暴力攻擊,並且任何密碼都能被破解,只是使用時間多少的問題。

為了解決這個問題,主要想法是盡可能降低暴力攻擊速度來保證最小化的損失。我們下一個算法同樣是基於這個概念。目標是使 Hash 函數足夠慢以妨礙攻擊,並對用戶來說仍然非常快且不會感到有明顯的延時。

要達到這個目的通常是使用某些 CPU 密集型算法來實現,比如 PBKDF2, Bcrypt 或 Scrypt 。這些算法采用 work factor(也稱之為 security factor)或迭代次數作為參數來確定 Hash 函數將變的有多慢,並且隨着日后計算能力的提高,可以逐步增大 work factor 來使之與計算能力達到平衡。

Java 中可通過 "PBKDF2WithHmacSHA1" 來實現"PBKDF2"算法,下面代碼是使用示例:

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().getBytes();
     
    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 String getSalt() throws NoSuchAlgorithmException
{
    SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
    byte[] salt = new byte[16];
    sr.nextBytes(salt);
    return salt.toString();
}
 
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:
     
: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 算法實現,但你仍然可以找到並下載它的源碼。

下以是 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 算法的源代碼並添加到在最后一節中的源碼下載中。看看它是如何使用:

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

最后說明

  1. 在應用程序中存儲密碼明文是極其危險的事情。
  2. MD5 提供了最基本的安全 Hash 生成,使用時應為其添加 slat 來進一步加強它的安全性。
  3. MD5 生成128位的 Hash。為了使它更安全,應該使用 SHA 算法生成 160-bit 或 512-bit 的長 Hash,其中 512-bit 是最強的。
  4. 雖然使用 SHA Hash 密碼也能被當今快速的硬件破解,如要避免這一點,你需要的算法是能讓暴力攻擊盡可能的變慢且使影響減至最低。這時候你可以使用 PBKDF2, BCrypt 或 SCrypt 算法。
  5. 請在深思熟慮后來選擇適當的安全算法。

參考資料:

  • 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/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM