天天看點

MD5算法+鹽Salt

1、MD算法的基的概念

   MD5算法是典型的消息摘要算法,其前身有MD2、MD3和MD4算法,它由MD4、MD3和MD2算法改進而來。不論是哪一種MD算法,它們都需 要獲得一個随機長度的資訊并産生一個128位的資訊摘要。如果将這個128位的二進制摘要資訊換算成十六進制,可以得到一個32位的字元串,故我們見到的 大部分MD5算法的數字指紋都是32為十六進制的字元串。

2、MD算法的發展史

2.1 MD2算法

   1989年,著名的非對稱算法RSA發明人之一----麻省理工學院教授羅納德.李維斯特開發了MD2算法。這個算法首先對資訊進行資料補位,使信 息的位元組長度是16的倍數。再以一個16位的檢驗和做為補充資訊追加到原資訊的末尾。最後根據這個新産生的資訊計算出一個128位的散列值,MD2算法由 此誕生。

2.2 MD4算法

   1990年,羅納德.李維斯特教授開發出較之MD2算法有着更高安全性的MD4算法。在這個算法中,我們仍需對資訊進行資料補位。不同的是,這種補 位使其資訊的位元組長度加上448個位元組後成為512的倍數(資訊位元組長度mod 512 =448)。此外,關于MD4算的處理和MD2算法有很大的差别。但最終仍舊會獲得一個128為的散列值。MD4算法對後續消息摘要算法起到了推動作用, 許多比較有名的消息摘要算法都是在MD4算法的基礎上發展而來的,如MD5、SHA-1、RIPE-MD和HAVAL算法等。

2.3 MD5算法

   1991年,繼MD4算法後,羅納德.李維斯特教授開發了MD5算法,将MD算法推向成熟。MD5算法經MD2、MD3和MD4算法發展而來,算法複雜程度和安全強度打打提高,但浙西MD算法的最終結果都是産生一個128位的資訊摘要。這也是MD系列算法的特點。MD5算法的算法特點如下: (1)壓縮性:任意長度的資料,算出的MD5值長度都是固定的。 (2)容易計算:從原資料計算出MD5值很容易。 (3)抗修改性:對原資料進行任何改動,哪怕隻修改1個位元組,所得到的MD5值都有很大差別。 (4)弱抗碰撞:已知原資料和其MD5值,想找到一個具有相同MD5值的資料(即僞造資料)是非常困難的。 (5)強抗碰撞:想找到兩個不同的資料,使它們具有相同的MD5值,是非常困難的。

2.4、MD5破解方面

   在破解md5方面,最常用的方法是“跑字典”,有兩種方法得到字典,一種是日常搜集的用做密碼的字元串表,另一種是用排列組合方法生成的,先用MD5程式計算出這些字典項的MD5值,然後再用目标的MD5值在這個字典中檢索。我們假設密碼的最大長度為8位位元組(8 Bytes),同時密碼隻能是字母和數字,共26+26+10=62個位元組,排列組合出的字典的項數則是P(62,1)+P(62,2)….+P(62,8),那也已經是一個很天文的數字了,存儲這個字典就需要TB級的磁盤陣列,而且這種方法還有一個前提,就是能獲得目标賬戶的密碼MD5值的情況下才可以。

是以總體而言,md5加密是十分安全的,即使有一些瑕疵,但并不影響具體的使用,外加md5是免費的,是以它的應用還是十分廣泛的。

3、MD5算法應用

3.1、Md5 密碼存儲加鹽

    MD5算法,可以用來儲存使用者的密碼資訊。為了更好的儲存,可以在儲存的過程中,加入鹽。/在儲存使用者密碼的時候,鹽可以利用生成的随機數。可以将密碼結合MD5加鹽,生成的資料摘要和鹽儲存起來 。以便于下次使用者驗證使用。在使用者表裡面,也儲存salt。

3.2、Md5 檔案完整性校驗

    每個檔案都可以用MD5驗證程式算出一個固定的MD5值,是獨一無二的。一般來說,開發方會在軟體釋出時預先算出檔案的MD5值,如果檔案被盜用,加了木馬或者被篡改版權,那麼它的MD5值也随之改變,也就是說我們對比檔案目前的MD5值和它标準的MD5值來檢驗它是否正确和完整。 (1)例如網盤中的秒傳4G檔案,可以使用使用者需要上傳的檔案進行Md5運算,判斷與伺服器中是否存在該檔案,如果存在隻需添加檔案索引,不存在再真正上傳。 (2)例如自動更新的用戶端,判斷下載下傳的程式安裝包是否完整,可以計算檔案的MD5值,與伺服器端計算的Md5值進行比對。

