哈希算法概述:
哈希算法(Hash):
又称摘要算法(Digest),作用是:对任意一组输入数据进行计算,得到一个固定长度的输出摘要。
哈希算法最重要的特点就是:
相同的输入一定得到相同的输出
不同的输入大概率得到不同的输出
So,哈希算法的目的:为了验证原始数据是否被篡改
为何不同的输入得到的输出只是大概率不同,这便是接下来要讲述的哈希碰撞。
哈希碰撞:
哈希碰撞是指:两个不同的输入得到了相同的输出
碰撞能不能避免?答案是不能。碰撞是一定会出现的,因为输出的字节长度是固定的,Java中String的hashCode()就是一个哈希算法,它的输入是任意字符串,输出是固定4字节整数,最多只有4294967296种输出,但输入的数据长度是不固定的,有无数种输入。所以,哈希算法是把一个无限的输入集合映射到一个有限的输出集合,必然会产生碰撞。
碰撞不可怕,我们要担心的不是碰撞,而是碰撞的概率,因为碰撞概率的高低关系到哈希算法的安全性。一个安全的哈希算法必须满足:
碰撞概率低
不能猜测输出
不能猜测输出是指:输入的任意一个bit的变化会造成输出完全不同,这样就很难从输出反推输入(只能依靠暴力穷举)
常用哈希算法:
哈希算法,根据碰撞概率,哈希算法的输出长度越长,就越难产生碰撞,也就越安全。
常用的哈希算法有:
算法 | 输出长度(位) | 输出长度(字节) |
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 |
接下来,我们用代码实现常见的两种哈希算法:MD5与SHA-1
MD5:
Java标准库提供了常用的哈希算法,通过统一的接口进行调用。以MD5算法为例,看看如何对输入内容计算哈希
Ⅰ.加密文本信息
public class md5 {
public static void main(String[] args) throws NoSuchAlgorithmException {
//创建基于MD5算法的消息摘要对象
MessageDigest md5 = MessageDigest.getInstance("md5");
//更新原始数据
md5.update("我本将心向明月".getBytes());
//获取加密后的结果
byte[] disest = md5.digest();
System.out.println(Arrays.toString(disest));//加密后的结果
System.out.println(HashTools.bytesToHex(disest));//将字节数组转换为16进制字符串
System.out.println(disest.length);//加密结果的长度
}
}
//Hash算法(信息摘要算法)工具
public class HashTools{
private HashTools(){}
public static String bytesToHex(byte[] bytes){
StringBuilder ret = new StringBuilder();
//将字节数组转换为16进制字符串
public static String bytesToHex(byte[] bytes){
StringBuilder ret = new StringBuilder();
for (byte b : bytes){
//将字节值转换为2位十六进制字符串
ret.append(String.format("%02x",b));
}
return ret.toString();
}
}
}
使用MessageDigest时,我们首先根据哈希算法获取一个MessageDigest实例,然后,反复调用update(byte[])输入数据。当输入结束后,调用digest()方法获得byte[]数组表示的摘要,最后,把它转换为十六进制的字符串
Ⅱ.加密图片信息
//按照MD5算法对图片进行“加密”
public class ImageHash {
public static void main(String[] args) throws IOException, NoSuchAlgorithmException {
//图片原始字节内容
byte[] images = Files.readAllBytes(Paths.get("d:\\text\\java.png"));
//创建基于MD5算法的消息摘要对象
MessageDigest md5 = MessageDigest.getInstance("md5");
//原始字节内容(图片)
md5.update(images);
//获取加密摘要
byte[] disest = md5.digest();
System.out.println(Arrays.toString(disest));
System.out.println(HashTools.bytesToHex(disest));
System.out.println(disest.length);//MD5算法固定输出长度为16个字节
}
}
//Hash算法(信息摘要算法)工具
public class HashTools{
private HashTools(){}
public static String bytesToHex(byte[] bytes){
StringBuilder ret = new StringBuilder();
//将字节数组转换为16进制字符串
public static String bytesToHex(byte[] bytes){
StringBuilder ret = new StringBuilder();
for (byte b : bytes){
//将字节值转换为2位十六进制字符串
ret.append(String.format("%02x",b));
}
return ret.toString();
}
}
对比两个实现方法不难发现,Hash算法工具代码块一直重复出现,所有我们可以将这部分代码封装在HashTools方法中
//Hash算法工具类
public class HashTools {
private HashTools(){}//构造方法私有
private static MessageDigest disgest;//消息摘要对象
//按照MD5进行消息摘要计算
public static String disgestByMD5(String source) throws NoSuchAlgorithmException {
disgest = MessageDigest.getInstance("MD5");
return handler(source);
}
//按照SHA1进行消息摘要计算
public static String disgestBySHA1(String source) throws NoSuchAlgorithmException {
disgest = MessageDigest.getInstance("SHA-1");
return handler(source);
}
//通过信息摘要对象,处理加密内容
private static String handler(String source){
disgest.update(source.getBytes());//调用update输入数据
byte[] bytes = disgest.digest();//调用digest()方法获得由byte[]数组表示的摘要
String hash = bytesToHex(bytes);
return hash;
}
//将字节数组转换为16进制字符串
public static String bytesToHex(byte[] bytes){
StringBuilder ret = new StringBuilder();
for (byte b : bytes){
//将字节值转换为2位十六进制字符串
ret.append(String.format("%02x",b));
}
return ret.toString();
}
}
SHA-1:
MD5与SHA-1算法原理相同,区别仅在于输出长度与字节大小的差异,我们可直接将SHA-1的实现方法封装在HashTools方法中(在Java中使用SHA-1,和MD5完全一样,只需要把算法名称改为"SHA-1")
public class hashdemo {
public static void main(String[] args) throws NoSuchAlgorithmException {
//创建基于MD5算法的消息摘要对象
//MessageDigest md5 = MessageDigest.getInstance("md5");
//创建基于SHA-1算法的消息摘要对象
//MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
//通过调用HashTools实现
String md5 = HashTools.disgestByMD5("落花有意");
String sha1 = HashTools.disgestBySHA1("流水无情");
System.out.println(md5);
System.out.println(sha1);
}
}
哈希算法用途:
校验下载文件:
因为相同的输入永远会得到相同的输出,因此,如果输入被修改了,得到的输出就会不同。如何判断下载到本地的软件是原始的、未经篡改的文件?我们只需要自己计算一下本地文件的哈希值,再与官网公开的哈希值对比,如果相同,说明文件下载正确,否则,说明文件已被篡改。
存储用户密码:
哈希算法的另一个重要用途是存储用户口令。如果直接将用户的原始口令存放到数据库中,会产生极大的安全风险:
数据库管理员能够看到用户明文口令
数据库数据一旦泄漏,黑客即可获取用户明文口令
不存储用户的原始口令,那么如何对用户进行认证?方法是存储用户口令的哈希,例如,MD5。在用户输入原始口令后,系统计算用户输入的原始口令的MD5并与数据库存储的MD5对比,如果一致,说明口令正确,否则,口令错误。
这样一来,数据库管理员看不到用户的原始口令。即使数据库泄漏,黑客也无法拿到用户的原始口令。想要拿到用户的原始口令,必须用暴力穷举的方法,一个口令一个口令地试,直到某个口令计算的MD5恰好等于指定值。
使用哈希口令时,还要注意防止彩虹表攻击。
什么是彩虹表?上面讲到,如果只拿到MD5,从MD5反推明文口令,只能使用暴力穷举的方法。然而黑客并不笨,暴力穷举会消耗大量的算力和时间。但是,如果有一个预先计算好的常用口令和它们的MD5的对照表,这个表就是彩虹表。如果用户使用了常用口令,黑客从MD5一下就能反查到原始口令(这就是为什么不要使用常用密码,以及不要使用生日作为密码的原因)
当然,我们也可以采取特殊措施来抵御彩虹表攻击:对每个口令额外添加随机数,这个方法称之为加盐(salt): digest = md5(salt + inputPassword)
代码实现:
public class HashPassWord {
public static void main(String[] args) throws NoSuchAlgorithmException {
//原始密码
String password = "我本将心向明月";
//创建基于MD5算法的消息摘要对象
MessageDigest md5 = MessageDigest.getInstance("md5");
//产生随机盐值
String salt = UUID.randomUUID().toString().substring(0,4);
md5.update(password.getBytes());//原始密码
md5.update(salt.getBytes());//盐值
String disgest = HashTools.bytesToHex(md5.digest());//计算加密结果
System.out.println(disgest);
}
}
But,自己手动加"盐"还是过于麻烦且安全指数直线下降,所以在这里我们需要来了解一种更为安全的算法:Hmac
Hmac算法就是一种基于密钥的消息认证码算法,它的全称是Hash-based Message Authentication Code,是一种更安全的消息摘要算法。
Hmac算法总是和某种哈希算法配合起来用的。例如,我们使用MD5算法,对应的就是Hmac MD5算法,它相当于“加盐”的MD5:HmacMD5 ≈ md5(secure_random_key, input)
因此,HmacMD5可以看作带有一个安全的key的MD5。使用HmacMD5而不是用MD5加salt,有如下好处:
HmacMD5使用的key长度是64字节,更安全
Hmac是标准算法,同样适用于SHA-1等其他哈希算法
Hmac输出和原有的哈希算法长度一致
可见,Hmac本质上就是把key混入摘要的算法。验证此哈希时,除了原始的输入数据,还要提供key。为了保证安全,我们不会自己指定key,而是通过Java标准库的KeyGenerator生成一个安全的随机的key
下面是使用HmacMD5的参考代码:
public class hmac {
public static void main(String[] args) {
String index = "我本将心向明月";
try {
//获取HmacMD5密钥生成器,产生密钥
KeyGenerator keyGen = KeyGenerator.getInstance("HmacMD5");
//生成密钥
SecretKey key = keyGen.generateKey();
System.out.println(Arrays.toString(key.getEncoded()));
System.out.println(key.getEncoded().length);
System.out.println(HashTools.bytesToHex(key.getEncoded()));
//使用密钥,进行加密
//获取HMac加密算法对象
Mac mac = Mac.getInstance("HmacMD5");
mac.init(key);//初始化密钥
mac.update(index .getBytes());//更新原始加密内容
byte[] bytes = mac.doFinal();//加密处理,获取加密结果
String result = HashTools.bytesToHex(bytes);//加密结果处理为16进制字符串
System.out.println(result);
System.out.println(bytes.length);
System.out.println(result.length());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
}
}
}
和MD5相比,使用HmacMD5的步骤是:
1通过名称HmacMD5获取KeyGenerator实例
2通过KeyGenerator创建一个SecretKey实例
3通过名称HmacMD5获取Mac实例
4用SecretKey初始化Mac实例
5对Mac实例反复调用update(byte[])输入数据
6调用Mac实例的doFinal()获取最终的哈希值
有了Hmac计算的哈希和SecretKey,我们想要验证怎么办?这时,SecretKey不能从KeyGenerator生成,而是从一个byte[]数组恢复:
public class DisHmac {
public static void main(String[] args) {
//原始密码
String password = "我本将心向明月";
//按照字节数组恢复Hmac密钥
//字节密钥
byte[] key = {-93, -65, -45, 40, 119, -32, -118, 83, 90, -122, -10, 54, -8, 84, -48, 75, 21, -93, 58, 28, 124, 107, -89, -64, 18, -4, 50, -18, -27, -118, -45, 114, 21, 45, 112, 123, 124, -85, -109, 93, 74, 99, -12, -36, -85, -93, -20, -12, -2, 33, 123, -53, -69, -66, 53, -76, -65, 54, -62, -77, 111, 30, -86, 80};
//持有字符密钥时恢复密钥
//String key = "a3bfd32877e08a535a86f636f854d04b15a33a1c7c6ba7c012fc32eee58ad372152d707b7cab935d4a63f4dcaba3ecf4fe217bcbbbbe35b4bf36c2b36f1eaa50";
//保存密钥,长度为64字节
//byte[] keyword = new byte[64];
//for (int i = 0,k = 0;i<key.length();i+=2,k++){
//String s = key.substring(i,i+2);
//keyword[k] = (byte)Integer.parseInt(s,16);//转化为16进制byte值
//}
//System.out.println(Arrays.toString(keyword));//可观察到将密钥转化为字节数组
try {
//恢复密钥
SecretKey secretKey = new SecretKeySpec(key,"HmacMD5");
//创建Hmac加密算法对象
Mac mac = Mac.getInstance("HmacMD5");
mac.init(secretKey);//初始化密钥
mac.update(password.getBytes());
String result = HashTools.bytesToHex(mac.doFinal());
//f5aa6840c061ffac15e69f2b0ee3b59c
System.out.println(result);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
}
}
}
Java标准库提供了一系列常用的哈希算法。但如果我们要用的某种算法,Java标准库没有提供时,抛开自己写一个这种不实际的想法,我们还可以找一个第三方库,BouncyCastle就是一个提供了很多哈希算法和加密算法的第三方开源库。它提供了Java标准库没有的一些算法,例如,RipeMD160哈希算法:
//使用第三方开源库提供的RipeMD160信息摘要算法实现
public class RipeMD160 {
public static void main(String[] args) throws NoSuchAlgorithmException {
//注册BouncyCastleBouncyCastleProvider通知类
//将提供的消息类摘要算法注册至Security
Security.addProvider(new BouncyCastleProvider());
//获取RipeMD160算法的”信息摘要对象“(加密对象)
MessageDigest ripeMd160 = MessageDigest.getInstance("RipeMD160");
//更新原始数据
ripeMd160.update("wbjxxmy".getBytes());
//获取信息摘要(加密)
byte[] result = ripeMd160.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);
}
}
小结:
1.哈希算法可用于验证数据完整性,具有防篡改检测的功能
2.常用的哈希算法有MD5、SHA-1等
3.用哈希存储口令时要考虑彩虹表攻击
4.BouncyCastle是一个开源的第三方算法提供商,提供了很多Java标准库没有提供的哈希算法和加密算法
5.Hmac算法是一种标准的基于密钥的哈希算法,可以配合MD5、SHA-1等哈希算法,计算的摘要长度和原摘要算法长度相同。
向上攀爬的痛苦,终会在登顶时烟消云散
——ZQY