哈希算法
- 哈希算法
- 1.概述
- 2.哈希碰撞
- 3.常用的哈希算法
- 4.哈希算法的用途
- 4.1校验下载文件
- 4.2存储用户密码
- MD5加密
- 5.SHA-1加密
- 小结:
哈希算法
1.概述
哈希算法(Hash)又称摘要算法(Digest),它的作用是:对任意一组输入数据进行计算,得到一个固定长度的输出摘要。
哈希算法最重要的特点就是:
● 相同的输入一定得到相同的输出;
● 不同的输入大概率得到不同的输出。
所以,哈希算法的目的:为了验证原始数据是否被篡改。
Java字符串的hashCode()就是一个哈希算法,它的输入是任意字符串,输出是固定的4字节int整数:
"hello".hashCode(); // 0x5e918d2
"hello, java".hashCode(); // 0x7a9d88e8
"hello, bob".hashCode(); // 0xa0dbae2f
两个相同的字符串永远会计算出相同的hashCode,否则基于hashCode定位的HashMap就无法正常工作。这也是为什么当我们自定义一个class时,覆写equals()方法时我们必须正确覆写hashCode()方法。
2.哈希碰撞
哈希碰撞是指:两个不同的输入得到了相同的输出。
例如:
"AaAaAa".hashCode(); // 0x7460e8c0
"BBAaBB".hashCode(); // 0x7460e8c0
"通话".hashCode(); // 0x11ff03
"重地".hashCode(); // 0x11ff03
碰撞能不能避免?答案是不能。碰撞是一定会出现的,因为输出的字节长度是固定的,String的hashCode()输出是4字节整数,最多只有4294967296种输出,但输入的数据长度是不固定的,有无数种输入。所以,哈希算法是把一个无限的输入集合映射到一个有限的输出集合,必然会产生碰撞。
碰撞不可怕,我们担心的不是碰撞,而是碰撞的概率,因为碰撞概率的高低关系到哈希算法的安全性。一个安全的哈希算法必须满足:
● 碰撞概率低;
● 不能猜测输出。
不能猜测输出是指:输入的任意一个bit的变化会造成输出完全不同,这样就很难从输出反推输入(只能依靠暴力穷举)。
假设一种哈希算法有如下规律:
hashA("java001") = "123456"
hashA("java002") = "123457"
hashA("java003") = "123458"
那么很容易从输出123459反推输入,这种哈希算法就不安全。安全的哈希算法从输出是看不出任何规律的:
hashB("java001") = "123456"
hashB("java002") = "580271"
hashB("java003") = ???
3.常用的哈希算法
哈希算法,根据碰撞概率,哈希算法的输出长度越长,就越难产生碰撞,也就越安全。
常用的哈希算法有:
算法 | 输出长度(位) | 输出长度(字节) |
---|---|---|
MD5 | 128 bits | 16 bytes |
SHA-1 | 160 bits | 20 bytes |
RipeMD-160 | 160 bits | 20 bytes |
SHA-256 | 256 bits | 32 bytes |
SHA-512 | 512 bits | 64 bytes |
Java标准库提供了常用的哈希算法,通过统一的接口进行调用。以MD5算法为例,看看如何对输入内容计算哈希:
import java.security.MessageDigest;
public class main {
public static void main(String[] args) {
// 创建一个MessageDigest实例:
MessageDigest md = MessageDigest.getInstance("MD5");
// 反复调用update输入数据:
md.update("Hello".getBytes("UTF-8"));
md.update("World".getBytes("UTF-8"));
// 16 bytes: 68e109f0f40ca72a15e05cc22786f8e6
byte[] results = md.digest();
StringBuilder sb = new StringBuilder();
for(byte bite : results) {
sb.append(String.format("%02x", bite));
}
System.out.println(sb.toString());
}
}
使用MessageDigest时,我们首先根据哈希算法获取一个MessageDigest实例,然后,反复调用update(byte[])输入数据。当输入结束后,调用digest()方法获得byte[]数组表示的摘要,最后,把它转换为十六进制的字符串。
运行上述代码,可以得到输入HelloWorld的MD5是68e109f0f40ca72a15e05cc22786f8e6。
4.哈希算法的用途
4.1校验下载文件
因为相同的输入永远会得到相同的输出,因此,如果输入被修改了,得到的输出就会不同。我们在网站上下载软件的时候,经常看到下载页显示的MD5哈希值:
如何判断下载到本地的软件是原始的、未经篡改的文件?我们只需要自己计算一下本地文件的哈希值,再与官网公开的哈希值对比,如果相同,说明文件下载正确,否则,说明文件已被篡改。
4.2存储用户密码
哈希算法的另一个重要用途是存储用户口令。如果直接将用户的原始口令存放到数据库中,会产生极大的安全风险:
● 数据库管理员能够看到用户明文口令;
● 数据库数据一旦泄漏,黑客即可获取用户明文口令。
username | password |
---|---|
admin | admin |
user | user |
tim | tim |
不存储用户的原始口令,那么如何对用户进行认证?方法是存储用户口令的哈希,例如,MD5。在用户输入原始口令后,系统计算用户输入的原始口令的MD5并与数据库存储的MD5对比,如果一致,说明口令正确,否则,口令错误。
因此,数据库存储用户名和口令的表内容应该像下面这样:
username | password |
---|---|
admin | 25f9e794323b453885f5181f1b624d0b |
user | 73a90acaae2b1ccc0e969709665bc62f |
tim | 19f9f30bd097d4c066d758fb01b75032 |
这样一来,数据库管理员看不到用户的原始口令。即使数据库泄漏,黑客也无法拿到用户的原始口令。想要拿到用户的原始口令,必须用暴力穷举的方法,一个口令一个口令地试,直到某个口令计算的MD5恰好等于指定值。
使用哈希口令时,还要注意防止彩虹表攻击。
什么是彩虹表呢?上面讲到了,如果只拿到MD5,从MD5反推明文口令,只能使用暴力穷举的方法。然而黑客并不笨,暴力穷举会消耗大量的算力和时间。但是,如果有一个预先计算好的常用口令和它们的MD5的对照表,这个表就是彩虹表。如果用户使用了常用口令,黑客从MD5一下就能反查到原始口令:
常用口令 | MD5 |
---|---|
hello123 | f30aa7a662c728b7407c54ae6bfd27d1 |
12345678 | 25d55ad283aa400af464c76d713c07ad |
… | … |
这就是为什么不要使用常用密码,以及不要使用生日作为密码的原因。
当然,我们也可以采取特殊措施来抵御彩虹表攻击:对每个口令额外添加随机数,这个方法称之为加盐(salt):
digest = md5(salt + inputPassword)
经过加盐处理的数据库表,内容如下:
username | salt | password |
---|---|---|
bob | H1r0a | a5022319ff4c56955e22a74abcc2c210 |
alice | 7$p2w | e5de688c99e961ed6e560b972dab8b6a |
tim | z5Sk9 | 1eee304b92dc0d105904e7ab58fd2f64 |
public class Demo05 {
public static void main(String[] args) throws NoSuchAlgorithmException, IOException {
byte[] password = "liyupiicu@com".getBytes();
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(password);
// 产生随机盐值
String uuid = UUID.randomUUID().toString().substring(16);
md5.update(uuid.getBytes());
// 获取消息摘要对象进行加密
byte[] digetByte = md5.digest();
System.out.println(Arrays.toString(digetByte));
System.out.println(Demo03.byteToHex(digetByte));
System.out.println(digetByte.length);
}
}
MD5加密
Demo01
public class Demo01 {
public static void main(String[] args) throws NoSuchAlgorithmException {
// 创建一个MessageDigest实例:
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] bytes = "测试测试测试".getBytes();
// 反复调用update输入数据:
md.update(bytes);
// 加密:查看内容
byte[] result = md.digest();
System.out.println(result.length);
System.out.println(Arrays.toString(result));
}
}
Demo02
public class Demo02 {
public static void main(String[] args) throws NoSuchAlgorithmException {
// 获取消息摘要对象
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] b = "我本将心照明月".getBytes();
// 数据量小使用信息加密md5.digest(b)
// byte[] result = md5.digest(b);
// System.out.println("加密前:" + Arrays.toString(b));
// System.out.println("加密后:" + Arrays.toString(result));
// System.out.println("加密后长度:" + result.length);
// 数据量大使用md5.update(b)加密
md5.update(b);
md5.update("爪爪吃蒸饺".getBytes());
md5.update("蒸米粉肉".getBytes());
//加密,查看内容
byte[] result=md5.digest();
System.out.println("加密前:" + Arrays.toString(b));
System.out.println("加密后:" + Arrays.toString(result));
System.out.println("加密后长度:" + result.length);
}
}
Demo03
public class Demo03 {
public static void main(String[] args) throws NoSuchAlgorithmException {
String password = "wbjxxmy";
byte[] bytepassword = password.getBytes();
// 获取消息摘要对象
MessageDigest md5 = MessageDigest.getInstance("MD5");
// 反复添加需要加密的内容
md5.update(bytepassword);
// 加密
byte[] digestpassword = md5.digest();
System.out.println(Arrays.toString(bytepassword));
System.out.println("加密后数组信息:" + Arrays.toString(digestpassword));
System.out.println("加密后16进制字符串信息:" + byteToHex(digestpassword));
}
public static String byteToHex(byte[] digestpassword) {
StringBuffer sb = new StringBuffer();
for (byte b : digestpassword) {
sb.append(String.format(",%02x", b));
}
return sb.toString();
}
}
Demo04
md5算法对图片进行加密
//md5算法对图片进行加密
public class Demo04 {
public static void main(String[] args) throws NoSuchAlgorithmException, IOException {
byte[] image = Files.readAllBytes(Paths.get("E:\\apesourcefile\\maomao.jpg"));
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(image);
byte[] result = md5.digest();
System.out.println(Arrays.toString(result));
System.out.println(Demo03.byteToHex(result));
// 06203919a4288aafd95b78964386f0fd
// 06203919a4288aafd95b78964386f0fd
}
}
5.SHA-1加密
SHA-1也是一种哈希算法,它的输出是160 bits,即20字节。SHA-1是由美国国家安全局开发的,SHA算法实际上是一个系列,包括SHA-0(已废弃)、SHA-1、SHA-256、SHA-512等。
在Java中使用SHA-1,和MD5完全一样,只需要把算法名称改为"SHA-1":
import java.security.MessageDigest;
public class main {
public static void main(String[] args) {
// 创建一个MessageDigest实例:
MessageDigest md = MessageDigest.getInstance("SHA-1");
// 反复调用update输入数据:
md.update("Hello".getBytes("UTF-8"));
md.update("World".getBytes("UTF-8"));
// 20 bytes: db8ac1c259eb89d4a131b253bacfca5f319d54f2
byte[] results = md.digest();
StringBuilder sb = new StringBuilder();
for(byte bite : results) {
sb.append(String.format("%02x", bite));
}
System.out.println(sb.toString());
}
}
类似的,计算SHA-256,我们需要传入名称"SHA-256",计算SHA-512,我们需要传入名称"SHA-512"。Java标准库支持的所有哈希算法可以在这里https://docs.oracle.com/en/java/javase/14/docs/specs/security/standard-names.html#messagedigest-algorithms查到。
sha-1加密和md5加密
注意:md5加密长度为32位,sha-1加密长度为40位。
public class Demo06 {
public static void main(String[] args) throws NoSuchAlgorithmException {
byte[] password = "爪爪吃蒸饺".getBytes();
// md5加密
String resultMd5 = HashUtlis.messageMD5(password);
// sha-1加密
String resultSha1 = HashUtlis.messageSHA1(password);
System.out.println("md5加密后为" + resultMd5);
System.out.println("Sha1加密后为" + resultSha1);
char[] r1 = resultMd5.toCharArray();
char[] r2 = resultSha1.toCharArray();
System.out.println(r1.length);
System.out.println(r2.length);
}
}
小结:
哈希算法可用于验证数据完整性,具有防篡改检测的功能;
● 常用的哈希算法有MD5、SHA-1等;
● 用哈希存储口令时要考虑彩虹表攻击。