4、MD5加鹽

  我們知道,如果直接對密碼進行散列,那麼黑客可以對通過獲得這個密碼散列值,然後通過查散列值字典(例如MD5密碼破解網站),得到某使用者的密碼。

  加Salt可以一定程度上解決這一問題。所謂加Salt方法,就是加點“佐料”。其基本想法是這樣的:當使用者首次提供密碼時(通常是注冊時),由系統自動往這個密碼裡撒一些“佐料”,然後再散列。而當使用者登入時,系統為使用者提供的代碼撒上同樣的“佐料”,然後散列,再比較散列值,已确定密碼是否正确。

  這裡的“佐料”被稱作“Salt值”,這個值是由系統随機生成的,并且隻有系統知道。這樣,即便兩個使用者使用了同一個密碼,由于系統為它們生成的salt值不同,他們的散列值也是不同的。即便黑客可以通過自己的密碼和自己生成的散列值來找具有特定密碼的使用者,但這個幾率太小了(密碼和salt值都得和黑客使用的一樣才行)。

下面詳細介紹一下加Salt散列的過程。介紹之前先強調一點,前面說過,驗證密碼時要使用和最初散列密碼時使用“相同的”佐料。是以Salt值是要存放在資料庫裡的。

使用者注冊時,

  1. 使用者輸入【賬号】和【密碼】(以及其他使用者資訊);
  2. 系統為使用者生成【Salt值】;
  3. 系統将【Salt值】和【使用者密碼】連接配接到一起;
  4. 對連接配接後的值進行散列,得到【Hash值】;
  5. 将【Hash值1】和【Salt值】分别放到資料庫中。

使用者登入時,

  1. 使用者輸入【賬号】和【密碼】;
  2. 系統通過使用者名找到與之對應的【Hash值】和【Salt值】;
  3. 系統将【Salt值】和【使用者輸入的密碼】連接配接到一起;
  4. 對連接配接後的值進行散列,得到【Hash值2】(注意是即時運算出來的值);
  5. 比較【Hash值1】和【Hash值2】是否相等,相等則表示密碼正确,否則表示密碼錯誤。

有時候,為了減輕開發壓力,程式員會統一使用一個salt值(儲存在某個地方),而不是每個使用者都生成私有的salt值。

例子詳解:

第一代密碼

 早期的軟體系統或者網際網路應用,資料庫中設計使用者表的時候,大緻是這樣的結構:

MD5算法+鹽Salt

  資料存儲形式如下:

MD5算法+鹽Salt

 主要的關鍵字段就是這麼兩個,一個是登陸時的使用者名,對應的一個密碼,而且那個時候的使用者名是明文存儲的,如果你登陸時使用者名是 123,那麼資料庫裡存的就是 123。這種設計思路非常簡單,但是缺陷也非常明顯,資料庫一旦洩露,那麼所有使用者名和密碼都會洩露,後果非常嚴重。 

第二代密碼

 為了規避第一代密碼設計的缺陷,聰明的人在資料庫中不在存儲明文密碼,轉而存儲加密後的密碼,典型的加密算法是 MD5 和 SHA1,其資料表大緻是這樣設計的:

MD5算法+鹽Salt

 資料存儲形式如下: 

MD5算法+鹽Salt

 假如你設定的密碼是 123,那麼資料庫中存儲的就是 202cb962ac59075b964b07152d234b70 或 40bd001563085fc35165329ea1ff5c5ecbdbbeef。當使用者登陸的時候,會把使用者輸入的密碼執行 MD5(或者 SHA1)後再和資料庫就行對比,判斷使用者身份是否合法,這種加密算法稱為散列。

 嚴格地說,這種算法不能算是加密,因為理論上來說,它不能被解密。是以即使資料庫丢失了,但是由于資料庫裡的密碼都是密文,根本無法判斷使用者的原始密碼,是以後果也不算太嚴重。

第三代密碼

 本來第二代密碼設計方法已經很不錯了,隻要你密碼設定得稍微複雜一點,就幾乎沒有被破解的可能性。但是如果你的密碼設定得不夠複雜,被破解出來的可能性還是比較大的。

 好事者收集常用的密碼,然後對他們執行 MD5 或者 SHA1,然後做成一個資料量非常龐大的資料字典,然後對洩露的資料庫中的密碼就行對比,如果你的原始密碼很不幸的被包含在這個資料字典中,那麼花不了多長時間就能把你的原始密碼比對出來。這個資料字典很容易收集,CSDN 洩露的那 600w 個密碼,就是很好的原始素材。

 于是,第三代密碼設計方法誕生,使用者表中多了一個字段:

MD5算法+鹽Salt

 資料存儲形式如下:

MD5算法+鹽Salt

  Salt 可以是任意字母、數字、或是字母或數字的組合,但必須是随機産生的,每個使用者的 Salt 都不一樣,使用者注冊的時候,資料庫中存入的不是明文密碼,也不是簡單的對明文密碼進行散列,而是 MD5( 明文密碼 + Salt),也就是說: 

