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值是要存放在數據庫里的。
用戶注冊時,
- 用戶輸入【賬號】和【密碼】(以及其他用戶信息);
- 系統為用戶生成【Salt值】;
- 系統將【Salt值】和【用戶密碼】連接到一起;
- 對連接后的值進行散列,得到【Hash值】;
- 將【Hash值1】和【Salt值】分別放到數據庫中。
用戶登錄時,
- 用戶輸入【賬號】和【密碼】;
- 系統通過用戶名找到與之對應的【Hash值】和【Salt值】;
- 系統將【Salt值】和【用戶輸入的密碼】連接到一起;
- 對連接后的值進行散列,得到【Hash值2】(注意是即時運算出來的值);
- 比較【Hash值1】和【Hash值2】是否相等,相等則表示密碼正確,否則表示密碼錯誤。
有時候,為了減輕開發壓力,程序員會統一使用一個salt值(儲存在某個地方),而不是每個用戶都生成私有的salt值。
例子詳解:
第一代密碼
早期的軟件系統或者互聯網應用,數據庫中設計用戶表的時候,大致是這樣的結構:
數據存儲形式如下:
主要的關鍵字段就是這么兩個,一個是登陸時的用戶名,對應的一個密碼,而且那個時候的用戶名是明文存儲的,如果你登陸時用戶名是 123,那么數據庫里存的就是 123。這種設計思路非常簡單,但是缺陷也非常明顯,數據庫一旦泄露,那么所有用戶名和密碼都會泄露,后果非常嚴重。
第二代密碼
為了規避第一代密碼設計的缺陷,聰明的人在數據庫中不在存儲明文密碼,轉而存儲加密后的密碼,典型的加密算法是 MD5 和 SHA1,其數據表大致是這樣設計的:
數據存儲形式如下:
假如你設置的密碼是 123,那么數據庫中存儲的就是 202cb962ac59075b964b07152d234b70 或 40bd001563085fc35165329ea1ff5c5ecbdbbeef。當用戶登陸的時候,會把用戶輸入的密碼執行 MD5(或者 SHA1)后再和數據庫就行對比,判斷用戶身份是否合法,這種加密算法稱為散列。
嚴格地說,這種算法不能算是加密,因為理論上來說,它不能被解密。所以即使數據庫丟失了,但是由於數據庫里的密碼都是密文,根本無法判斷用戶的原始密碼,所以后果也不算太嚴重。
第三代密碼
本來第二代密碼設計方法已經很不錯了,只要你密碼設置得稍微復雜一點,就幾乎沒有被破解的可能性。但是如果你的密碼設置得不夠復雜,被破解出來的可能性還是比較大的。
好事者收集常用的密碼,然后對他們執行 MD5 或者 SHA1,然后做成一個數據量非常龐大的數據字典,然后對泄露的數據庫中的密碼就行對比,如果你的原始密碼很不幸的被包含在這個數據字典中,那么花不了多長時間就能把你的原始密碼匹配出來。這個數據字典很容易收集,CSDN 泄露的那 600w 個密碼,就是很好的原始素材。
於是,第三代密碼設計方法誕生,用戶表中多了一個字段:
數據存儲形式如下:
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)); } } }