目录
一、编码算法
1.什么是编码?
ASCII
码就是一种编码,字母A
的编码是十六进制的0x41
,字母B
是0x42
,以此类推:
字母 |
ASCII编码 |
A |
0x41 |
B |
0x42 |
C |
0x43 |
D |
0x44 |
… |
… |
因为ASCII
编码最多只能有127
个字符,要想对更多的文字进行编码,就需要用占用2
个字节的Unicode
或者3
个字节的UTF-8
。例如:中文的"中"字使用Unicode
编码就是0x4e2d
,UTF-8
编码是0xe4b8ad
。
汉字 |
Unicode编码 |
UTF-8编码 |
中 |
0x4e2d |
0xe4b8ad |
文 |
0x6587 |
0xe69687 |
编 |
0x7f16 |
0xe7bc96 |
码 |
0x7801 |
0xe7a081 |
因此,最简单的编码是直接给每个字符指定一个若干字节表示的整数,复杂一点的编码就需要根据一个已有的编码推算出来。
2.URL编码
URL
编码是浏览器发送数据给服务器时使用的编码,它通常附加在URL
的参数部分,例如:
之所以需要URL
编码,是因为出于兼容性考虑,很多服务器只识别ASCII
字符。但如果URL
中包含中文、日文这些非ASCII
字符怎么办?不要紧,URL
编码有一套规则:
如果字符是A
~Z
,a
~z
,0
~9
以及-
、_
、.
、*
,则保持不变;
如果是其他字符,先转换为UTF-8
编码,然后对每个字节以%XX
表示。
例如:字符"中"的UTF-8
编码是0xe4b8ad
,因此,它的URL
编码是%E4%B8%AD
。URL
编码总是大写。
Java
标准库提供了一个URLEncoder
类来对任意字符串进行URL
编码。
如果服务器收到URL
编码的字符串,就可以对其进行解码,还原成原始字符串。Java
标准库的URLDecoder
就可以解码:
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Scanner;
public class Demo01 {
public static void main(String[] args) throws UnsupportedEncodingException {
//编码
System.out.println("请输入:");
String input= new Scanner(System.in).next();
String encodeString=URLEncoder.encode(input,"UTF-8");
System.out.println("编码后的结果为:"+encodeString);
//解码
String decodeString=URLDecoder.decode(encodeString,"UTF-8");
System.out.println("解码后的结果为:"+decodeString);
}
}
//运行结果
//请输入:
//中文!
//编码后的结果为:%E4%B8%AD%E6%96%87%21
//解码后的结果为:中文!
上述代码如果在控制台输入的是"中文!"的运行结果是%E4%B8%AD%E6%96%87%21
,"中"的URL
编码是%E4%B8%AD
,"文"的URL
编码是%E6%96%87
,!
虽然是ASCII
字符,也要对其编码为%21
。
要特别注意:URL
编码是编码算法,不是加密算法。URL
编码的目的是把任意文本数据编码为%
前缀表示的文本,编码后的文本仅包含A
~Z
,a
~z
,0
~9
,-
,_
,.
,*
和%
,便于浏览器和服务器处理。
3.Base64编码
URL
编码是对字符进行编码,表示成%xx
的形式,而Base64
编码是对二进制数据进行编码,表示成文本格式。
Base64
编码可以把任意长度的二进制数据变为纯文本,并且纯文本内容中且只包含指定字符内容:A
~Z
、a
~z
、0
~9
、+
、/
、=
。它的原理是把3
字节的二进制数据按6bit
一组,用4
个整数表示,然后查表,把整数用索引对应到字符,得到编码后的字符串。
6
位整数的范围总是0
~63
,所以,能用64
个字符表示:字符A
~Z
对应索引0
~25
,字符a
~z
对应索引26
~51
,字符0
~9
对应索引52
~61
,最后两个索引62
、63
分别用字符+
和/
表示。
举个例子:3
个byte
数据分别是e4
、b8
、ad
,按6bit
分组得到十六进制39
、0b
、22
和2d
,分别对应十进制57
、11
、34
、45
,通过索引计算结果为5Lit4。
┌───────────────┬───────────────┬───────────────┐
│ e4 │ b8 │ ad │
└───────────────┴───────────────┴───────────────┘
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│1│1│1│0│0│1│0│0│1│0│1│1│1│0│0│0│1│0│1│0│1│1│0│1│二进制
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
┌───────────┬───────────┬───────────┬───────────┐
│ 39 │ 0b │ 22 │ 2d │十六进制
└───────────┴───────────┴───────────┴───────────┘
┌───────────┬───────────┬───────────┬───────────┐
│ 57 │ 11 │ 34 │ 45 │十进制
└───────────┴───────────┴───────────┴───────────┘
┌───────────┬───────────┬───────────┬───────────┐
│ 5 │ L │ i │ t │十进制
└───────────┴───────────┴───────────┴───────────┘
在Java
中,二进制数据就是byte[]
数组。Java
标准库提供了Base64
来对byte[]
数组进行编码:
public class Main {
public static void main(String[] args) {
byte[] input = new byte[] { (byte) 0xe4, (byte) 0xb8, (byte) 0xad };
String b64encoded = Base64.getEncoder().encodeToString(input);
System.out.println(b64encoded);
}
}
编码后得到字符串结果:5Lit
。要对这个字符使用Base64
解码,仍然用Base64
这个类:
public class Main {
public static void main(String[] args) {
byte[] output = Base64.getDecoder().decode("5Lit");
System.out.println(Arrays.toString(output)); // [-28, -72, -83]
}
}
因为标准的Base64
编码会出现+
、/
和=
,所以不适合把Base64
编码后的字符串放到URL
中。一种针对URL
的Base64
编码可以在URL
中使用的Base64
编码,它仅仅是把+
变成-
,/
变成_
:
import java.util.Arrays;
import java.util.Base64;
public class Demo03 {
public static void main(String[] args) {
//原始字节数组
byte[] input=new byte[]{0x01,0x02,0x7f,0x00};
//编码
String baseEncode=Base64.getEncoder().encodeToString(input);
System.out.println("base64进行编码:"+baseEncode);
String baseEncode1=Base64.getUrlEncoder().encodeToString(input);
System.out.println("base64url进行编码:"+baseEncode1);
//解码
byte[] bytes=Base64.getDecoder().decode(baseEncode);
System.out.println("base64解码后的内容:"+ Arrays.toString(bytes));
byte[] bytes1=Base64.getUrlDecoder().decode(baseEncode1);
System.out.println("base64url解码后的内容:"+ Arrays.toString(bytes1));
}
}
Base64
编码的目的是把二进制数据变成文本格式,这样在很多文本中就可以处理二进制数据。例如,电子邮件协议就是文本协议,如果要在电子邮件中添加一个二进制文件,就可以用Base64
编码,然后以文本的形式传送。
Base64
编码的缺点是传输效率会降低,因为它把原始数据的长度增加了1/3。和URL
编码一样,Base64
编码是一种编码算法,不是加密算法。
如果把Base64
的64
个字符编码表换成32
个、48
个或者58
个,就可以使用Base32
编码,Base48
编码和Base58
编码。字符越少,编码的效率就会越低。
4.小结
(1)URL
编码和Base64
编码都是编码算法,它们不是加密算法;
(2)URL
编码的目的是把任意文本数据编码为%
前缀表示的文本,便于浏览器和服务器处理;
(3)Base64
编码的目的是把任意二进制数据编码为文本,但编码后数据量会增加1/3
。
二、哈希算法
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.常用哈希算法
哈希算法,根据碰撞概率,哈希算法的输出长度越长,就越难产生碰撞,也就越安全。
常用的哈希算法有:
算法 |
输出长度(位) |
输出长度(字节) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Java
标准库提供了常用的哈希算法,通过统一的接口进行调用。以MD5
算法为例,看看如何对输入内容计算哈希:
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class Demo02 {
public static void main(String[] args) throws NoSuchAlgorithmException {
//1.创建MessageDigest实例
MessageDigest md=MessageDigest.getInstance("MD5");
//2.使用MD5进行信息加密
byte[] message="我本将心向明月".getBytes();
md.update(message);
//3.进行加密,加密后长度不受原字节数组长度影响,一直是16
// 是单向加密,无法反推
byte[] bytes=md.digest();
System.out.println("加密前的字节数组为:"+Arrays.toString(message));
System.out.println("加密后的长度为:"+message.length);
System.out.println("加密后的字节数组为:"+Arrays.toString(bytes));
System.out.println("加密后的长度为:"+bytes.length);
String digMessage=byteToHex(bytes);
System.out.println("加密后的字符串为:"+digMessage);
System.out.println("加密后的长度为:"+digMessage.length());
}
public static String byteToHex(byte[] bytes){
StringBuilder sb = new StringBuilder();
for (byte b:bytes) {
//将一个字节数字转成2位的16进制的字符串数字
sb.append(String.format("%02x",b));
}
return sb.toString();
}
}
//运行结果:
//加密前的字节数组为:[-26, -120, -111, -26, -100, -84, -27, -80, //-122, -27, -65, -125, -27, -112, -111, -26, -104, -114, -26, //-100, -120]
//加密后的长度为:21
//加密后的字节数组为:[-99, 85, -59, -110, 118, -116, 36, 49, 127, //57, 43, 33, 17, 19, 83, -21]
//加密后的长度为:16
//加密后的字符串为:9d55c592768c24317f392b21111353eb
//加密后的长度为:32
使用MessageDigest
时,我们首先根据哈希算法获取一个MessageDigest
实例,然后,反复调用update(byte[])
输入数据。当输入结束后,调用digest()
方法获得byte[]
数组表示的摘要,最后,把它转换为十六进制的字符串。
4.哈希算法的用途
4.1 校验下载文件
因为相同的输入永远会得到相同的输出,因此,如果输入被修改了,得到的输出就会不同。我们在网站上下载软件的时候,经常看到下载页显示的MD5哈希值:
如何判断下载到本地的软件是原始的、未经篡改的文件?我们只需要自己计算一下本地文件的哈希值,再与官网公开的哈希值对比,如果相同,说明文件下载正确,否则,说明文件已被篡改。
4.2 存储用户密码
哈希算法的另一个重要用途是存储用户口令。如果直接将用户的原始口令存放到数据库中,会产生极大的安全风险:
数据库管理员能够看到用户明文口令;
数据库数据一旦泄漏,黑客即可获取用户明文口令。
username |
password |
bob |
123456789 |
alice |
sdfsdfsdf |
tim |
justdoit |
不存储用户的原始口令,那么如何对用户进行认证?方法是存储用户口令的哈希,例如,MD5
。在用户输入原始口令后,系统计算用户输入的原始口令的MD5
并与数据库存储的MD5
对比,如果一致,说明口令正确,否则,口令错误。
因此,数据库存储用户名和口令的表内容应该像下面这样:
username |
password |
bob |
25f9e794323b453885f5181f1b624d0b |
alice |
73a90acaae2b1ccc0e969709665bc62f |
tim |
19f9f30bd097d4c066d758fb01b75032 |
这样一来,数据库管理员看不到用户的原始口令。即使数据库泄漏,黑客也无法拿到用户的原始口令。想要拿到用户的原始口令,必须用暴力穷举的方法,一个口令一个口令地试,直到某个口令计算的MD5
恰好等于指定值。
使用哈希口令时,还要注意防止彩虹表攻击。
什么是彩虹表呢?上面讲到了,如果只拿到MD5
,从MD5
反推明文口令,只能使用暴力穷举的方法。然而黑客并不笨,暴力穷举会消耗大量的算力和时间。但是,如果有一个预先计算好的常用口令和它们的MD5
的对照表,这个表就是彩虹表。如果用户使用了常用口令,黑客从MD5
一下就能反查到原始口令:
常用口令 |
MD5 |
hello123 |
f30aa7a662c728b7407c54ae6bfd27d1 |
12345678 |
25d55ad283aa400af464c76d713c07ad |
passw0rd |
bed128365216c019988915ed3add75fb |
19700101 |
570da6d5277a646f6552b8832012f5dc |
… |
… |
wbjxxmy |
11d7a82f45f6a176fd9d5c100ccab40a |
这就是为什么不要使用常用密码,以及不要使用生日作为密码的原因。
当然,我们也可以采取特殊措施来抵御彩虹表攻击:对每个口令额外添加随机数,这个方法称之为加盐(salt
):
digest = md5(salt + inputPassword)
经过加盐处理的数据库表,内容如下:
username |
salt |
password |
bob |
H1r0a |
a5022319ff4c56955e22a74abcc2c210 |
alice |
7$p2w |
e5de688c99e961ed6e560b972dab8b6a |
tim |
z5Sk9 |
1eee304b92dc0d105904e7ab58fd2f64 |
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.UUID;
/**
* 此次的盐值信息为:afe39a
* 加密后的结果为:[85, -38, -11, -44, 76, 97, 3, -117, 117, 91, 38, -117, 69, -110, 1, 70]
* 加密后的字符串为:55daf5d44c61038b755b268b45920146
* 加密后的长度为:16
*/
public class Demo03 {
public static void main(String[] args) throws NoSuchAlgorithmException {
//1.创建MessageDigest实例
MessageDigest md=MessageDigest.getInstance("MD5");
//2.使用MD5进行信息加密
byte[] message="我本将心向明月".getBytes();
md.update(message);
//3.添加盐值,随机值
// String uuid= UUID.randomUUID().toString().substring(0,6);
// System.out.println("此次的盐值信息为:"+uuid);
// md.update(uuid.getBytes());
//用上面刚获取的盐值查看一下加密后的字符串是否一致
md.update("afe39a".getBytes());
//4.进行加密
byte[] bytes=md.digest();
System.out.println("加密后的结果为:"+Arrays.toString(bytes));
System.out.println("加密后的字符串为:"+Demo02.byteToHex(bytes));
System.out.println("加密后的长度为:"+bytes.length);
}
}
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
":
// 创建一个MessageDigest实例:
MessageDigest md = MessageDigest.getInstance("SHA-1");
类似的,计算SHA-256
,我们需要传入名称"SHA-256
",计算SHA-512
,我们需要传入名称"SHA-512
"。Java
标准库支持的所有哈希算法可以在这里查到。
例题:我们自己提供一个HashTools工具类:
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.util.UUID;
public class HashTools {
//创建信息摘要对象--成员变量
private static MessageDigest md;
private HashTools() {
}
public static String messageMd5(String str) throws NoSuchAlgorithmException {
//创建信息摘要对象md5
md = MessageDigest.getInstance("MD5");
return handle(str);
}
public static String messageSha1(String str) throws NoSuchAlgorithmException {
//创建信息摘要对象md5
md = MessageDigest.getInstance("SHA-1");
return handle(str);
}
public static String handle(String str) {
//获取要加密的信息的字节数组并添加
byte[] message = str.getBytes();
md.update(message);
//添加盐值
md.update(UUID.randomUUID().toString().substring(0, 6).getBytes());
//加密操作
byte[] bytes = md.digest();
//转字符串
return byteToHex(bytes);
}
public static String byteToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
//将一个字节数字转成2位的16进制的字符串数字
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}
用的时候直接调用:
import java.security.NoSuchAlgorithmException;
public class Demo05 {
public static void main(String[] args) throws NoSuchAlgorithmException {
String message="我本将心向明月";
String md5Dig=HashTools.messageMd5(message);
System.out.println("md5加密后的字符串为:"+md5Dig);
System.out.println(md5Dig.length());//16*2
String sha1Dig=HashTools.messageSha1(message);
System.out.println("sha1加密后的字符串为:"+sha1Dig);
System.out.println(sha1Dig.length());//20*2
}
}
6.小结
(1)哈希算法可用于验证数据完整性,具有防篡改检测的功能;
(2)常用的哈希算法有MD5
、SHA-1
等;
(3)用哈希存储口令时要考虑彩虹表攻击。
三、Hmac算法
1.概述
在前面讲到哈希算法时,我们说,存储用户的哈希口令时,要加盐存储,目的就在于抵御彩虹表攻击。我们回顾一下哈希算法:digest = hash(input)
正是因为相同的输入会产生相同的输出,我们加盐的目的就在于,使得输入有所变化:
digest = hash(salt + input)
这个salt
可以看作是一个额外的“认证码”,同样的输入,不同的认证码,会产生不同的输出。因此,要验证输出的哈希,必须同时提供“认证码”。
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
的参考代码:
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class Demo06 {
public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException {
//产生密钥生成器对象
KeyGenerator generator = KeyGenerator.getInstance("HmacMD5");
//通过密钥生成器生成密钥
SecretKey key = generator.generateKey();
//获取密钥字节信息
byte[] bytes = key.getEncoded();
System.out.println("密钥字节信息:" + Arrays.toString(bytes));
System.out.println("密钥字符串:" + HashTools.byteToHex(bytes));
System.out.println("密钥长度:" + bytes.length);
//获取加密对象
Mac mac = Mac.getInstance("HmacMD5");
//初始化key值
mac.init(key);
//提供需要进行加密的信息添加进来
mac.update("我本将心向明月".getBytes());
//加密操作
byte[] md5Message = mac.doFinal();
System.out.println("加密字节信息:" + Arrays.toString(md5Message));
System.out.println("加密字符串:" + HashTools.byteToHex(md5Message));
System.out.println("加密长度:" + md5Message.length);
}
}
和MD5
相比,使用HmacMD5
的步骤是:
- 通过名称
HmacMD5
获取KeyGenerator
实例; - 通过
KeyGenerator
创建一个SecretKey
实例; - 通过名称
HmacMD5
获取Mac
实例; - 用
SecretKey
初始化Mac实例; - 对
Mac
实例反复调用update(byte[])
输入数据; - 调用
Mac
实例的doFinal()
获取最终的哈希值。
我们可以用Hmac
算法取代原有的自定义的加盐算法,因此,存储用户名和口令的数据库结构如下:
username |
secret_key (64 bytes) |
password |
bob |
a8c06e05f92e...5e16 |
7e0387872a57c85ef6dddbaa12f376de |
alice |
e6a343693985...f4be |
c1f929ac2552642b302e739bc0cdbaac |
tim |
f27a973dfdc0...6003 |
af57651c3a8a73303515804d4af43790 |
有了Hmac
计算的哈希和SecretKey
,我们想要验证怎么办?这时,SecretKey
不能从KeyGenerator
生成,而是从一个byte[]
数组恢复:
首先在HashTools工具类中添加一个stringTobyte()方法:
public static byte[] stringTobyte(String str){
byte[] bytes=new byte[str.length()/2];
for (int i = 0; i <str.length() ; i=i+2) {
String subString =str.substring(i,i+2);
bytes[i/2] =(byte) Integer.parseInt(subString,16);
}
return bytes;
}
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
/**
* 密钥字节信息:[-6, 15, -26, 90, -100, 18, 105, 25, 104, 50, 98, 74, -101, 50, 2, 42, 77, -99, -14, 60, 37, 114, -47, -20, -76, -7, -41, -1, -26, -91, 68, -7, -41, 115, -87, 21, -57, -110, -28, -64, 38, -99, -62, 82, 50, -95, -60, -103, -49, 115, 21, -39, -4, -60, 117, 6, -97, 20, 69, 116, 55, -128, 80, 51]
* 密钥字符串:fa0fe65a9c1269196832624a9b32022a4d9df23c2572d1ecb4f9d7ffe6a544f9d773a915c792e4c0269dc25232a1c499cf7315d9fcc475069f14457437805033
* 密钥长度:64
* 加密字节信息:[-112, 60, 66, -40, 89, -83, -110, 40, 121, 54, -93, 118, 1, 94, 28, 119]
* 加密字符串:903c42d859ad92287936a376015e1c77
* 加密长度:16
*/
public class Demo07 {
public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException {
//1.密钥字符串转字节数组操作
String str="fa0fe65a9c1269196832624a9b32022a4d9df23c2572d1ecb4f9d7ffe6a544f9d773a915c792e4c0269dc25232a1c499cf7315d9fcc475069f14457437805033";
byte[] bytes=HashTools.stringTobyte(str);
System.out.println(Arrays.toString(bytes));
//2.还原密钥key
SecretKey key=new SecretKeySpec(bytes,"HmacMD5");
//3.再次加密
Mac mac = Mac.getInstance("HmacMD5");
//初始化key值
mac.init(key);
//提供需要进行加密的信息添加进来
mac.update("我本将心向明月".getBytes());
//加密操作
byte[] md5Message = mac.doFinal();
System.out.println("加密字节信息:" + Arrays.toString(md5Message));
System.out.println("加密字符串:" + HashTools.byteToHex(md5Message));
System.out.println("加密长度:" + md5Message.length);
}
}
2.小结
Hmac
算法是一种标准的基于密钥的哈希算法,可以配合MD5
、SHA-1
等哈希算法,计算的摘要长度和原摘要算法长度相同。
四、BouncyCastle
1.概述
Java
标准库提供了一系列常用的哈希算法。但如果我们要用的某种算法,Java
标准库没有提供怎么办?
方法一:自己写一个,难度很大;
方法二:找一个现成的第三方库,直接使用。
BouncyCastle就是一个提供了很多哈希算法和加密算法的第三方开源库。它提供了Java
标准库没有的一些算法,例如,RipeMD160
哈希算法。
RIPEMD160
是一种基于Merkle-Damgård
结构的加密哈希函数,它是比特币标准之一。RIPEMD-160
是RIPEMD
算法的增强版本,RIPEMD-160
算法可以产生出160
位的的哈希摘要。
2.用法
首先,我们必须把BouncyCastle
提供的bcprov-jdk15on-1.70.jar
添加至classpath
。这个jar
包可以从官方网站下载。
其次,Java
标准库的java.security
包提供了一种标准机制,允许第三方提供商无缝接入。我们要使用BouncyCastle
提供的RipeMD160
算法,需要先把BouncyCastle
注册一下:
可以在HashTools中添加messageRipeMD160方法,然后直接调用:
public static String messageRipeMD160(String str) throws NoSuchAlgorithmException {
//创建信息摘要对象md5
Security.addProvider(new BouncyCastleProvider());
md = MessageDigest.getInstance("RipeMD160");
return handle(str);
}
String ripemd160Dig=HashTools.messageRipeMD160(message);
System.out.println("ripemd160加密后的字符串为:"+ripemd160Dig);
System.out.println(ripemd160Dig.length());//20*2
3.小结
(1)BouncyCastle
是一个开源的第三方算法提供商;
(2)BouncyCastle
提供了很多Java
标准库没有提供的哈希算法和加密算法;
五、对称加密算法
1.概述
对称加密算法就是传统的用一个秘钥进行加密和解密。例如,我们常用的WinZIP
和WinRAR
对压缩包的加密和解密,就是使用对称加密算法:
从程序的角度看,所谓加密,就是这样一个函数,它接收密码和明文,然后输出密文:
secret = encrypt(key, message);
而解密则相反,它接收密码和密文,然后输出明文:
plain = decrypt(key, secret);
在软件开发中,常用的对称加密算法有:
算法 |
密钥长度 |
工作模式 |
填充模式 |
DES |
56/64 |
ECB/CBC/PCBC/CTR/... |
NoPadding/PKCS5Padding/... |
AES |
128/192/256 |
ECB/CBC/PCBC/CTR/... |
NoPadding/PKCS5Padding/PKCS7Padding/... |
IDEA |
128 |
ECB |
PKCS5Padding/PKCS7Padding/... |
密钥长度直接决定加密强度,而工作模式和填充模式可以看成是对称加密算法的参数和格式选择。Java
标准库提供的算法实现并不包括所有的工作模式和所有填充模式。
最后,值得注意的是,DES
算法由于密钥过短,可以在短时间内被暴力破解,所以现在已经不安全了。
2.使用AES加密
AES
算法是目前应用最广泛的加密算法。比较常见的工作模式是ECB
和CBC
。
2.1 ECB模式
ECB
模式是最简单的AES
加密模式,它需要一个固定长度的密钥,固定的明文会生成固定的密文。
我们先用ECB
模式加密并解密:
import apesource.knowledge.hashStudy.HashTools;
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class Demo01 {
public static void main(String[] args) throws IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException {
//加密 加密的字节数组长度随着input的变化而变化
byte[] input="我本将心向明月".getBytes();
//keys是固定长度的密钥,固定的明文会生成固定的密文。
// 128位密钥 = 16 bytes Key
byte[] keys="1234567890abcedf".getBytes();
byte[] encodeBytes=encodeMessage(input,keys);
System.out.println("加密后的结果:"+ Arrays.toString(encodeBytes));
System.out.println("加密后的字符串:"+ HashTools.byteToHex(encodeBytes));
System.out.println("加密后字节数组的长度:"+encodeBytes.length);
//解密
byte[] decodeMessage=decodeMessage(encodeBytes,keys);
System.out.println("解密后的信息:"+new String(decodeMessage));
}
private static byte[] encodeMessage(byte[] message, byte[] key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
//1.获取对称加密对象,设置加密算法,工作模式,填充模式
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
//2.加密准备密钥信息
SecretKey keys = new SecretKeySpec(key, "AES");
//3.初始化加密对象
cipher.init(Cipher.ENCRYPT_MODE, keys);
//4.加密操作
byte[] bytes = cipher.doFinal(message);
return bytes;
}
private static byte[] decodeMessage(byte[] encode,byte[] key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
//1.获取对称加密对象,设置加密算法,工作模式,填充模式
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
//2.加密准备密钥信息
SecretKey keys = new SecretKeySpec(key, "AES");
//3.初始化解密对象
cipher.init(Cipher.DECRYPT_MODE, keys);
//4.执行解密操作
return cipher.doFinal(encode);
}
}
Java
标准库提供的对称加密接口非常简单,使用时按以下步骤编写代码:
1.根据算法名称/工作模式/填充模式获取Cipher
实例;
2.根据算法名称初始化一个SecretKey
实例,密钥必须是指定长度;
3.使用SerectKey
初始化Cipher
实例,并设置加密或解密模式;
4.传入明文或密文,获得密文或明文。
2.2 CBC模式
ECB
模式是最简单的AES
加密模式,这种一对一的加密方式会导致安全性降低。所以,更好的方式是通过CBC
模式,它需要一个随机数作为IV
参数,这样对于同一份明文,每次生成的密文都不同:
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
public class Demo02 {
public static void main(String[] args) throws Exception {
byte[] message="我本将心向明月".getBytes();
byte[] keys="1234567890abcdef".getBytes();
byte[] encode=encodeMessage(message,keys);
System.out.println("加密后的信息为:"+Arrays.toString(encode));
byte[] decode=decodeMessage(encode,keys);
System.out.println(new String(decode));
}
private static byte[] encodeMessage(byte[] message, byte[] key) throws Exception{
//1.获取Cipher对象
Cipher cipher=Cipher.getInstance("AES/CBC/PKCS5Padding");
//2.还原key
SecretKey keys=new SecretKeySpec(key,"AES");
//3.准备IV偏移 SecureRandom安全随机数
SecureRandom sr=SecureRandom.getInstanceStrong();
byte[] bytes=sr.generateSeed(16);
// System.out.println("iv偏移值"+Arrays.toString(bytes));
IvParameterSpec iv=new IvParameterSpec(bytes);
//4.初始化操作,设置加密模式,设置key,设置偏移量
cipher.init(Cipher.ENCRYPT_MODE,keys,iv);
//5.加密
byte[] endcode=cipher.doFinal(message);
return addByte(endcode,bytes);
}
private static byte[] decodeMessage(byte[] decodeMessage, byte[] key) throws Exception{
//1.获取Cipher对象
Cipher cipher=Cipher.getInstance("AES/CBC/PKCS5Padding");
//2.还原key
SecretKey keys=new SecretKeySpec(key,"AES");
//3.还原iv值
byte[] bytes=Arrays.copyOfRange(decodeMessage,decodeMessage.length-16,decodeMessage.length);
IvParameterSpec iv=new IvParameterSpec(bytes);
//4.初始化操作,设置解密模式,设置key,设置偏移量
cipher.init(Cipher.DECRYPT_MODE,keys,iv);
//5.加密
byte[] endcode=cipher.doFinal(Arrays.copyOf(decodeMessage,decodeMessage.length-16));
return endcode;
}
public static byte[] addByte(byte[] endcode,byte[] iv){
byte[] bytes=new byte[endcode.length+iv.length];
System.arraycopy(endcode,0,bytes,0,endcode.length);
System.arraycopy(iv,0,bytes,endcode.length,iv.length);
return bytes;
}
}
在CBC
模式下,需要一个随机生成的16
字节IV参数,必须使用SecureRandom
生成。因为多了一个IvParameterSpec
实例,因此,初始化方法需要调用Cipher
的一个重载方法并传入IvParameterSpec
。
观察输出,可以发现每次生成的IV
不同,密文也不同。
3.小结
(1)对称加密算法使用同一个密钥进行加密和解密,常用算法有DES
、AES
和IDEA
等;
(2)密钥长度由算法设计决定,AES
的密钥长度是128
/192
/256
位;
(3)使用对称加密算法需要指定算法名称、工作模式和填充模式。