论坛首页 Java企业应用论坛

一种简单的给MD5加盐算法

浏览 31931 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (3)
作者 正文
   发表时间:2012-10-19   最后修改:2012-10-25
现在的MD5密码数据库的数据量已经非常庞大了,大部分常用密码都可以通过MD5摘要反向查询到密码明文。为了防止内部人员(能够接触到数据库或者数据库备份文件的人员)和外部入侵者通过MD5反查密码明文,更好地保护用户的密码和个人帐户安全(一个用户可能会在多个系统中使用同样的密码,因此涉及到用户在其他网站和系统中的数据安全),需要对MD5摘要结果掺入其他信息,称之为加盐。

加盐的算法有很多,考虑到加盐的目的(防止拥有系统底层权限的人员),想做到绝对不可反查是很困难的,需要有其他软件或者硬件的协助,在很多场景下的实用性比较差。如果只是想增加反查的难度,倒是有很多方法可以选择,一种便利的方法是md5(Password+UserName),即将用户名和密码字符串相加再MD5,这样的MD5摘要基本上不可反查。但有时候用户名可能会发生变化,发生变化后密码即不可用了(验证密码实际上就是再次计算摘要的过程)。

因此我们做了一个非常简单的算法,每次保存密码到数据库时,都生成一个随机16位数字,将这16位数字和密码相加再求MD5摘要,然后在摘要中再将这16位数字按规则掺入形成一个48位的字符串。在验证密码时再从48位字符串中按规则提取16位数字,和用户输入的密码相加再MD5。按照这种方法形成的结果肯定是不可直接反查的,且同一个密码每次保存时形成的摘要也都是不同的。如以下代码所示:

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、如果以上情况有一条你不需要考虑,则本算法对你确实没有什么用处,就当作者自卖自夸灌水吧。因为本算法有一点点聊胜于无的优势,所以发个帖子请大家斧正,对回帖的兄弟们表示感谢。
   发表时间:2012-10-19  
改了一下,不需要引用其他类了。
0 请登录后投票
   发表时间:2012-10-19   最后修改:2012-10-19
要防止MD5解密办法多的是。
比如加盐、多次加密、打乱顺序、添加干扰位等。

一般情况下对原字符串MD5加密后打乱顺序就行了。即保密又不会增加MD5字符串的长度。
0 请登录后投票
   发表时间:2012-10-19  
mfkvfn 写道
要防止MD5解密办法多的是。
比如加盐、多次加密、打乱顺序、添加干扰位等。

一般情况下对原字符串MD5加密后打乱顺序就行了。即保密又不会增加MD5字符串的长度。


我想加入点随机性,让同一个密码形成不同的MD5结果,以避免破解了一个就破解了所有相同的密码。
0 请登录后投票
   发表时间:2012-10-19  
别人如果知道打乱的顺序了当然可以破解。
同样的,如果你的方法,别人知道了哪几位是盐,还不是一样可以破解。
0 请登录后投票
   发表时间:2012-10-19  
mfkvfn 写道
别人如果知道打乱的顺序了当然可以破解。
同样的,如果你的方法,别人知道了哪几位是盐,还不是一样可以破解。


额,肯定是都能破解的,如果密码比较常见的话,别人根据算法和MD5数据库中的记录重新跑一遍,就能把你的MD5结果和他的数据库中的结果中己有记录匹配上,只是个运算时间的问题。

我的意思是说如果有N个用户都用某个常用密码(便如123456),则打乱顺序这N个用户只需要计算一次就行了,如果有随机性,则需要运算N次。

算是聊胜于无吧。
0 请登录后投票
   发表时间:2012-10-19  
为什么不选择多次加密呢?
0 请登录后投票
   发表时间:2012-10-19   最后修改:2012-10-19
原来有个朋友介绍的一种方法,不算很保险,但也没太大问题。
同样也是UserName+PassWord再MD5后,截取固定长度。
就是不保存原始的MD5,不影响验证,也最简单。
0 请登录后投票
   发表时间:2012-10-20  
我觉的有一种方法还是比较好的;
假设:user(账户)、passwd(密码)、security(盐码6-8)
security(盐码):默认是123456;
userpassword:passwd和security加密后的字符串;

数据库保存的是user、userpassword;

user、security和passwd由用户存放;

用户验证时:
输入user、passwd、security;

系统验证账户密码时:根据passwd和security生成的加密字符串,和数据库保存的密码比对即可;

客户的原始密码就不能根据数据库中存的破解了;
0 请登录后投票
   发表时间:2012-10-20  
作为一个加密函数,你这程序写的性能未免有点低。

首先你Salt补零的过程用循环就会是一个问题。你为什么不初始化一个用16个‘0’的字符串初始化一个StringBuffer,然后再作后面的工作就可以了。

对于后面48位密码产生也是同样的道理。

还有基本上你这种加密没什么意义,因为48位密码的混合顺序是固定的(人为的),而不是consistent的随机产生。
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics