一、说明
MD5消息摘要算法,属Hash算法一类。MD5算法对输入任意长度的消息进行运行,产生一个128位的消息摘要(32位的数字字母混合码)。
二、主要特点
不可逆,相同数据的MD5值肯定一样,不同数据的MD5值不一样
(一个MD5理论上的确是可能对应无数多个原文的,因为MD5是有限多个的而原文可以是无数多个。比如主流使用的MD5将任意长度的“字节串映射为一个128bit的大整数。也就是一共有2128种可能,大概是3.4*1038,这个数字是有限多个的,而但是世界上可以被用来加密的原文则会有无数的可能性)
MD5的性质:
1、压缩性:任意长度的数据,算出的MD5值长度都是固定的(相当于超损压缩)。
2、容易计算:从原数据计算出MD5值很容易。
3、抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。
4、弱抗碰撞:已知原数据和其MD5值,想找到一个具有相同MD5值的数据(伪造数据)非常困难。
5、强抗碰撞:想找到两个不同的数据,使它们具有相同的MD5值,是非常困难的。
虽说MD5有不可逆的特点;但是由于某些MD5激活成功教程网站,专门用来查询MD5码,其通过把常用的密码先MD5处理,并将数据存储起来,然后跟需要查询的MD5结果匹配,这时就有可能通过匹配的MD5得到明文,所以有些简单的MD5码是反查到加密前原文的。
为了让MD5码更加安全,涌现了很多其他方法,如加盐。 盐要足够长足够乱 得到的MD5码就很难查到。
三、使用场景
1.防止被篡改
-
比如发送一个电子文档,发送前,我先得到MD5的输出结果a。然后在对方收到电子文档后,对方也得到一个MD5的输出结果b。如果a与b一样就代表中途未被篡改。
-
比如我提供文件下载,为了防止不法分子在安装程序中添加木马,我可以在网站上公布由安装文件得到的MD5输出结果。
-
SVN在检测文件是否在CheckOut后被修改过,也是用到了MD5.
2.防止直接看到明文
现在很多网站在数据库存储用户的密码的时候都是存储用户密码的MD5值。这样就算不法分子得到数据库的用户密码的MD5值,也无法知道用户的密码。(比如在UNIX系统中用户的密码就是以MD5(或其它类似的算法)经加密后存储在文件系统中。当用户登录的时候,系统把用户输入的密码计算成MD5值,然后再去和保存在文件系统中的MD5值进行比较,进而确定输入的密码是否正确。通过这样的步骤,系统在并不知道用户密码的明码的情况下就可以确定用户登录系统的合法性。这不但可以避免用户的密码被具有系统管理员权限的用户知道,而且还在一定程度上增加了密码被激活成功教程的难度。)
3.防止抵赖(数字签名)
这需要一个第三方认证机构。例如A写了一个文件,认证机构对此文件用MD5算法产生摘要信息并做好记录。若以后A说这文件不是他写的,权威机构只需对此文件重新产生摘要信息,然后跟记录在册的摘要信息进行比对,相同的话,就证明是A写的了。这就是所谓的“数字签名”。
四、密码增强
MD5 加盐(salt)和不加盐在安全性上有着显著的差异。MD5 是一种哈希算法,用于将任意长度的输入数据转换为固定长度的输出(通常为128位的哈希值)。然而,MD5 在设计时就存在一些安全性问题,例如碰撞攻击,即两个不同的输入可以产生相同的输出哈希值。
因此,MD5 不再被推荐用于安全敏感的应用,如密码存储。
原理
加盐是指在输入数据之前,先添加一个随机的、唯一的字符串(即“盐”)。这个盐在哈希计算过程中与输入数据一起被哈希。加盐的主要目的是增加哈希值的唯一性,使得即使两个输入数据相同,只要它们的盐不同,生成的哈希值也会不同。这可以显著降低碰撞攻击的可能性。
-
增加唯一性:通过使用不同的盐,即使两个用户输入了相同的密码,它们的哈希值也会因为盐的不同而不同。这使得暴力破解和字典攻击变得更加困难。
-
防止彩虹表攻击:彩虹表是一种预先计算的哈希表,用于快速查找密码的哈希值。加盐使得每个用户的哈希值都是独一无二的,彩虹表无法直接应用到每个用户的具体情况。
-
增加计算成本:加盐增加了计算哈希值的步骤,因为每次哈希计算都需要处理不同的盐。这在一定程度上增加了破解的计算成本。
实战
步骤 | 逻辑 | 处理方 | 说明 |
---|---|---|---|
1 | 用户注册:md5(密码和固定盐salt1混排)=>md5pwd1 | 前端 | 防止密码明文传输 |
2 | 用户注册:生成随机盐salt2,md5pwd2 = md5(md5pwd1+salt2)、随机盐存库 | 后台 | 二次md5 |
3 | 用户登录:用户输入密码后做md5(密码和固定盐1混排)=>md5pwd1 | 前端 | 保持密码加密传输 |
4 | 用户校验:根据用户id取出注册时的md5pwd2、salt2 | 后台 | 取出注册的md5pwd2、salt2 |
5 | 用户校验:将取出的salt2+前端处理的md5pwd1,加密后与db数据做一致性比对 | 后台 | 加密结果比对 |
代码
package com.bj58.spider.contentfeedpost.abtest;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Scanner;
public class PasswordUtils {
/**
* 生成16位随机盐,再通过base64加密成字符串
*/
public static String generateSalt() {
SecureRandom random = new SecureRandom();
byte[] salt = new byte[16];
random.nextBytes(salt);
return Base64.getEncoder().encodeToString(salt);
}
/**
* 密码加密
*
* @param password 原始密码
* @param salt 存在db中的随机盐
* @return 密码
*/
public static String encryptPassword(String password, String salt) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(salt.getBytes());
byte[] hashedPassword = md.digest(password.getBytes());
return Base64.getEncoder().encodeToString(hashedPassword);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
}
/**
* 密码核验
*
* @param password 密码
* @param salt db中的随机盐
* @param hashedPassword 前端通过md5(原始密码+固定salt)得到的pwd
* @return
*/
public static boolean checkPassword(String password, String salt, String hashedPassword) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(salt.getBytes());
byte[] hashedInput = md.digest(password.getBytes());
String hashedInputString = Base64.getEncoder().encodeToString(hashedInput);
return hashedInputString.equals(hashedPassword);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return false;
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入一个密码:");
String pwd = scanner.nextLine();
System.out.print("请输入一个固定盐:");
String salt1 = scanner.nextLine();
// 用户注册
// 前端的第一次md5加密只是为了不明文传输,实际怎么混排、加密、加盐都和后台无关
String md5pwd1 = String.format("MD5(%s)", pwd + salt1);// 我为了测试方便,先随便写一个加密,哈哈
System.out.println(String.format("1.用户注册:密码:%s,固定盐:%s,得到第一次加密的md5 = %s,将加密的md5pwd1传给后台", pwd, salt1, md5pwd1));
// 后台准备第二次md5加密,先生成一个随机盐做加密,存db,再对前端加密后的pwd,做md5加密
String salt2 = generateSalt();
String md5pwd2 = encryptPassword(md5pwd1, salt2);
System.out.println(String.format("2.用户注册:密码:%s,随机盐:%s,得到第二次加密的md5 = %s,将加密的md5pwd2和salt2存入db", md5pwd1, salt2, md5pwd2));
// 用户登录
//前端不管怎么处理,都必须要保持md5加密一致性,不同场景都是一样的加密值
String md5pwd1Param = md5pwd1;
// 根据用户id取出注册时后台生成的随机盐salt2
String dbSalt = salt2;
String result = checkPassword(md5pwd1Param, dbSalt, md5pwd2) + "";
System.out.println(String.format("3.用户登录:前端密码:%s,db中的密码:%s,db中随机盐:%s,比对结果:%s", md5pwd1Param, md5pwd2, dbSalt, result));
}
}
总结
- 防止密码泄露是多层次的,要从数据上报=》存储都尽可能考虑到
- MD5加盐的算法实现包括hash有很多,但是核心流程基本上一样
- 抛开有人提过极致的暴力破解和db层加密串覆盖,不管从哪个层次看基本上开发和黑客都在短时间无法拿到原始密码
- 很遗憾,md5从原理上看,很多文章还是说有缺陷,我个人感觉最危险的就是前端的md5加密,虽然也加盐了,但是厚度好像还是不够,还是得继续搜素其他的加密策略才行
参考资料
- https://cloud.tencent.com/developer/article/2107970
- https://blog.51cto.com/u_16213353/7560254?share_token=961D7FD8-3412-48C8-B89E-126B2BBF81D1&tt_from=weixin&utm_source=weixin&utm_medium=toutiao_ios&utm_campaign=client_share&wxshare_count=1