哈希算法可对任意一组输入数据进行计算,得到一个固定长度的输出摘要。哈希算法可以验证原始数据是否被篡改。
哈希算法的用途:
1.可用于校验下载文件
2.存储用户密码,直接将原始密码放入数据库中会有很大风险。
一、哈希碰撞
哈希碰撞是指两个不同的输入得到了相同的输出。
哈希碰撞无法避免,所以一个好的哈希算法必须要保证哈希碰撞概率低,不能猜测输出。
二、常用的哈希算法
哈希算法的输出长度越长,就越难产生碰撞。
1、MD5算法
MD5算法输出长度为16字节,128位。MD5算法只能进行加密不能进行解密。可通过比较加密后的结果,验证数据是否一致。
public class Demo01 {
public static void main(String[] args) {
try {
//创建基于md5算法的消息摘要对象
MessageDigest md5=MessageDigest.getInstance("md5");
//更新原始数据
md5.update("天王盖地虎".getBytes());
//获取加密后的结果
byte[] disgestBytes=md5.digest();
System.out.println("加密后的结果(字节数组)"+Arrays.toString(disgestBytes));
System.out.println("加密后的结果(十六进制字符串)"+HashTools.bytesToHex(disgestBytes));
System.out.println("加密后的长度"+disgestBytes.length);
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
MD5算法首先要通过MessageDigest类创建一个消息摘要对象,然后通过update()方法更新数据,数据必须为字节,也可以重复调用update()方法更新数据,只要顺序不变加密后的结果仍然不变。然后在调用digest()方法进行加密,该方法返回值为字节数组。所以加密结果要保存在字节数组中。
如果想要将加密结果以字符串输出,可以创建HashTools工具类,写一个bytesToHex()方法,将加密结果转换为字符串。"%02x"是指将字节转换为两位的16进制字符串。
//将字节数组转换为16进制字符串
public static String bytesToHex(byte[] bytes) {
StringBuilder ret=new StringBuilder();
//将字节数值转换为2位16进制字符串
for(byte b:bytes) {
ret.append(String.format("%02x", b));//format可将字节按照指定进制格式转换
}
return ret.toString();
}
例1:按照MD5算法对图片进行加密
MD5算法也可以对图片进行加密 ,图片也是由很多个字节组成。
public class Demo02 {
public static void main(String[] args) {
try {
//图片的原始字节内容
byte[] bytes=Files.readAllBytes(Paths.get("D:\\y\\test\\2.jpg"));
//创建基于md5算法的消息摘要对象
MessageDigest md5=MessageDigest.getInstance("MD5");
//更新原始数据
md5.update(bytes);
//获取加密后的结果
byte[] disgestBytes=md5.digest(bytes);
System.out.println("加密后的结果(字节数组)"+Arrays.toString(disgestBytes));
System.out.println("加密后的结果(十六进制字符串)"+HashTools.bytesToHex(disgestBytes));
System.out.println("加密后的长度"+disgestBytes.length);
} catch (IOException | NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
加密图片,则需通过Files.readAllBytes(paths.get(" "))方法先获取图片的原始字节内容。其他步骤一致。
2.通过随机加盐,解决彩虹表攻击问题
常用密码即便加密后,也容易反推出原始密码。
彩虹表就是通过预先计算好的常用口令和MD5对照表,黑课以此很容易反查到原始口令。也就是彩虹表攻击。
为解决彩虹表攻击,那么可以额外添加随机数,也就是加盐。
盐可以是随即数也可以是固定的。
public class Demo03 {
public static void main(String[] args) throws NoSuchAlgorithmException {
//原始密码
String password="wbjxxmy";
//产生随机的盐值
String salt=UUID.randomUUID().toString().substring(0,4);
//MessageDigest md5=MessageDigest.getInstance("MD5");
//基于SHA-1算法的消息摘要对象
MessageDigest sha=MessageDigest.getInstance("SHA-1");
// md5.update(password.getBytes());
sha.update(password.getBytes());//原始密码
sha.update(salt.getBytes());//盐值
// String digestHex=HashTools.bytesToHex(md5.digest());
//计算加密结果,SHA-1的输出结果为20个字节(40个字符)
String digestHex=HashTools.bytesToHex(sha.digest());
System.out.println(digestHex);
}
}
MD5和SHA-1算法都可加盐。
2、SHA算法
SHA算法是一个系列算法,也是一种哈希算法,常用的有SHA-1,SHA-256,SHA-512等,它们的区别只是输出长度不同。
1、SHA-1
SHA-1的输出是160bits,即20字节。
使用SHA-1和MD5算法完全一样,只需要将算法名称改为”SHA-1"即可。
//基于SHA-1算法的消息摘要对象
MessageDigest sha=MessageDigest.getInstance("SHA-1");
2.SHA-256
SHA-256的输出是256bits,32字节。使用时只需要将算法名称改为”SHA-256"即可。
3.SHA-512
SHA-512的输出是512bits,64字节。使用时只需要将算法名称改为”SHA-512"即可。
哈希算法工具类
MD5算法和SHA-1算法在算法加密时有多个步骤是相同的,可将其封装在HashTools工具类中.
//Hash算法(消息摘要算法)工具类
public class HashTools {
//消息摘要对象
private static MessageDigest digest;
//构造方法私有
private HashTools() {}
//将字节数组转换为16进制字符串
public static String bytesToHex(byte[] bytes) {
StringBuilder ret=new StringBuilder();
//将字节数值转换为2位十进制字符串
for(byte b:bytes) {
ret.append(String.format("%02x", b));
}
return ret.toString();
}
//按照md5进行消息摘要计算(哈希计算)
public static String digestByMD5(String source) throws NoSuchAlgorithmException {
digest=MessageDigest.getInstance("MD5");
return handler(source);
}
//按照SHA-1进行消息摘要计算(哈希计算)
public static String digestBySHA1(String source) throws NoSuchAlgorithmException {
digest=MessageDigest.getInstance("SHA-1");
return handler(source);
}
//通过消息摘要对象处理加密内容
private static String handler(String source) {
digest.update(source.getBytes());
byte[] bytes=digest.digest();
String hash=bytesToHex(bytes);
return hash;
}
}
public class Demo04 {
public static void main(String[] args) {
try {
//md5加密
String md5=HashTools.digestByMD5("wbjxxmy");
//SHA-1加密
String sha1=HashTools.digestBySHA1("wbjxxmy");
System.out.println("md5="+md5);
System.out.println("sha-1="+sha1);
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
3、RipeMD-160算法
RipeMD-160消息摘要算法是使用第三方开源库实现的。
它的输出长度是160bits,20字节。
RipeMD-160算法使用时要先注册bouncyCastleBouncyCastleProvider通知类,然后将提供的消息摘要算法注册至Security。然后再才能创建消息摘要对象。
public class Demo03 {
public static void main(String[] args) throws NoSuchAlgorithmException {
//注册bouncyCastleBouncyCastleProvider通知类
//将提供的消息摘要算法注册至Security
Security.addProvider(new BouncyCastleProvider());
//创建基于ripeMd160算法的消息摘要对象
MessageDigest ripeMd160=MessageDigest.getInstance("ripeMd160");
//更新原始数据
ripeMd160.update("天王盖地虎".getBytes());
//获取加密后的结果
byte[] disgestBytes=ripeMd160.digest();
//消息摘要的字节长度和内容
System.out.println("加密后的结果(字节数组)"+Arrays.toString(disgestBytes));
System.out.println("加密后的长度"+disgestBytes.length);
//16进制内容字符串
String hex=new BigInteger(1,disgestBytes).toString(16);
System.out.println("加密后的结果(字符串内容)"+Arrays.toString(disgestBytes));
System.out.println("加密后的结果(字符串长度)"+disgestBytes.length);
}
}
RipeMD160消息摘要算法实现首先要通过new BouncyCastleProvider()注册BouncyCastleProvider通知类,再调用Security.addProvider()将提供的消息摘要算法注册至Security。然后通过MessageDigest类创建基于ripeMd160算法的消息摘要对象。然后调用update()更新数据,调用digest()方法进行加密。
三、Hmac算法
Hmac算法是一种基于密钥的消息认证算法。Hmac算法总是和某种哈希算法配合使用。例如使用MD5算法,对应的是HmacMD5算法,相当于’加盐‘的MD5。
为了保证安全,我们不会自己指定key,而是通过Java标准库的KeyGenerator生成随机的key.HmacMD5的key是64字节。
public class Demo01 {
public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException {
String password="wbjxxmy";
//1.产生秘钥
//获取HmacMD5秘钥生成器
KeyGenerator keyGen=KeyGenerator.getInstance("HmacMD5");
//生成秘钥
SecretKey key=keyGen.generateKey();
System.out.println("秘钥"+Arrays.toString(key.getEncoded()));
System.out.println("秘钥的长度(64字节):"+key.getEncoded().length);
System.out.println("秘钥"+HashTools.bytesToHex(key.getEncoded()));
//2.使用秘钥进行加密
//获取Mac加密算法对象
Mac mac=Mac.getInstance("HmacMD5");
mac.init(key);//初始化秘钥
mac.update(password.getBytes());//更新原始加密内容
byte[] bytes=mac.doFinal();//加密处理,并获取加密结果
String result=HashTools.bytesToHex(bytes);//加密结果处理成16进制字符串
System.out.println("加密结果16进制字符串"+result);
System.out.println("加密结果(字节长度16字节)"+bytes.length);
System.out.println("加密结果(字符长度32字节)"+result.length());
}
}
Hmac算法分为两步:1.产生秘钥 2.使用秘钥进行加密
产生密钥先通过KeyGenerator.getInstance("HmacMD5")获取HmacMD5秘钥生成器,再调用generateKey()生成秘钥。秘钥的长度是固定的,但每次生成的内容不同,提高了安全性。在生成秘钥后通过Mac.getInstance("HmacMD5")获取Mac加密算法对象,在调用mac.init(key)初始化秘钥.同样通过update()方法更新原始加密内容。不同的是Hmac算法调用doFinal()进行加密处理。加密结果同样是字节。
我们想要进行验证时就无法生成密钥了,而是要从字节数组或字符串恢复密钥。
public class Demo02 {
public static void main(String[] args) {
//原始密码
String password="wbjxxmy";
//秘钥(字节数组)
//byte[] keyBytes= {118, -124, 86, -65, -20, 45, 73, -117, -62, 107, 53, 27, -113, 20, 9, 3, -8, -66, -31, 108, 104, -16, -86, 100, -114, 103, -124, 100, 75, 53, -50, 37, -53, -94, -126, -18, -125, 8, 20, 115, -51, -11, -98, 93, 111, 18, 50, -18, -94, -76, -116, 87, -59, 112, -81, -11, -67, 64, 15, -107, -3, -7, 119, 33};
//秘钥(字节串)
String keystr="6db400f5489bf3591d5f573428a6b137";
//用于保存秘钥:密钥长度为64字节
byte[] keyBytes=new byte[64];
for(int i=0,k=0;i<keystr.length();i+=2,k++) {
String s=keystr.substring(i, i+2);
keyBytes[k]=(byte)Integer.parseInt(s,16);//转换为16进制byte值
}
//恢复密钥并加密
try {
//恢复密钥(字节数组)
SecretKey key=new SecretKeySpec(keyBytes, "HmacMD5");
//创建Hmac加密算法对象
Mac mac=Mac.getInstance("HmacMD5");
mac.init(key);//初始化密钥
mac.update(password.getBytes());
String result=HashTools.bytesToHex(mac.doFinal());
System.out.println(result);
} catch (InvalidKeyException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
当密钥内容是字节数组时,我们要通过SecretKey key=new SecretKeySpec(keyBytes, "HmacMD5")来恢复密钥。参数一:字节数组 参数二:指定算法
当密钥是字符串时我们要先将其转换为字节数组,然后再进行恢复密钥。密钥恢复后才能进行加密。