MD5('123' + '1ck12b13k1jmjxrg1h0129h2lj') = '6c22ef52be70e11b6f3bcf0f672c96ce'
MD5('456' + '1h029kh2lj11jmjxrg13k1c12b') = '7128f587d88d6686974d6ef57c193628'      

  由于加了 Salt,即便資料庫洩露了,但是由于密碼都是加了 Salt 之後的散列,壞人們的資料字典已經無法直接比對,明文密碼被破解出來的機率也大大降低。

  是不是加了 Salt 之後就絕對安全了呢?淡然沒有!壞人們還是可以他們資料字典中的密碼,加上我們洩露資料庫中的 Salt,然後散列,然後再比對。但是由于我們的 Salt 是随機産生的,假如我們的使用者資料表中有 30w 條資料,資料字典中有 600w 條資料,壞人們如果想要完全覆寫的壞,他們加上 Salt 後再散列的資料字典資料量就應該是 300000* 6000000 = 1800000000000,一萬八千億啊,幹壞事的成本太高了吧。但是如果隻是想破解某個使用者的密碼的話,隻需為這 600w 條資料加上 Salt,然後散列比對。可見 Salt 雖然大大提高了安全系數,但也并非絕對安全。

  實際項目中,Salt 不一定要加在最前面或最後面,也可以插在中間嘛,也可以分開插入,也可以倒序,程式設計時可以靈活調整,都可以使破解的難度指數級增長。

MD5加鹽實作

import java.util.Random;
import org.apache.commons.codec.binary.Hex;
import java.security.NoSuchAlgorithmException;
import java.security.MessageDigest;
 
/**
 * MD5工具類,加鹽
 */
public class MD5Util {
 
    /**
     * 普通MD5
     * @param inStr
     * @return
     */
    public static String MD5(String input) {
        MessageDigest md5 = null;
        try {
            md5 = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            return "check jdk";
        } catch (Exception e) {
            e.printStackTrace();
            return "";
        }
        char[] charArray = input.toCharArray();
        byte[] byteArray = new byte[charArray.length];
 
        for (int i = 0; i < charArray.length; i++)
            byteArray[i] = (byte) charArray[i];
        byte[] md5Bytes = md5.digest(byteArray);
        StringBuffer hexValue = new StringBuffer();
        for (int i = 0; i < md5Bytes.length; i++) {
            int val = ((int) md5Bytes[i]) & 0xff;
            if (val < 16)
                hexValue.append("0");
            hexValue.append(Integer.toHexString(val));
        }
        return hexValue.toString();
 
    }
 
     
     
     
    /**
     * 加鹽MD5
     * @param password
     * @return
     */
        public static String generate(String password) {
            Random r = new Random();
             StringBuilder sb = new StringBuilder(16);
             sb.append(r.nextInt(99999999)).append(r.nextInt(99999999));
             int len = sb.length();
             if (len < 16) {
                 for (int i = 0; i < 16 - len; i++) {
                     sb.append("0");
                 }
             }
             String salt = sb.toString();
             password = md5Hex(password + salt);
             char[] cs = new char[48];
             for (int i = 0; i < 48; i += 3) {
                 cs[i] = password.charAt(i / 3 * 2);
                 char c = salt.charAt(i / 3);
                 cs[i + 1] = c;
                 cs[i + 2] = password.charAt(i / 3 * 2 + 1);
             }
            return new String(cs);
        }
 
        /**
         * 校驗加鹽後是否和原文一緻
         * @param password
         * @param md5
         * @return
         */
        public static boolean verify(String password, String md5) {
             char[] cs1 = new char[32];
            char[] cs2 = new char[16];
            for (int i = 0; i < 48; i += 3) {
                cs1[i / 3 * 2] = md5.charAt(i);
                cs1[i / 3 * 2 + 1] = md5.charAt(i + 2);
                cs2[i / 3] = md5.charAt(i + 1);
            }
            String salt = new String(cs2);
            return md5Hex(password + salt).equals(new String(cs1));
        }
 
        /**
         * 擷取十六進制字元串形式的MD5摘要
         */
        private static String md5Hex(String src) {
            try {
                MessageDigest md5 = MessageDigest.getInstance("MD5");
                byte[] bs = md5.digest(src.getBytes());
                return new String(new Hex().encode(bs));
            } catch (Exception e) {
                return null;
            }
        }          
}      

測試類:

public class Zmain {
 
    // 測試主函數
    public static void main(String args[]) {
        // 原文
        String plaintext = "DingSai";
    //  plaintext = "123456";
        System.out.println("原始:" + plaintext);
        System.out.println("普通MD5後:" + MD5Util.MD5(plaintext));
 
        // 擷取加鹽後的MD5值
        String ciphertext = MD5Util.generate(plaintext);
        System.out.println("加鹽後MD5:" + ciphertext);
        System.out.println("是否是同一字元串:" + MD5Util.verify(plaintext, ciphertext));
        /**
         * 其中某次DingSai字元串的MD5值
         */
        String[] tempSalt = { "c4d980d6905a646d27c0c437b1f046d4207aa2396df6af86", "66db82d9da2e35c95416471a147d12e46925d38e1185c043", "61a718e4c15d914504a41d95230087a51816632183732b5a" };
 
        for (String temp : tempSalt) {
            System.out.println("是否是同一字元串:" + MD5Util.verify(plaintext, temp));
        }            
        
    }
}