常见哈希算法总结
(1)什么是哈希算法?
哈希算法(Hash)又称摘要算法(Digest),它的作用是:对任意一组输出数据进行计算,得到一个固定长度的输出摘要。哈希算法的目的;为了验证原始数据是否被篡改.
哈希算法最重要的特点是:
相同的输入一定得到相同的输出;
不同的输入大概率得到不同的输出.
Java字符串的hashCode()就是一个哈希算法,它的输入是任意字符串,输出是固定的4字节int整数:
"hello".hashCode(); // 0x5e918d2
"hello, java".hashCode(); // 0x7a9d88e8
"hello, bob".hashCode(); // 0xa0dbae2f
(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") = ???
常见哈希算法
常见的哈希算法有:根据碰撞概率,哈希算法的输出长度越长,就越难产生碰撞,也就越安全。
算法 | 输出长度(位) | 输出长度(字节) |
MD5 | 128bits | 16bytes |
SHA-1 | 160bits | 20bytes |
RipeMD-160 | 160bits | 20bytes |
SHA-256 | 256bits | 32bytes |
SHA-512 | 512bits | 64bytes |
Java标准库提供了常用的哈希算法,并且有一套统一的接口。我们以MD5算法为例,看看如何对输入计算哈希:
public class main{
public static void main(String[] args){
//创建一个MessageDigest实例;
MessageDigest md=MessageDigest.getInstance("MD5");
//反复调用updata输入数据:
md.update("hello".getBytes("UTF-8"));
md.updata("world".getBytes("UTF-8"));
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[]数组表示的摘要,最后,把它转换成十六进制的字符串。
案例:读取一张图片调用digest()方法获得byte[]数组表示的摘要,最后,把它转换成十六进制的字符串。
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Test08 {
public static void main(String[] args) {
try {
byte[] imageByteArray=Files.readAllBytes(Paths.get("c:\\test\\img\\cz.jpg"));
MessageDigest digest=MessageDigest.getInstance("MD5");
digest.update(imageByteArray);
byte[] resultByteArray=digest.digest();
StringBuilder result=new StringBuilder();
for(byte bite:resultByteArray) {
result.append(String.format("%02x", bite));
}
System.out.println(result);
System.out.println(result.length());
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
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());
}
}
哈希算法的用途
检验下载文件
因为相同的输入永远会得到相同的输出,因此,如果输入被修改了,得到的输出就会不同。
如何判断下载到本地的软件是原始的,未经篡改的文件?我们只需要自己计算一下本地文件的哈希值,再与官网公开的哈希值对比,如果相同,说明文件下载正确,否则,说明文件已被篡改。
存储用户密码
哈希算法的另一个重要用途是存储用户口令。如果直接将用户的原始口令存储到数据库中,会产生极大的安全风险:
数据库管理员能够看到用户明文口令;
数据库数据一旦泄露,黑客即可获取用户明文口令。
username | password |
bob | 123456789 |
alice | sdfsdfsdf |
tim | justdoit |
如何对用户进认证?存储用户口令的哈希,系统计算用户输入的原始口令的MD5并与数据库存储的MD5对比,如果一致,说明口令正确,否则,口令错误。
这样,数据库管理员看不到用户的原始口令。即使数据库泄露,黑客也无法拿到用户的原始口令。想要拿到用户的原始口令,必须使用暴力穷举的方法,一个口令一个口令的试,直到某个口令计算的MD5恰好等于指定值。
使用哈希口令时,还要注意防止彩虹表攻击。什么是彩虹表:如果一个预先计算好的常用口令和他们的MD5的对照表,这个表就是彩虹表。如果用户使用了常用口令,黑客从MD5一下就能反查到原始口令:
常用口令 | MD5 |
hello123 | f30aa7a662c728b7407c54ae6bfd27d1 |
..... | ...... |
当然,我们也可以采取特殊措施来抵御彩虹表攻击;对每个口令额外添加随机数,这个方法称之为加盐(salt):
digest=md5(salt+inputPassword)
案例:
public class Test09 {
public static void main(String[] args) {
String password="wbjxxmy";
String salt=UUID.randomUUID().toString().substring(0, 5);
System.out.println(salt);
try {
//获取SHA-1算法的工具对象
MessageDigest digest=MessageDigest.getInstance("SHA-1");
digest.update(password.getBytes());
digest.update(salt.getBytes());
byte[] resultByteArray=digest.digest();
System.out.println(Arrays.toString(resultByteArray));
System.out.println(resultByteArray.length);
StringBuilder result=new StringBuilder();
for(byte b:resultByteArray) {
result.append(String.format("%02x", b));
}
System.out.println(result);
System.out.println(result.length());
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
Hmac算法
Hmac算法总是和某种哈希算法配合起来用的。例如:我们使用MD5算法,对应的就是Hmac MD5算法,它相当于加盐的MD5
好处:
HmacMD5使用的key长度是64字节,更安全;
Hmac是标准算法,同样适用于SHA-1等其他哈希算法;
Hmac输出和原有的哈希算法长度一致。
可见,Hmac本质上就是把key混入摘要的算法。验证此哈希时,除了原始的输入数据,还要提供key.为了保证安全,我们不会指定key,而是通过Java标准库的KeyGenerator生成一个安全的随机key.
//获取HmacMD5密钥生成器
KeyGenerator keyGen=KeyGenerator.getInstance("HmacMD5");
//产生密钥
SecretKey secreKey=keyGen.generateKey();
//打印随机生成的密钥:
byte[] keyArray=sereKey.getEncoded();
StringBuilder key=StringBuilder();
for(byte bite:keyArray){
key.append(String.format("%02x",bite));
}
System.out.println(key);
//使用HmacMD5加密
Mac mac=Mac.getInstance("HmacMD5");
mac.init(secreKey);//初始化
mac.update("HelloWorld".getBytes("UTF-8"));
byte[] resultArray=mac.doFinal();
StringBuilder result=new StringBuilder();
for(byte bite:resultArray){
result.append(String.format("%02x",bite));
}
System.out.println(result);
和MD5相比,使用HmacMD5的步骤是:
1.通过名称HmacMD5获取KeyGenerator实例;
2.通过KeyGenerator创建一个SecretKey实例;
3.通过HmacMD5获取Mac实例;
4.用SecretKey初始化Mac实例;
5.对Mac实例反复调用updata(byte[])输入数据;
6.调用Mac实例的doFinal()获得最终的哈希值
有了Hmac计算的哈希和SecretKey,我们想要验证怎么办?SecretKey不能从KeyGenerator生成,而是从一个byte[]数组恢复。
byte[] keyArray = {70, 31, 31, 113, -75, 45, 5, 112, -32, -32, 57, 59, -77, -52, -22, -67, -115, -15, -32, -57, 94, -78, 58, -115, 76, -104, 41, -120, -21, 28, 123, -13, -79, -17, 18, 63, -45, -14, -43, -33, -126, -115, 76, -87, -123, -16, -109, -127, -113, -114, 19, 96, 69, 73, -2, -75, -66, -88, -10, -9, -14, 104, -97, -69};
SecretKey secreKey = new SecretKeySpec(keyArray, "HmacMD5");
Mac mac = Mac.getInstance("HmacMD5");
mac.init(secreKey); // 初始化key
mac.update("HelloWorld".getBytes("UTF-8"));
byte[] resultArray = mac.doFinal();
StringBuilder result = new StringBuilder();
for(byte bite:resultArray) {
result.append(String.format("%02x", bite));
}
System.out.println(result);
BouncyCastle
提供了很多哈希算法和加密算法的第三方开源库。它提供了Java标准库没有的一些算法,例如:RipeMD160算法。
用法
1.添加jar包至classpath
java标准库的java.seurity包提供了一种标准机制,允许第三方提供无缝接入。我们使用BouncyCastle提供的RipeMD160算法,需先把BouncyCastle注册一下
public class Main {
public static void main(String[] args) throws Exception {
// 注册BouncyCastle提供的通知类对象BouncyCastleProvider
Security.addProvider(new BouncyCastleProvider());
// 获取RipeMD160算法的"消息摘要对象"(加密对象)
MessageDigest md = MessageDigest.getInstance("RipeMD160");
// 更新原始数据
md.update("HelloWorld".getBytes());
// 获取消息摘要(加密)
byte[] result = md.digest();
// 消息摘要的字节长度和内容
System.out.println(result.length); // 160位=20字节
System.out.println(Arrays.toString(result));
// 16进制内容字符串
String hex = new BigInteger(1,result).toString(16);
System.out.println(hex.length()); // 20字节=40个字符
System.out.println(hex);
}
}