現在的MD5密碼資料庫的資料量已經非常龐大了,大部分常用密碼都可以通過MD5摘要反向查詢到密碼明文。為了防止內部人員(能夠接觸到資料庫或者Database Backup檔案的人員)和外部入侵者通過MD5反查密碼明文,更好地保護使用者的密碼和個人帳戶安全(一個使用者可能會在多個系統中使用同樣的密碼,因此涉及到使用者在其他網站和系統中的資料安全),需要對MD5摘要結果摻入其他資訊,稱之為加鹽。
加鹽的演算法有很多,考慮到加鹽的目的(防止擁有系統底層許可權的人員),想做到絕對不可反查是很困難的,需要有其他軟體或者硬體的協助,在很多情境下的實用性比較差。如果只是想增加反查的難度,倒是有很多方法可以選擇,一種便利的方法是md5(Password+UserName),即將使用者名稱和密碼字串相加再MD5,這樣的MD5摘要基本上不可反查。但有時候使用者名稱可能會發生變化,發生變化後密碼即不可用了(驗證密碼實際上就是再次計算摘要的過程)。
因此我們做了一個非常簡單的演算法,每次儲存密碼到資料庫時,都產生一個隨機16位元字,將這16位元字和密碼相加再求MD5摘要,然後在摘要中再將這16位元字按規則摻入形成一個48位的字串。在驗證密碼時再從48位字串中按規則提取16位元字,和使用者輸入的密碼相加再MD5。按照這種方法形成的結果肯定是不可直接反查的,且同一個密碼每次儲存時形成的摘要也都是不同的。如以下代碼所示:
Java代碼
- package com.zving.framework.security;
-
- import java.security.MessageDigest;
- import java.util.Random;
-
- import org.apache.commons.codec.binary.Hex;
-
- /**
- * @author wyuch
- * @email wyuch@zving.com
- * @date 2011-12-31
- */
- public class PasswordUtil {
- /**
- * 產生含有隨機鹽的密碼
- */
- 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);
- }
-
- /**
- * 校正密碼是否正確
- */
- 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摘要
- */
- public 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 static void main(String[] args) {
- String password = generate("admin");
- System.out.println(verify("admin", password));
- }
- }
package com.zving.framework.security;import java.security.MessageDigest;import java.util.Random;import org.apache.commons.codec.binary.Hex;/** * @author wyuch * @email wyuch@zving.com * @date 2011-12-31 */public class PasswordUtil {/** * 產生含有隨機鹽的密碼 */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);}/** * 校正密碼是否正確 */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摘要 */public 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 static void main(String[] args) {String password = generate("admin");System.out.println(verify("admin", password));}}
--------------------------回應樓下幾位的問題---------------------------
1、這是一個很簡單的演算法,和絕大部分加鹽演算法一樣,只是為了增加通過MD5資料庫反查(例如使用http://www.md5.cc/類似的反查資料庫)的難度,並不能保證絕對不可反查。
2、作者假定,能夠拿到一個應用中使用者密碼的MD5摘要的人,也能獲得該應用的密碼摘要形成的演算法,以及該演算法依賴的外部因素(持久層中的使用者ID、使用者名稱、加鹽規則、隨機資料等)。因此不管是什麼樣的演算法(例如多次MD5以及各種加鹽規則),破解者都可以通過將手頭的MD5反查庫根據你的演算法和資料重新運算一遍,以獲得針對你的使用者資料庫的專用的MD5反查庫,然後再逐一反查密碼明文。
3、考慮到使用者密碼驗證實際上是再次形成MD5摘要的過程,而使用者密碼驗證是一個經常發生的行為,會有高並發的情況。所以一般使用者密碼驗證不允許採用非常複雜和消耗效能的演算法,一次驗證消耗的CPU時間應在100毫秒之內。我們採用MD5或SHA1的原因一是久經考驗的摘要無法復原性;二是效能在允許的範圍之內(對於密碼這樣的短字串,一台普通PC每秒都可以運算上萬次)。
4、因為第3點提到的效能要求,所以根據你的演算法重新構建一個專用MD5反查庫的時間不會特別長(採用較好的機器和並行計算,可以在有意義的時間範圍之內重新形成有100億條以上記錄的MD5反查庫)。
5、根據以上2/3/4點,不管採用什麼加鹽演算法,只要演算法可以獲得,則破解者都可以破解任意一個使用者的密碼的明文(準確地說是驗證該密碼是否在MD5反查庫中存在,如果存在則能獲得明文。特別複雜的密碼在MD5反查庫中不會有記錄,因此即使不加鹽也無法破解)。
6、但多次MD5、打亂MD5結果的數位順序這些演算法的安全性要更差一些,便如多次MD5,不管你是幾次MD5,破解者只需要根據你演算法運算形成MD5反查庫,則一次性就破解了你的所有密碼。
7、本文中的演算法以及MD5(UserName+Password+Salt)、MD5(UserID+Password+Salt)等,是在密碼之外有引入了幹擾項(稱之為鹽),因此破解者必須為每一個密碼單獨形成一個MD5反查庫,代價就會非常高,要想破解所有密碼實際上已經不可行了。
8、但MD5(UserName+Password+Salt)要求UserName永遠不變,MD5(UserID+Password+Salt)要求使用者在使用者名稱之外必須有一個不變的UserID。這些額外的要求,導致其適用性沒有本文中的演算法好。
9、如果以上情況有一條你不需要考慮,則本演算法對你確實沒有什麼用處,就當作者自賣自誇灌水吧。因為本演算法有一點點聊勝於無的優勢,所以發個文章請大家斧正,對回帖的兄弟們表示感謝