一.编码算法
什么是编码?
ASCII码
就是一种编码,字母A
的编码是十六进制的0x41
,字母B是
0x42`,以此类推:
因为ASCII编码最多只能有127个字符,要想对更多的文字进行编码,就需要用Unicode
。而中文的中使用Unicode编码就是0x4e2d
,使用UTF-8
则需要3个字节
编码:
因此,最简单的编码是直接给每个字符
指定一个若干字节表示的整数
,复杂一点的编码就需要根据一个已有的编码推算出来
。
- 比如: UTF-8编码,它是一种
不定长编码
,但可以从给定字符的Unicode编码推算出来
。
URL编码
1.什么是URL编码算法
URL编码是 浏览器
发送数据给服务器时使用的编码,它通常拼接在·URL的参数部分·
- 例如:
https://www.baidu.com/s?wd=%E4%B8%AD%E6%96%87
之所以需要URL编码,是因为出于兼容性考虑
,很多服务器只识别ASCII字符
。但如果URL中包含中文、日文
这些非ASCII字符怎么办?不要紧,URL编码有一套规则:
- 如果字符是
A~Z,a~z,0~9
以及-、_、.、*
,则保持不变; - 如果是
其他字符
,先转换为UTF-8编码
,然后对每个字节以%XX
表示。- 例如:字符"中"的UTF-8编码是
0xe4b8ad
,因此,它的URL编码是%E4%B8%AD
。URL编码总是大写。
- 例如:字符"中"的UTF-8编码是
2.Java中使用URL编码算法
Java标准库提供了一个URLEncoder类
来对任意字符串进行URL编码
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
public class TestURLEncode {
public static void main(String[] args) throws UnsupportedEncodingException {
String encoded = URLEncoder.encode("中文!","UTF-8");
System.out.println(encoded);
}
}
上述代码的运行结果是%E4%B8%AD%E6%96%87%21
,中的URL编码是%E4%B8%AD
,文的URL编码是%E6%96%87
,!
虽然是ASCII字符
,也要对其编码为%21
。
-
和标准的URL编码稍有不同,URLEncoder把
空格字符编码成
“+”,而 现在的URL编码标准要求空格被编码为“%20”,不过,服务器都可以处理这两种情况。 -
!!!要特别注意:URL编码是
编码算法
,不是加密算法
。URL编码的目的是把任意文本数据编码为%前缀表示的文本
,编码后的文本仅包含A~Z,a~z,0~9,-,_,.,*和%
,便于浏览器和服务器处理
。
如果服务器收到URL编码的字符串,就可以对其进行解码,还原成原始字符串。Java标准库的URLDecoder
就可以解码:
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
public class TestURLDecoder {
public static void main(String[] args) throws UnsupportedEncodingException {
String decoded = URLDecoder.decode("%E4%B8%AD%E6%96%87%21", "UTF-8");
System.out.println(decoded);
}
}
Base64编码
1.什么是Base64编码算法
- URL编码是
对字符进行编码
,表示成%xx
的形式 - 而Base64编码是
对二进制数据进行编码
,表示成文本格式
。- Base64编码可以把任意长度的
二进制数据
变为纯文本
,且只包含A~Z、a~z、0~9、+、/、=
这些字符。 - 它的原理是 把
3字节
的二进制数据按6bit(1个字节=2bit)
一组,用4个int整数
表示,然后查表
,把int整数
用索引对应到字符
,得到编码后的字符串
- Base64编码可以把任意长度的
举个例子:3个byte
数据分别是e4、b8、ad
,按6bit分组
得到39
、0b
、22
和2d
:
因为6位整数
的范围总是0~63
,所以,能用64个字符
表示:字符A~Z
对应索引0~25
,字符a~z
对应索引26~51
,字符0~9
对应索引52~61
,最后两个索引62、63
分别用字符+和/
表示。
2.Java中使用Base64编码算法
在Java中,二进制数据就是byte[]数组
。Java标准库提供了Base64
来对byte[]数组
进行编解码
:
import java.util.Arrays;
import java.util.Base64;
public class TestBase64 {
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);
System.out.println("-----------------------");
//编码后得到5Lit4个字符。要对Base64解码,仍然用Base64这个类进行解码
byte[] output = Base64.getDecoder().decode("5Lit");
System.out.println(Arrays.toString(output)); // [-28, -72, -83]
}
}
如果输入的byte[]数组长度不是3的整数倍肿么办?
- 这种情况下,需要对
输入的末尾补一个或两个0x00
- 编码后,
在结尾加一个=表示补充了1个0x00
,加两个=表示补充了2个0x00
- 解码的时候,
去掉末尾补充的一个或两个0x00即可。
**
*实际上,因为编码后的长度加上=总是4的倍数
,所以即使不加=也可以计算出原始输入的byte[]
。
- Base64编码的时候可以用
withoutPadding()
去掉=,解码出来的结果是一样的:*
import java.util.Arrays;
import java.util.Base64;
public class TestBase642 {
public static void main(String[] args) {
byte[] input = new byte[]{(byte) 0xe4, (byte) 0xb8, (byte) 0xad, 0x21};
String b64encoded = Base64.getEncoder().encodeToString(input);
System.out.println("encodeToString=>"+b64encoded);
//用withoutPadding()去掉=
String b64encoded2 = Base64.getEncoder().withoutPadding().encodeToString(input);
System.out.println("withoutPadding=>"+b64encoded2);
//将去掉=的Base64编码重新解码
byte[] output = Base64.getDecoder().decode(b64encoded2);
System.out.println(Arrays.toString(output));
}
}
因为标准的Base64编码会出现+
、/
和=
,所以不适合把Base64编码后的字符串放到URL中。
- 一种针对URL的Base64编码可以在URL中使用的Base64编码,它仅仅是把
+变成-
,/变成_
import java.util.Arrays;
import java.util.Base64;
public class TestBase64URL {
public static void main(String[] args) {
//编码
byte[] input = new byte[]{0x01, 0x02, 0x7f, 0x00};
String b64encoded = Base64.getUrlEncoder().encodeToString(input);
System.out.println(b64encoded);
//解码
byte[] output = Base64.getUrlDecoder().decode(b64encoded);
System.out.println(Arrays.toString(output));
}
}
-
Base64编码的目的是
把二进制数据变成文本格式
,这样在很多文本中就可以处理二进制数据。- 例如,
电子邮件协议
就是文本协议,如果要在电子邮件中添加一个二进制文件,就可以用Base64编码,然后以文本的形式传送。
- 例如,
-
Base64编码的缺点是:
传输效率会降低
,因为它把原始数据的长度增加了1/3
。 -
和URL编码一样,Base64编码是一种编码算法,不是加密算法。
-
如果把Base64的
64个字符编码表
换成32个
、48个
或者58个
,就可以使用Base32编码
,Base48编码和Base58编码
。字符越少,编码的效率就会越低。
小结
-
URL编码和Base64编码都是
编码算法
,它们不是加密算法
; -
URL编码的目的是
把任意文本数据编码为"%"前缀表示的文本
,便于浏览器和服务器处理; -
Base64编码的目的是
把任意二进制数据编码为文本
,但编码后数据量会增加1/3,传输效率会降低
二.哈希算法
1.什么是哈希算法?
- 哈希算法(Hash)又称
摘要算法(Digest)
,它的作用是:对任意一组输入数据进行计算,得到一个固定长度的输出摘要。
哈希算法最重要的特点就是:
- 相同的输入一定得到相同的输出;
- 不同的输入大概率得到不同的输出。
哈希算法的目的就是为了验证原始数据是否被篡改
。
Java字符串的hashCode()方法
就是一个哈希算法,它的 输入
是任意字符串,输出
是固定的4字节int整数:
System.out.println("hello".hashCode()); // 99162322
System.out.println("hello, java".hashCode()); // 2057144552
System.out.println("hello, bob".hashCode()); // -1596215761
两个相同的字符串永远会计算出相同的hashCode
,否则基于hashCode定位
的HashMap就无法正常工作。这也是为什么当我们自定义一个class时,覆写equals()方法时我们必须正确覆写hashCode()方法
。
2.哈希碰撞
哈希碰撞是指: 两个不同的输入得到了相同的输出:
System.out.println("AaAaAa".hashCode());; // 1952508096
System.out.println("BBAaBB".hashCode());// 1952508096
碰撞能不能避免?
答案是不能。碰撞是一定会出现的
,因为输出的字节长度是固定的
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 |
根据碰撞概率,哈希算法的输出长度越长
,就越难产生碰撞
,也就越安全
。
MD5 加密后的位数有两种:
16 位与 32 位
。16 位实际上是从 32 位字符串中取中间的第 9 位到第 24 位的部分,用 Java 语言来说,即:
String md5_16 = md5_32.substring(8, 24);
MD5 加密后的字符串又分为
大写与小写两种
,也就是其中的字母是大写还是小写
。
所以对字符串“yjclsx”进行 MD5 加密后的结果类型有这些:
Java 中 MD5 加密的结果默认是32位小写。
上图中所使用的MD5在线加密工具地址
4.Java中使用哈希算法
Java标准库提供了常用的哈希算法,并且有一套统一的接口
。我们以MD5算法为例,
看看如何对输入计算哈希:
import java.math.BigInteger;
import java.security.MessageDigest;
public class TestMD5 {
public static void main(String[] args) throws Exception {
// 创建一个MessageDigest实例:
MessageDigest md = MessageDigest.getInstance("MD5");
// 反复调用update输入数据:
md.update("Hello".getBytes("UTF-8"));
md.update("World".getBytes("UTF-8"));
byte[] result = md.digest(); // 16 bytes: 68e109f0f40ca72a15e05cc22786f8e6
//转换为十六进制的字符串
System.out.println(new BigInteger(1, result).toString(16));
}
}
使用MessageDigest时,我们首先根据哈希算法获取一个MessageDigest实例
,然后,反复调用update(byte[])输入数据
。当输入结束后,调用digest()方法获得byte[]数组表示的摘要
,最后,把它转换为十六进制的字符串。
5.哈希算法的用途
因为相同的输入永远会得到相同的输出,因此,如果输入被修改了,得到的输出就会不同。
我们在网站上下载软件的时候,经常看到下载页显示的哈希:
如何判断下载到本地的软件是原始的、未经篡改的文件?
- 我们只需要自己
计算一下本地文件的哈希值
,再与官网公开的哈希值对比
,如果相同
,说明文件下载正确
,否则,说明文件已被篡改
。
哈希算法的另一个重要用途是存储用户密码
。 如果直接将用户的原始密码存放到数据库中
,会产生极大的安全风险:
- 数据库管理员能够看到用户明文密码;
- 数据库数据一旦泄漏,黑客即可获取用户明文密码。
不存储用户的原始口令,那么如何对用户进行认证?
- 方法是存储用户密码的哈希,例如:
MD5
。 - 在用户输入原始密码后,系统计算用户输入的原始密码的MD5并与数据库存储的MD5对比,如果一致,说明密码正确,否则,密码错误。
因此数据库存储用户名和密码的表内容应该像下面这样:
这样一来,数据库管理员看不到用户的原始密码
。即使数据库泄漏,黑客也无法拿到用户的原始密码。想要拿到用户的原始密码,必须用暴力穷举
的方法,一个密码一个密码地试,直到某个密码计算的MD5恰好等于指定值
。
使用哈希密码时,还要注意防止彩虹表攻击。
- 什么是彩虹表呢?上面讲到了,如果只拿到MD5,
从MD5反推明文密码
,只能使用暴力穷举
的方法。- 然而黑客并不笨,
暴力穷举会消耗大量的算力和时间
。但是,如果有一个预先计算好的常用密码和它们的MD5的对照表:
这个表就是彩虹表。如果用户使用了常用密码
,黑客从MD5一下就能反查到原始密码
bob的MD5:f30aa7a662c728b7407c54ae6bfd27d1
,原始密码:hello123
;
alice的MD5:25d55ad283aa400af464c76d713c07ad
,原始密码:12345678
;
tim的MD5:bed128365216c019988915ed3add75fb
,原始口令:passw0rd
这就是为什么不要使用常用密码,以及不要使用生日作为密码的原因。
- 然而黑客并不笨,
加盐
即使用户使用了常用密码,我们也可以采取措施来抵御彩虹表攻击
,方法是对每个密码额外添加随机数
,这个方法称之为加盐(salt)
:
digest = md5(salt+inputPassword)
经过加盐处理的数据库表,内容如下:
加盐的目的在于使黑客的彩虹表失效,即使用户使用常用密码,也无法从MD5反推原始密码。
6.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.math.BigInteger;
import java.security.MessageDigest;
public class TestSHA1 {
public static void main(String[] args) throws Exception {
// 创建一个MessageDigest实例:
MessageDigest md = MessageDigest.getInstance("SHA-1");
// 反复调用update输入数据:
md.update("Hello".getBytes("UTF-8"));
md.update("World".getBytes("UTF-8"));
byte[] result = md.digest(); // 20 bytes: 6f44e49f848dd8ed27f73f59ab5bd4631b3f6b0d
//转为16进制字符串
System.out.println(new BigInteger(1, result).toString(16));
}
}
7.小结
-
哈希算法可用于验证数据完整性,具有防篡改检测的功能;
-
常用的哈希算法有MD5、SHA-1等;
-
用哈希存储口令时要考虑彩虹表攻击。
三.BouncyCastle
Java标准库提供了一系列常用的哈希算法。但如果我们要用的某种算法,Java标准库没有提供的话, 我们可以使用现成的第三方库,直接使用(自己写难度大)
-
BouncyCastle是一个开源的第三方算法提供商;
-
BouncyCastle提供了很多Java标准库没有提供的哈希算法和加密算法;
引入BouncyCastle依赖
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId>
<version>1.46</version>
</dependency>
Java标准库的java.security
包提供了一种标准机制,允许第三方提供商无缝接入
。我们要使用BouncyCastle
提供的RipeMD160
算法,需要先把BouncyCastle注册一下
:
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.Security;
public class TestBouncyCastle {
public static void main(String[] args) throws Exception {
// 注册BouncyCastle:
Security.addProvider(new BouncyCastleProvider());
// 按名称正常调用:
MessageDigest md = MessageDigest.getInstance("RipeMD160");
md.update("HelloWorld".getBytes("UTF-8"));
//转换为16进制字符串
byte[] result = md.digest();
System.out.println(new BigInteger(1, result).toString(16));
}
}
注册只需要在启动时进行一次
,后续就可以使用BouncyCastle提供的所有哈希算法
和加密算法
。
四.Hmac算法
1.什么是Hmac算法?
前面讲到哈希算法时,强调存储用户的哈希密码时,要加盐存储
,目的就在于抵御彩虹表攻击。
回顾一下哈希算法
digest = hash(input)
正是因为相同的输入会产生相同的输出
,我们加盐的目的就在于,使得输入有所变化
:
digest = hash(salt + input)
salt
变量可以看作是一个额外的“认证码”
,同样的输入,不同的认证码,会产生不同的输出。 因此,要验证输出的哈希,必须同时提供“认证码”。
Hmac算法就是一种基于密钥的消息认证码算法
,它的全称是Hash-based Message Authentication Code
,是一种更安全的消息摘要算法。
Hmac算法总是和某种哈希算法配合起来用的
。例如,我们使用MD5算法
,对应的就是HmacMD5算法
,它相当于“加盐”的MD5
:
HmacMD5 ≈ md5(secure_random_key, input)
因此,HmacMD5可以看作带有一个安全的key
的MD5。使用HmacMD5
而不是用MD5加salt,有如下好处:
- HmacMD5使用的
key长度是64字节
,更安全; - Hmac是
标准算法
,同样适用于SHA-1等其他哈希算法
,如HmacSHA1; - Hmac
输出
和原有的哈希算法长度
一致。
可见,Hmac本质上就是把key混入摘要的算法。
- 验证此哈希时,除了
原始的输入数据,
还要提供key
。 - 为了保证安全,
我们不会自己指定key
,而是通过Java标准库的KeyGenerator
生成一个安全的随机的key
。
2.Java中使用Hmac算法
下面是使用HmacMD5的代码:
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import java.math.BigInteger;
public class TestHmacMD5Encryption {
public static void main(String[] args) throws Exception {
//通过JavaApi生成安全的随机Key
KeyGenerator keyGen = KeyGenerator.getInstance("HmacMD5");
SecretKey key = keyGen.generateKey();
// 打印随机生成的key:
byte[] skey = key.getEncoded();
System.out.println(new BigInteger(1, skey).toString(16));
//获取HmacMD5算法实例
Mac mac = Mac.getInstance("HmacMD5");
//初始化key
mac.init(key);
//输入数据
mac.update("HelloWorld".getBytes("UTF-8"));
//将输出转换成16进制字符串
byte[] result = mac.doFinal();
System.out.println(new BigInteger(1, result).toString(16));
}
}
[97, -46, 58, 88, -86, -31, 54, 32, 89, -2, 49, -39, -66, -22, 95, -18, -120, 49, 47, 126, 27, 109, 11, 10, -63, -57, -94, 14, 31, 13, -97, -101, 95, 104, -78, 13, -117, -110, 64, 95, 61, 76, 27, -93, 106, 113, -89, 63, -41, 4, 1, 100, 38, -88, 99, 29, -60, -67, -46, 98, -60, -82, -124, 37]
61d23a58aae1362059fe31d9beea5fee88312f7e1b6d0b0ac1c7a20e1f0d9f9b5f68b20d8b92405f3d4c1ba36a71a73fd704016426a8631dc4bdd262c4ae8425
c3610512aa3a053a44f918bcac692e8c
和MD5相比,使用HmacMD5的步骤是:
- 通过名称HmacMD5获取KeyGenerator实例;
- 通过KeyGenerator创建一个SecretKey实例;
- 通过名称HmacMD5获取Mac实例;
- 用SecretKey初始化Mac实例;
- 对Mac实例反复调用update(byte[])输入数据;
- 调用Mac实例的doFinal()获取最终的哈希值。
我们可以用Hmac算法取代原有的自定义的加盐算法,因此,存储用户名和密码的数据库结构如下:
有了Hmac
计算的哈希
和SecretKey
,我们想要验证怎么办?
- 这时,
SecretKey
不能从KeyGenerator生成
,而是从一个byte[]数组恢复:
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigInteger;
public class TestHmacMD5Decryption {
public static void main(String[] args) throws Exception {
byte[] hkey = new byte[]{97, -46, 58, 88, -86, -31, 54, 32, 89, -2, 49, -39, -66, -22, 95,
-18, -120, 49, 47, 126, 27, 109, 11, 10, -63, -57, -94, 14, 31, 13,
-97, -101, 95, 104, -78, 13, -117, -110, 64, 95, 61, 76, 27,
-93, 106, 113, -89, 63, -41, 4, 1, 100, 38, -88, 99, 29, -60, -67, -46, 98, -60, -82, -124, 37};
//字节数组转换后的16进制字符串
System.out.println(new BigInteger(1, hkey).toString(16));
SecretKey key = new SecretKeySpec(hkey, "HmacMD5");
Mac mac = Mac.getInstance("HmacMD5");
mac.init(key);
mac.update("HelloWorld".getBytes("UTF-8"));
byte[] result = mac.doFinal();
System.out.println(new BigInteger(1, result).toString(16));
}
}
61d23a58aae1362059fe31d9beea5fee88312f7e1b6d0b0ac1c7a20e1f0d9f9b5f68b20d8b92405f3d4c1ba36a71a73fd704016426a8631dc4bdd262c4ae8425
c3610512aa3a053a44f918bcac692e8c
恢复SecretKey
的语句就是new SecretKeySpec(hkey, "HmacMD5")
。
3.小结
- Hmac算法是一种标准的基于密钥的哈希算法,可以配合
MD5、SHA-1
等哈希算法,计算的摘要长度
和原摘要算法长度相同
。
五.对称加密算法
1.什么是对称加密算法?
对称加密算法就是传统
的用 同一个密钥进行加密和解密。
- 例如,我们常用的
WinZIP
和WinRAR
对压缩包的加密和解密
,就是使用对称加密算法
:
从程序的角度
看,所谓加密,就是这样一个函数,它接收密码和明文,然后输出密文:
secret = encrypt(key, message);
//key为密码 message为明文 secret为密钥
而解密则相反,它接收密码
和密文
,然后输出明文
:
plain = decrypt(key, secret);
//key为密码 secret为encrypt(key, message)返回的密钥
2.常用的对称加密算法
算法 | 密钥长度 | 工作模式 | 填充模式 |
---|---|---|---|
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算法
由于密钥过短
,可以在短时间内被暴力破解
,所以现在已经不安全
了。
3.Java中使用AES加密
AES算法是目前应用最广泛的加密算法。我们先用ECB模式
加密并解密:
import java.security.*;
import java.util.Base64;
import javax.crypto.*;
import javax.crypto.spec.*;
public class TestAesECB {
public static void main(String[] args) throws Exception {
// 原文:
String message = "Hello, world!";
System.out.println("Message: " + message);//Message: Hello, world!
// 128位密钥 = 16 bytes Key:
byte[] key = "1234567890abcdef".getBytes("UTF-8"); //超出16字节会报错 :java.security.InvalidKeyException: Invalid AES key length: xxx bytes
// 加密:
byte[] data = message.getBytes("UTF-8");
byte[] encrypted = encrypt(key, data);
System.out.println("Encrypted: " + Base64.getEncoder().encodeToString(encrypted));//Encrypted: 2xiGROlFBhC57b7EGu5c3g==
// 解密:
byte[] decrypted = decrypt(key, encrypted);
System.out.println("Decrypted: " + new String(decrypted, "UTF-8"));//Decrypted: Hello, world!
}
// 加密:
public static byte[] encrypt(byte[] key, byte[] input) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
SecretKey keySpec = new SecretKeySpec(key, "AES");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
return cipher.doFinal(input);
}
// 解密:
public static byte[] decrypt(byte[] key, byte[] input) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
SecretKey keySpec = new SecretKeySpec(key, "AES");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
return cipher.doFinal(input);
}
}
Java标准库提供的对称加密接口非常简单,使用时按以下步骤编写代码:
- 根据算法名称/工作模式/填充模式获取
Cipher
实例; - 根据算法名称初始化一个
SecretKey实例
,密钥必须是指定长度
; - 使用
SerectKey初始化Cipher实例
,并设置加密或解密模式
; - 传入明文或密文,获得密文或明文。
ECB模式是最简单
的AES加密模式,它只需要一个固定长度的密钥
,固定的明文会生成固定的密文
,这种一对一的加密方式会导致安全性降低
- 更好的方式是通过
CBC模式
,它需要一个随机数作为IV参数
,这样对于同一份明文,每次生成的密文都不同:
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.util.Base64;
public class TestAesCBC {
public static void main(String[] args) throws Exception {
// 原文:
String message = "Hello, world!";
System.out.println("Message: " + message);
// 256位密钥 = 32 bytes Key:
byte[] key = "1234567890abcdef1234567890abcdef".getBytes("UTF-8");
// 加密:
byte[] data = message.getBytes("UTF-8");
byte[] encrypted = encrypt(key, data);
System.out.println("Encrypted: " + Base64.getEncoder().encodeToString(encrypted));
// 解密:
byte[] decrypted = decrypt(key, encrypted);
System.out.println("Decrypted: " + new String(decrypted, "UTF-8"));
}
// 加密:
public static byte[] encrypt(byte[] key, byte[] input) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
// CBC模式需要生成一个16 bytes的initialization vector:
SecureRandom sr = SecureRandom.getInstanceStrong();
byte[] iv = sr.generateSeed(16);
IvParameterSpec ivps = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivps);
byte[] data = cipher.doFinal(input);
// IV不需要保密,把IV和密文一起返回:
return join(iv, data);
}
// 解密:
public static byte[] decrypt(byte[] key, byte[] input) throws GeneralSecurityException {
// 把input分割成IV和密文:
byte[] iv = new byte[16];
byte[] data = new byte[input.length - 16];
System.arraycopy(input, 0, iv, 0, 16);
System.arraycopy(input, 16, data, 0, data.length);
// 解密:
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
IvParameterSpec ivps = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivps);
return cipher.doFinal(data);
}
public static byte[] join(byte[] bs1, byte[] bs2) {
byte[] r = new byte[bs1.length + bs2.length];
System.arraycopy(bs1, 0, r, 0, bs1.length);
System.arraycopy(bs2, 0, r, bs1.length, bs2.length);
return r;
}
}
- 在CBC模式下,需要一个
随机生成的16字节IV参数
,必须使用SecureRandom
生成。因为多了一个IvParameterSpec实例
,因此,初始化方法需要调用Cipher
的一个重载方法并传入IvParameterSpec
。
观察输出,可以发现每次生成的IV不同,密文也不同。
4.小结
-
对称加密算法使用同一个密钥进行加密和解密,常用算法有
DES、AES和IDEA
等; -
密钥长度由
算法设计
决定,AES的密钥长度是128/192/256位
; -
使用对称加密算法需要指定
算法名称
、工作模式
和填充模式
。
六.口令加密算法
1.什么是口令加密算法?
第5节
我们讲的AES加密
,细心的童鞋可能会发现,密钥长度是固定的128/192/256位,而不是我们用WinZip/WinRAR
那样,随便输入几位都可以
。
- 这是因为对称加密算法决定了
口令
必须是固定长度
,然后对明文进行分块加密
。又因为安全需求,口令长度往往都是128位
以上,即至少16个字符
。
但是我们平时使用的加密软件,输入6位、8位
都可以,难道加密方式不一样?
-
实际上用户输入的口令并
不能直接作为AES的密钥进行加密
(除非长度恰好是128/192/256位),并且用户输入的口令一般都有规律
,安全性远远不如安全随机数产生的随机口令
。 -
因此,用户输入的口令,通常还需要使用
PBE算法
,采用随机数杂凑计算
出真正的密钥
,再进行加密
。 -
PBE
就是Password Based Encryption的缩写,它的作用如下:key = generate(userPassword, secureRandomPassword);
- PBE的作用就是把用户输入的口令和一个安全随机的口令采用杂凑后计算出真正的密钥。
2.Java中使用PBE
- 引入BouncyCastle依赖
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId>
<version>1.46</version>
</dependency>
- 以AES密钥为例,我们让用户
输入一个口令
,然后生成一个随机数
,通过PBE算法计算出真正的AES口令
,再进行加密
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Base64;
public class TestPBE {
public static void main(String[] args) throws Exception {
// 把BouncyCastle作为Provider添加到java.security:
Security.addProvider(new BouncyCastleProvider());
// 原文:
String message = "Hello, world!";
// 加密口令:
String password = "hello12345";
// 16 bytes随机salt:盐的意思
byte[] salt = SecureRandom.getInstanceStrong().generateSeed(16);
System.out.printf("salt: "+ new BigInteger(1, salt));
// 加密:
byte[] data = message.getBytes("UTF-8");
byte[] encrypted = encrypt(password, salt, data);
System.out.println("encrypted: " + Base64.getEncoder().encodeToString(encrypted));
// 解密:
byte[] decrypted = decrypt(password, salt, encrypted);
System.out.println("decrypted: " + new String(decrypted, "UTF-8"));
}
// 加密:
public static byte[] encrypt(String password, byte[] salt, byte[] input) throws GeneralSecurityException {
PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
SecretKeyFactory skeyFactory = SecretKeyFactory.getInstance("PBEwithSHA1and128bitAES-CBC-BC");
SecretKey skey = skeyFactory.generateSecret(keySpec);
PBEParameterSpec pbeps = new PBEParameterSpec(salt, 1000);
Cipher cipher = Cipher.getInstance("PBEwithSHA1and128bitAES-CBC-BC");
cipher.init(Cipher.ENCRYPT_MODE, skey, pbeps);
return cipher.doFinal(input);
}
// 解密:
public static byte[] decrypt(String password, byte[] salt, byte[] input) throws GeneralSecurityException {
PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
SecretKeyFactory skeyFactory = SecretKeyFactory.getInstance("PBEwithSHA1and128bitAES-CBC-BC");
SecretKey skey = skeyFactory.generateSecret(keySpec);
PBEParameterSpec pbeps = new PBEParameterSpec(salt, 1000);
Cipher cipher = Cipher.getInstance("PBEwithSHA1and128bitAES-CBC-BC");
cipher.init(Cipher.DECRYPT_MODE, skey, pbeps);
return cipher.doFinal(input);
}
}
-
使用PBE时,我们还需要引入
BouncyCastle
,并指定算法是PBEwithSHA1and128bitAES-CBC-B
C。 -
观察代码,实际上真正的
AES密钥
是调用Cipher的init()
方法时同时传入SecretKey和PBEParameterSpec
实现的。 -
在
创建PBEParameterSpec
的时候,我们还指定了循环次数
1000,循环次数越多,暴力破解需要的计算量就越大。- 如果我们把salt(随机数)和循环次数固定,就得到了一个通用的“口令”加密软件。
- 如果我们把随机生成的
salt存储在U盘
,就得到了一个“口令”
加USB Key的加密软件
,它的好处在于,即使用户使用了一个非常弱的口令
,没有USB Key
仍然无法解密,因为USB Key
存储的随机数密钥
安全性非常高
3.小结
-
PBE算法通过
用户口令
和安全的随机salt
计算出Key,然后再进行加密; -
Key通过口令和安全的随机salt计算得出,大大提高了安全性;
-
PBE算法内部
使用的仍然是标准对称加密算法(例如AES)
七.密钥交换算法
1.什么是密钥交换算法?
对称加密算法解决了数据加密的问题。
- 我们以AES加密为例,在现实世界中,小明要向路人甲发送一个加密文件,他可以
先生成一个AES密钥
,对文件进行加密*
,然后把加密文件发送给对方。因为对方要解密,就必须需要小明生成的密钥。
现在问题来了:如何传递密钥?
- 在不安全的通信通道上传递加密文件是没有问题的,因为黑客拿到加密文件没有用。但是,如何如何
在不安全的信道上安全地传输密钥
?- 要解决这个问题,密钥交换算法即
DH算法
:Diffie-Hellman
算法应运而生。 - DH算法解决了密钥在
双方不直接传递密钥的情况下完成密钥交换
,这个神奇的交换原理完全由数学理论
支持。
- 要解决这个问题,密钥交换算法即
DH算法交换密钥的步骤。假设甲乙双方需要传递密钥,他们之间可以这么做:
- 甲首选选择一个素数p,例如: 509,底数g,任选,例如:5,随机数a,例如: 123,然后计算
A=g^a mod p
,结果是215,然后,甲发送p=509,g=5,A=215
给乙; - 乙方收到后,也选择一个随机数b,例如:
456
,然后计算B=g^b mod p
,结果是181,乙再同时计算s=A^b mod p
,结果是121; - 乙把计算的B=181发给甲,甲计算
s=B^a mod p
的余数,计算结果与乙算出的结果一样,都是121。
- 最终双方协商出的密钥s是121。这个
密钥s并没有在网络上传输
。而通过网络传输的p,g
, A和B是无法推算出s的,因为实际算法选择的素数是非常大的- 更确切地说,DH算法是一个
密钥协商算法
,双方最终协商出一个共同的密钥
,而这个密钥不会通过网络传输
。
- 更确切地说,DH算法是一个
如果我们把a看成甲的私钥
,A看成甲的公钥
,b看成乙的私钥
,B看成乙的公钥
,DH算法的本质就是双方各自生成自己的私钥和公钥
,私钥仅对自己可见
,然后交换公钥
,并根据自己的私钥和对方的公钥
,生成最终的密钥secretKey
DH算法通过数学定律
保证了双方各自计算出的secretKey是相同的
。
2.Java中使用密钥交换算法
import javax.crypto.KeyAgreement;
import java.math.BigInteger;
import java.security.*;
import java.security.spec.X509EncodedKeySpec;
public class TestPE {
public static void main(String[] args) {
// Bob和Alice:
Person bob = new Person("Bob");
Person alice = new Person("Alice");
// 各自生成KeyPair:
bob.generateKeyPair();
alice.generateKeyPair();
// 双方交换各自的PublicKey:
// Bob根据Alice的PublicKey生成自己的本地密钥:
bob.generateSecretKey(alice.publicKey.getEncoded());
// Alice根据Bob的PublicKey生成自己的本地密钥:
alice.generateSecretKey(bob.publicKey.getEncoded());
// 检查双方的本地密钥是否相同:
bob.printKeys();
alice.printKeys();
// 双方的SecretKey相同,后续通信将使用SecretKey作为密钥进行AES加解密...
}
}
class Person {
public final String name;
public PublicKey publicKey;
private PrivateKey privateKey;
private byte[] secretKey;
public Person(String name) {
this.name = name;
}
// 生成本地KeyPair:
public void generateKeyPair() {
try {
KeyPairGenerator kpGen = KeyPairGenerator.getInstance("DH");
kpGen.initialize(512);
KeyPair kp = kpGen.generateKeyPair();
this.privateKey = kp.getPrivate();
this.publicKey = kp.getPublic();
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
public void generateSecretKey(byte[] receivedPubKeyBytes) {
try {
// 从byte[]恢复PublicKey:
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(receivedPubKeyBytes);
KeyFactory kf = KeyFactory.getInstance("DH");
PublicKey receivedPublicKey = kf.generatePublic(keySpec);
// 生成本地密钥:
KeyAgreement keyAgreement = KeyAgreement.getInstance("DH");
keyAgreement.init(this.privateKey); // 自己的PrivateKey
keyAgreement.doPhase(receivedPublicKey, true); // 对方的PublicKey
// 生成SecretKey密钥:
this.secretKey = keyAgreement.generateSecret();
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
public void printKeys() {
System.out.println("Name:" + this.name);
System.out.println("Private key:" + new BigInteger(1, this.privateKey.getEncoded()));
System.out.println("Public key: " + new BigInteger(1, this.publicKey.getEncoded()));
System.out.println("Secret key: " + new BigInteger(1, this.secretKey));
}
}
执行结果:双方的生成私钥/公钥不一样,但密钥是一样的
Name: Bob
Private key:
3081d202010030819706092a864886f70d010301308189024100fca682ce8e12caba26efccf7110e526db078b05edecbcd1eb4a208f3ae1617ae01f35b91a47e6df63413c5e12ed0899bcd132acd50d99151bdc43ee737592e170240678471b27a9cf44ee91a49c5147db1a9aaf244f05a434d6486931d2d14271b9e35030b71fd73da179069b32e2935630e1c2062354d0da20a6c416e50be794ca4020201800433023100a6e17f260c8fe1a50a6fea55521fd3eae9ae288a42a58e8366ce57eed09e9d2c39d5b13631beecc274eb0b1f76f2921f
Public key:
3081e030819706092a864886f70d010301308189024100fca682ce8e12caba26efccf7110e526db078b05edecbcd1eb4a208f3ae1617ae01f35b91a47e6df63413c5e12ed0899bcd132acd50d99151bdc43ee737592e170240678471b27a9cf44ee91a49c5147db1a9aaf244f05a434d6486931d2d14271b9e35030b71fd73da179069b32e2935630e1c2062354d0da20a6c416e50be794ca402020180034400024100af320ffdd7b48571753d943bac8806251764f0d0847d4c3d3b152c980c8f12cf353f41894112b6c2be46b1d5ce467ac6e5b743b0db874442e7b47ab0ac4ebace
Secret key:
f3ab293e8a5c5ef48110debef20e54359b9fa5808e2cc46d22dab6b42e75b7bd94d5a858dcfb66d9c80f13b914f0fd58f95d503c612585202d77a8c916eb8be9
Name: Alice
Private key:
3081d202010030819706092a864886f70d010301308189024100fca682ce8e12caba26efccf7110e526db078b05edecbcd1eb4a208f3ae1617ae01f35b91a47e6df63413c5e12ed0899bcd132acd50d99151bdc43ee737592e170240678471b27a9cf44ee91a49c5147db1a9aaf244f05a434d6486931d2d14271b9e35030b71fd73da179069b32e2935630e1c2062354d0da20a6c416e50be794ca4020201800433023100a611a42054f7f379b9f042716073c2d203121a9c63994a15ec209ef6e5be5b93837e11bba79a66c6e236c3fcf309898b
Public key:
3081e030819706092a864886f70d010301308189024100fca682ce8e12caba26efccf7110e526db078b05edecbcd1eb4a208f3ae1617ae01f35b91a47e6df63413c5e12ed0899bcd132acd50d99151bdc43ee737592e170240678471b27a9cf44ee91a49c5147db1a9aaf244f05a434d6486931d2d14271b9e35030b71fd73da179069b32e2935630e1c2062354d0da20a6c416e50be794ca4020201800344000241009b12a12b1a3d5a6933e16f17ef5acee4965c0609a655f2fabd8fe704194ea1800554c21b28da28556c0d50f24c72f14a8612bd39187f5debb6e224d14e34e261
Secret key:
f3ab293e8a5c5ef48110debef20e54359b9fa5808e2cc46d22dab6b42e75b7bd94d5a858dcfb66d9c80f13b914f0fd58f95d503c612585202d77a8c916eb8be9
Name: Bob
Private key:
3081d202010030819706092a864886f70d010301308189024100fca682ce8e12caba26efccf7110e526db078b05edecbcd1eb4a208f3ae1617ae01f35b91a47e6df63413c5e12ed0899bcd132acd50d99151bdc43ee737592e170240678471b27a9cf44ee91a49c5147db1a9aaf244f05a434d6486931d2d14271b9e35030b71fd73da179069b32e2935630e1c2062354d0da20a6c416e50be794ca4020201800433023100c609ee8f68af1792ac644702f8d31d1bbb068ef59d8d4a23a26cf8f6b056c1d41c956cd26155f6a11d49f804354e0e17
Public key:
3081e030819706092a864886f70d010301308189024100fca682ce8e12caba26efccf7110e526db078b05edecbcd1eb4a208f3ae1617ae01f35b91a47e6df63413c5e12ed0899bcd132acd50d99151bdc43ee737592e170240678471b27a9cf44ee91a49c5147db1a9aaf244f05a434d6486931d2d14271b9e35030b71fd73da179069b32e2935630e1c2062354d0da20a6c416e50be794ca402020180034400024100e9ebb65b9af3d2e692dad538da8850a71111aa38953c39008080762f196c534218f3e71905558e2473e2a12b9c5f967285a5f8391ce61610497e5fe4a45faa84
Secret key:
80b9e53f8b5703dca9e3dcd4820cd912668d3682d50ca60f9fa4292eccdd4918783b8b512f873ab29ba526afa0a1f9d7a844ae22b82b616c5e62f329129c1571
Name: Alice
Private key:
3081d202010030819706092a864886f70d010301308189024100fca682ce8e12caba26efccf7110e526db078b05edecbcd1eb4a208f3ae1617ae01f35b91a47e6df63413c5e12ed0899bcd132acd50d99151bdc43ee737592e170240678471b27a9cf44ee91a49c5147db1a9aaf244f05a434d6486931d2d14271b9e35030b71fd73da179069b32e2935630e1c2062354d0da20a6c416e50be794ca40202018004330231008a70d28bb2693a8938aed4f6f0110537ca08cfb8c2e2d38b82b695d259090c28e6c0fce018e0601f3fb0ee032dfb42b2
Public key:
3081df30819706092a864886f70d010301308189024100fca682ce8e12caba26efccf7110e526db078b05edecbcd1eb4a208f3ae1617ae01f35b91a47e6df63413c5e12ed0899bcd132acd50d99151bdc43ee737592e170240678471b27a9cf44ee91a49c5147db1a9aaf244f05a434d6486931d2d14271b9e35030b71fd73da179069b32e2935630e1c2062354d0da20a6c416e50be794ca4020201800343000240539adde87d828ec754c25d9324cc69ef37d63f41c1b0e7b7c3ca8fa740fc588bebcdcc10e42787e05e6a42e201311064b627f5dc0b86abbcf06df13b880f5512
Secret key:
80b9e53f8b5703dca9e3dcd4820cd912668d3682d50ca60f9fa4292eccdd4918783b8b512f873ab29ba526afa0a1f9d7a844ae22b82b616c5e62f329129c1571`
但DH算法并未解决中间人攻击
,即甲乙双方并不能确保与自己通信的是否真的是对方
。消除中间人攻击需要其他方法。
3.小结
-
DH算法是一种
密钥交换协议
,通信双方通过不安全的通信通道协商密钥,然后进行对称加密传输
。 -
DH算法
没有解决中间人攻击
。
八.非对称加密算法
1.什么是非对称加密算法
从DH算法我们可以看到,公钥-私钥
组成的密钥
对是非常有用的加密方式,因为公钥是可以公开的
,而私钥是完全保密的
,由此奠定了非对称加密的基础。
- 非对称加密就是
加密
和解密
使用的不是相同的密钥
:只有同一个公钥-私钥对才能正常加解密。
如果小明要加密一个文件发送给小红,他应该首先向小红索取她的公钥
,然后,他用小红的公钥加密
,把加密文件发送给小红,此文件只能由小红的私钥解开
,因为小红的私钥在她自己手里
,所以,除了小红,没有任何人能解开此文件
。
非对称加密的典型算法就是RSA算法
,它是由Ron Rivest,Adi Shamir,Leonard Adleman
这三个哥们一起发明的,所以用他们仨的姓的首字母缩写表示。
非对称加密相比对称加密的显著优点在于:
- 对称加密需要
协商密钥
,而非对称加密可以安全地公开各自的公钥
,在N个人之间通信
的时候:使用非对称加密只需要N个密钥对,每个人只管理自己的密钥对
。 - 而
使用对称加密需
要则需要N*(N-1)/2
个密钥,因此每个人需要管理N-1个密钥
,密钥管理难度大,而且非常容易泄漏
。
既然非对称加密这么好,那我们抛弃对称加密,完全使用非对称加密行不行?
- 也不行。因为
非对称加密的缺点就是运算速度非常慢,比对称加密要慢很多
。 - 在实际应用中,
非对称加密总是和对称加密一起使用
。
假设小明需要给小红需要传输加密文件,他俩首先交换了各自的公钥,然后:
- 小明生成一个随机的AES口令,然后用小红的公钥通过RSA加密这个口令,并发给小红;
- 小红用自己的RSA私钥解密得到AES口令;
- 双方使用这个共享的AES口令用AES加密通信。
- 可见非对称加密实际上应用在第一步,即
加密“AES口令”
。这也是我们在浏览器中常用的HTTPS协议的做法
,即浏览器和服务器先通过RSA
交换AES口令
,接下来双方通信实际上采用的是速度较快的AES对称加密
,而不是缓慢的RSA非对称加密。
2.常用加密算法
DH(Diffie-Hellman)密钥交换算法
密钥长度 | 默认 | 实现方 |
---|---|---|
512~1024 | 1024 | JDK |
RSA 基于因子分解
- 支持公钥加密、私钥解密
- 支持私钥加密、公钥解密
密钥长度 | 默认 | 工作方式 | 填充方式 | 实现方 |
---|---|---|---|---|
512~65536(64整数倍) | 1024 | ECB | NoPadding PKCS1Padding OAEPWITHMD5AndMGF1Pading OAEPWITHSHA1AndMGF1Pading OAEPWITHSHA256AndMGF1Pading OAEPWITHSHA384AndMGF1Pading OAEPWITHSHA512AndMGF1Pading |
JDK |
512~65536(64整数倍) | 2048 | NONE | NoPadding PKCS1Padding OAEPWITHMD5AndMGF1Pading OAEPWITHSHA1AndMGF1Pading OAEPWITHSHA224AndMGF1Pading OAEPWITHSHA256AndMGF1Pading OAEPWITHSHA384AndMGF1Pading OAEPWITHSHA512AndMGF1Pading ISO9796-1Padding |
BC |
ElGamal 基于离散对数
- 只支持公钥加密、私钥解密
密钥长度 | 默认 | 工作方式 | 填充方式 | 实现方 |
---|---|---|---|---|
160~16384(8整数倍) | 1024 | ECB/NONE | NoPadding PKCS1Padding OAEPWITHMD5AndMGF1Pading OAEPWITHSHA1AndMGF1Pading OAEPWITHSHA224AndMGF1Pading OAEPWITHSHA256AndMGF1Pading OAEPWITHSHA384AndMGF1Pading OAEPWITHSHA512AndMGF1Pading |
BC |
3.Java中使用非对称加密:RSA
Java标准库提供了RSA算法的实现
import javax.crypto.Cipher;
import java.math.BigInteger;
import java.security.*;
public class TestRSA {
public static void main(String[] args) throws Exception {
// 明文:
byte[] plain = "Hello, encrypt use RSA".getBytes("UTF-8");
// 创建公钥/私钥对:
User alice = new User("Alice");
// 用Alice的公钥加密:
byte[] pk = alice.getPublicKey();
System.out.println("public key: "+ new BigInteger(1, pk));
byte[] encrypted = alice.encrypt(plain);
System.out.println("encrypted:" + new BigInteger(1, encrypted));
// 用Alice的私钥解密:
byte[] sk = alice.getPrivateKey();
System.out.println("private key: " + new BigInteger(1, sk));
byte[] decrypted = alice.decrypt(encrypted);
System.out.println("message:"+new String(decrypted, "UTF-8"));
}
}
class User {
String name;
// 私钥:
PrivateKey sk;
// 公钥:
PublicKey pk;
public User(String name) throws GeneralSecurityException {
this.name = name;
// 生成公钥/私钥对:
KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA");
kpGen.initialize(1024);
KeyPair kp = kpGen.generateKeyPair();
this.sk = kp.getPrivate();
this.pk = kp.getPublic();
}
// 把私钥导出为字节
public byte[] getPrivateKey() {
return this.sk.getEncoded();
}
// 把公钥导出为字节
public byte[] getPublicKey() {
return this.pk.getEncoded();
}
// 用公钥加密:
public byte[] encrypt(byte[] message) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, this.pk);
return cipher.doFinal(message);
}
// 用私钥解密:
public byte[] decrypt(byte[] input) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, this.sk);
return cipher.doFinal(input);
}
}
public key:
258483531987721813854435365666199783121097212864526576114955744050873233416978460385720807632511042025887321394199564988946925113552474628257578991426222518653492752073825557973941580976185929855755126136184460429858023625970309769480473617744643360199685949175089088072083451603416629173866359385161557606583489570259164788422501178077574742191521899675123594872473057815439214721531707393
encrypted:7357701548650248397119186199341212128105949131996823043095135810722853199598929412630336578050855655046901801152625111930484804989299120995259427666426900233541115482457721971843862041591087250146419933728673655872715134494920722508625411515575908243379933616713059978941673551459654375285626706144680751126
private key:
126389230230897930352385109045517175528919326976939639978192012327670621489047580094424093347895106473158995486297818153845100800374066219279088270227103776443346678559225651411243949057439018803818712562334372813466479573095825995902793365554040228324983073023437959077332782446806445389451178446097902522297420849449067653048526927048574038057913418519602892955157832046281795231488786746134458680510642411365508145522713289277432059987480268926571507025615189816131078478165538128065100495965869638623035503190141246642682592750048468556149331982161723143365302759633779097391464221693355683426424121095237273122987899008802581769295560288684023705564826027759156975759007789231144598980631325861123895628082804273304568675956078395362737203472984044346631391512204890077550208901182260350291766794706465472560035674485061043795810860066257947799776736931896171929054531087538236760527363471302299771294463030776203043926087328831636000763083457579972948329590512529023631941605115445241577795248651015223721616421418033789583560300106264160016192613765394342701914265994233654249542852184833300693419417541839857094943971266461361484207091880715767169648337876589064339760488255578132653962744328960998120052746479817803473343253095383715676383706837674601440495451078432523646329280999963815924027868199915996880222209002556445360526767683097111028908143071002120728228693248288394880817434832942256683270449130135658991967108731314978501849548376780523770946985086628520495555970515338612022941015090369221423920279064552
message:
Hello, encrypt use RSA
RSA的公钥
和私钥
都可以通过getEncoded()方法获
得以byte[]表示的二进制数据,并根据需要
保存到文件中。要从
byte[]数组恢复公钥或私钥`,可以这么写:
byte[] pkData = ...
byte[] skData = ...
KeyFactory kf = KeyFactory.getInstance("RSA");
// 恢复公钥:
X509EncodedKeySpec pkSpec = new X509EncodedKeySpec(pkData);
PublicKey pk = kf.generatePublic(pkSpec);
// 恢复私钥:
PKCS8EncodedKeySpec skSpec = new PKCS8EncodedKeySpec(skData);
PrivateKey sk = kf.generatePrivate(skSpec);
-
以RSA算法为例,它的密钥有
256/512/1024/2048/4096
等不同的长度。长度越长,密码强度越大,当然计算速度也越慢。 -
如果
修改待加密的byte[]数据的大小
,可以发现,使用512bit
的RSA加密时,明文长度不能超过53字节,使用1024bit
的RSA加密时,明文长度不能超过117字节,这也是为什么使用RSA的时候,总是配合AES一起使用
,即用AES加密任意长度的明文,用RSA加密AES口令。 -
此外,只使用非对称加密算法不能防止中间人攻击。
4.小结
-
非对称加密就是加密和解密使用的
不同的密钥
,只有同一个公钥-私钥对才能正常加解密; -
只使用非对称加密算法不能防止中间人攻击。
九.签名算法
1.什么是签名算法?
我们使用非对称加密算法的时候,对于一个公钥-私钥对,通常是用公钥加密,私钥解密
。
-
如果使用
私钥加密,公钥解密
是否可行呢?实际上是完全可行的 -
不过我们再仔细想一想,私钥是保密的,而公钥是公开的,用私钥加密,那相当于所有人都可以用公钥解密。这个加密有什么意义?
- 使用私钥加密的意义在于: 如果小明用自己的私钥加密了一条消息,比如小明喜欢小红,然后他公开了加密消息,由于任何人都可以用小明的公钥解密,从而使得任何人都可以确认小明喜欢小红这条消息肯定是小明发出的,任何人都不能伪造这个消息,小明也不能抵赖这条消息不是自己写的。
-
因此,
私钥加密得到的密文
实际上就是数字签名
,要验证
这个签名是否正确
,只能用私钥持有者的公钥进行解密验证。 使用数字签名的目的是为了确认某个信息确实是由某个发送方
发送的,任何人都不可能伪造消息
,并且,发送方
也不能抵赖。
在实际应用中,签名实际上并不是针对原始消息,而是针对原始消息的哈希
进行签名,即:
signature = encrypt(privateKey, sha256(message))
对签名进行验证
实际上就是用公钥解密
:
hash = decrypt(publicKey, signature)
然后把解密后的哈希
与原始消息的哈希
进行对比
。
- 因为用户总是
使用自己的私钥进行签名
,所以,私钥就相当于用户身份
。而公钥用来给外部验证用户身份
。
常用数字签名算法有:
- MD5withRSA
- SHA1withRSA
- SHA256withRSA
它们实际上就是指定某种哈希算法进行RSA签名的方式
。
2.Java中使用签名算法
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.*;
public class TestSignature {
public static void main(String[] args) throws GeneralSecurityException {
// 生成RSA公钥/私钥:
KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA");
kpGen.initialize(1024);// 初始化长度
KeyPair kp = kpGen.generateKeyPair();
PrivateKey sk = kp.getPrivate();// 私钥
PublicKey pk = kp.getPublic();// 公钥
System.out.println("public key: "+ new BigInteger(1, pk.getEncoded()));
System.out.println("private key: "+ new BigInteger(1, sk.getEncoded()));
// 待签名的消息:
byte[] message = "Hello, I am Bob!".getBytes(StandardCharsets.UTF_8);
// 用私钥签名:
Signature s = Signature.getInstance("SHA1withRSA");
s.initSign(sk);
s.update(message);
byte[] signed = s.sign();
System.out.println(String.format("signature:" + new BigInteger(1, signed)));
// 用公钥验证:
Signature v = Signature.getInstance("SHA1withRSA");
v.initVerify(pk);
v.update(message);
boolean valid = v.verify(signed);
System.out.println("valid? " + valid);
}
}
执行结果:
public key:
258483531987721813854435365666199783121097212864526576114955744050873239740064534841529078431430280855212345252013971193784630219513155514344209323368115007171233501932199794600838512078933167243135938444442565850713458066025013464926088106162861364363982447339608316017857002096226224450754197103334037956305457568176486913688450839612561473610054104693068467895141992909827115586808774657
private key:
32355642978867282667812022272056567596553430054090922075653514240779563683342184953551357425216541957796612446529058909814073720528080862054621801127166654843227642799768819028725968384862544746562215485360280789453060638228462944824599872084827243631020161353265080841807139205514780770192993963331054988422123065524419922960968664421887403874004695116675782725145963309330934850495657191622097453164134496029756676837196703909317097128831432463473803455828810246443162988226804756957190200345511572541100412421072951917135183882298475780101500368315398841788764399411582106285002787561216214909578203173073655289743866570368623923952119024912565305268781589939036052889235599652733017669767833128150590636630069137249807413779405575219428836996636109981653090511932640856624525740583672185392230062068870301520775183899502621464251272941148189917239317022424381267627164291312698901674835518627479163503799615966609958229467677857247015947806160284023751098552830353153912149839698438565300303499981516732057088341014492765179706834288592146097370286736254084544587778634951465181664428409427600713387465013533646801941618680705301607646930228454932595872773233559141853046272021795865601184766876759958931305514735967868367402449647364656450393157956894593514357977696690984503857333707225109334930323254617088349524805216380715531109203820917728527746139957459085481338702532505119524205090693816777832347752195095588033458844792170947507105793269535768803608687779803063661209071369617089225755013365173431184860361410455550
signature:
92408928084566399696005122808089985962996917163299114857688853777614627588270910849212417864611944915422686724210690796459800265563405003101016568726618924754348270787851652778241258511967647804121237214672125965154516274441599537656917786588485253772533093340489241261474027020838010848059522423315054438025
valid?
: true
使用其他公钥,或者验证签名的时候修改原始信息,都无法验证成功。
DSA签名
DSA是Digital Signature Algorithm
的缩写,它使用ElGamal数字签名算法
。
- DSA只能配合SHA使用,常用的算法有:
- SHA1withDSA
- SHA256withDSA
- SHA512withDSA
和RSA数字签名
相比,DSA的优点是更快
。
ECDSA签名
椭圆曲线签名算法ECDSA
:Elliptic Curve Digital Signature Algorithm
也是一种常用的签名算法,
- 它的特点是
可以从私钥推出公钥
。比特币的签名算法就采用了ECDSA算法,使用标准椭圆曲线secp256k1
。 BouncyCastle
提供了ECDSA的完整实现。
3.小结
-
数字签名就是用
发送方
的私钥
对原始数据
进行签名,只有用发送方公钥
才能通过签名验证
。
数字签名用于:- 防止伪造;
- 防止抵赖;
- 检测篡改。
-
常用的数字签名算法包括:
- MD5withRSA
- SHA1withRSA
- SHA256withRSA
- SHA1withDSA
- SHA256withDSA
- SHA512withDSA
- ECDSA等。
九.数字证书
1.什么是数字证书
摘要算法(哈希算法)
用来确保数据没有被篡
,非对称加密算法
可以对数据进行加解密
,签名算法
可以确保数据完整性和抗否认性
,把这些算法集合
到一起,并搞一套完善的标准
,这就是数字证书。-
因此,数字证书就是
集合了多种密码学算法
,用于实现数据加解密
、身份认证
、签名
等多种功能的一种安全标准
。 -
数字证书可以
防止中间人攻击
,因为它采用链式签名认证,即通过根证书(Root CA)
去签名下一级证书,这样层层签名
,直到最终的用户证书
。而Root CA证书内置于操作系统中
,所以,任何经过CA认证的数字证书都可以对其本身进行校验,确保证书本身不是伪造的。
-
我们在上网时常用的HTTPS协议
就是数字证书的应用
。浏览器会自动验证证书
的有效性:
要使用数字证书,首先需要创建证书。
- 正常情况下,一个
合法的数字证书
需要经过CA签名
,这需要认证域名并支付一定的费用。 - 开发的时候,我们可以使用
自签名的证书
,这种证书可以正常开发调试
,但不能对外作为服务
使用,因为其他客户端
并不认可未经CA签名的证书
。 - 在Java程序中,
数字证书
存储在一种Java专用的key store文件
中,JDK提供了一系列命令
来创建和管理key store
。
2.Java中使用数字证书
我们用下面的命令创建一个key store,并设定口令123456:
keytool -storepass 123456 -genkeypair -keyalg RSA -keysize 1024 -sigalg SHA1withRSA -validity 3650 -alias mycert -keystore my.keystore -dname "CN=www.sample.com, OU=sample, O=sample, L=BJ, ST=BJ, C=CN"
几个主要的参数是:
keyalg:指定RSA加密算法;
sigalg:指定SHA1withRSA签名算法;
validity:指定证书有效期3650天;
alias:指定证书在程序中引用的名称;
dname:最重要的CN=www.sample.com指定了Common Name,如果证书用在HTTPS中,这个名称必须与域名完全一致。
执行上述命令,JDK
会在当前目录
创建一个my.keystore
文件,并存储创建成功的一个私钥和一个证书,它的别名是mycert
。
有了key store存储的证书
,我们就可以通过数字证书
进行加解密和签名:
import java.io.FileInputStream;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.*;
import javax.crypto.Cipher;
public class TestDigitalCertificate{
public static void main(String[] args) throws Exception {
byte[] message = "Hello, use X.509 cert!".getBytes("UTF-8");
// 读取KeyStore:
KeyStore ks = loadKeyStore("C:/Users/87772/my.keystore", "123456");
// 读取私钥:
PrivateKey privateKey = (PrivateKey) ks.getKey("mycert", "123456".toCharArray());
// 读取证书:
X509Certificate certificate = (X509Certificate) ks.getCertificate("mycert");
// 加密:
byte[] encrypted = encrypt(certificate, message);
System.out.println(String.format("encrypted: %x", new BigInteger(1, encrypted)));
// 解密:
byte[] decrypted = decrypt(privateKey, encrypted);
System.out.println("decrypted: " + new String(decrypted, "UTF-8"));
// 签名:
byte[] sign = sign(privateKey, certificate, message);
System.out.println(String.format("signature: %x", new BigInteger(1, sign)));
// 验证签名:
boolean verified = verify(certificate, message, sign);
System.out.println("verify: " + verified);
}
static KeyStore loadKeyStore(String keyStoreFile, String password) {
try (InputStream input = new FileInputStream(keyStoreFile)) {
if (input == null) {
throw new RuntimeException("file not found in classpath: " + keyStoreFile);
}
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(input, password.toCharArray());
return ks;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
static byte[] encrypt(X509Certificate certificate, byte[] message) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance(certificate.getPublicKey().getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, certificate.getPublicKey());
return cipher.doFinal(message);
}
static byte[] decrypt(PrivateKey privateKey, byte[] data) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance(privateKey.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(data);
}
static byte[] sign(PrivateKey privateKey, X509Certificate certificate, byte[] message)
throws GeneralSecurityException {
Signature signature = Signature.getInstance(certificate.getSigAlgName());
signature.initSign(privateKey);
signature.update(message);
return signature.sign();
}
static boolean verify(X509Certificate certificate, byte[] message, byte[] sig) throws GeneralSecurityException {
Signature signature = Signature.getInstance(certificate.getSigAlgName());
signature.initVerify(certificate);
signature.update(message);
return signature.verify(sig);
}
}
执行结果
encrypted:
848123282e523f793cb1f661655a1e60db75cc377399906d9757f9c2dccab811426e7e68451828eb458f64fe7d6ca4bc758d96394737d8c6819f7ab98285c8c22cd7603e3a56c937aef59ff77464f40aab511bb0802d004f2d15c0d7d73df0580fa366cea43c842f15dc7c1442c8707f95fbf81b801a524b068a9bb57060767e
decrypted:
Hello, use X.509 cert!
signature:
6c1e1dd220236cff0436ec11f471e636a14efc6857bda2105148e4bd68bef2fb092d90c5a0230240eac6a38074e6f28e4d9f2cc8ac882af534edd321b0f717f1e691f3ffb6037fc1ea526d69fe085521f097897e4dd2c02f799f4909c60970c897d26d7e84d0f5739f91acd40b6baab10acef98e8f869fd9af7d9c6c766157c1
verify:
true
在上述代码中,我们从key store
直接读取了私钥-公钥对
,私钥以PrivateKey实例
表示,公钥以X509Certificate表示
,实际上数字证书只包含公钥,因此,读取证书并不需要口令
,只有读取私钥才需要
。
- 如果部署到Web服务器上,例如Nginx
,需要把私钥导出为Private Key格式
,把证书导出为X509Certificate格式
。
以HTTPS协议为例,浏览器和服务器建立安全连接的步骤如下:
- 浏览器向服务器发起请求,服务器向浏览器发送自己的数字证书;
- 浏览器用
操作系统内置的Root CA
来验证服务器的证书是否有效,如果有效,就使用该证书加密一个随机的AES口令并发送给服务器
; - 服务器用自己的
私钥解密
获得AES口令
,并在后续通讯中使用AES加密。
上述流程只是一种最常见的单向验证
。如果服务器还要验证客户端
,那么客户端也需要把自己的证书发送给服务器验证
,这种场景常见于网银
等。
注意: 数字证书存储的是公钥
,以及相关的证书链
和算法信息
。 私钥必须严格保密,如果数字证书对应的私钥泄漏,就会造成严重的安全威胁。如果CA证书的私钥泄漏,那么该CA证书签发的所有证书将不可信。数字证书服务商DigiNotar就发生过私钥泄漏导致公司破产的事故。
3.小结
-
数字证书就是集合了多种密码学算法,用于实现数据加解密、身份认证、签名等多种功能的一种安全标准。
-
数字证书采用链式签名管理,顶级的Root CA证书已内置在操作系统中。
-
数字证书存储的是公钥,可以安全公开,而私钥必须严格保密。