1 加密与安全
数据安全:防窃听,防篡改,防伪造。
摘要算法:确保信息没有被篡改
对称加密算法/非对称加密算法:对数据进行加密/解密
签名算法:确保信息的完整性和抗否认性
1.1 编码算法
什么是编码,ASCII码,Unicode,UTF-8这些就是编码,如字母 A
使用 ASCII 编码就是 0x41
,中文字的 中
使用 Unicode 编码就是 0x4e2d
,使用 UTF-8
编码就是 0xe4b8ad
。
1.1.1 URL 编码
URL 编码是浏览器发送数据给服务器时所用到的编码,它是编码算法而不是加密算法,它的目的是把任意文本数据编码为 %
为前缀表示的文本,编码后的文本仅包含A-Z
,a-z
,0-9
以及 -_.*
,这是为了便于浏览器和服务器的处理,它的编码规则是:
A-Z
,a-z
,0-9
以及-_.*
保持不变- 其它字符以
%XXX
的形式表示,如:小于号<
编码为%3C
,中文字的中
编码为%E4%B8$AD
(0xe4b8ad
)
Java 使用 URL编码需要使用到 URLEncoder
,解码需要使用 URLDecoder
,eg:
String str = "value=中 文";
String url = URLEncoder.encode(str, "UTF-8");
System.out.println(url);
String str2 = URLDecoder.decode(url, "utf-8");
System.out.println(str2);
java的
URLEncoder
与URL编码的规则有些区别,如空格符,URLEncoder
编码为+
,而URL编码的是%20
,所幸一般的服务器都可以处理这两种情况
1.1.2 Base64 编码
Base64编码是一种编码算法而不是加密算法,它的目的是把 任意二进制数据编码为文本,适用于文本协议。比方说使用笔记本打开一些二进制的文件如 jpg
,exe
等文件的时候,会看到一堆乱码,如果这时候要让记事本这样的文本处理软件能处理二进制数据,就需要一个二进制到字符串的转换方法,而 Base64 是一种最常见的二进制编码方法。
1.1.2.1 Base64 编码的原理
Base64 首先需要一个64个字符的转化表:
然后如下图所示,对表示中文字 中
的UTF-8字节进行base64编码:
- 首先对二进制的字符串进行处理,先将它们以每3个字节一组(即 3x8=24
bit
)进行划分 - 再把划分好的每一组(24个
bit
)重新划分为4等份,此时每份正好6个bit
,得到的 6个bit
之后再在其头部填两个空bit
,从而获得一个新的字节 - 此时该字节二进制最大的数值是
00111111
,转化为十进制最大的数为 63,最后再依据数值查询上面的表,就可以获得四个字符了(图中的数值是十六进制的)。
如果要编码的二进制数据不是3的倍数,最后会剩下1个或2个字节怎么办?Base64用一个或两个 0x00
字节在末尾补足,而且还会在编码的末尾加上1个或2个 =
号,表示补了多少字节,解码的时候,会自动去掉。
所以,Base64编码会把3字节的二进制数据编码为4字节的文本数据,长度增加 1/3
,好处是编码后的文本数据可以在邮件正文、网页等直接显示。
1.1.2.2 base64.getEncoder
,base64.getDecoder
,base64.getUrlEncoder
和 base64.getUrlDecoder
Java 使用 base64
类的 getEncoder
方法对二进制字节进行编码,getDecoder
方法对已经编码的字符串进行解码,其中由于某些标准的base64编码在 Url 中会引起冲突,此时需要使用 getUrlEncoder
和 getUrlDecoder
,如就是将冲突的 +
和 /
换成 -
号和 _
。
eg:
// base64编码
// 字符串转成 字节数组后,使用 getEncoder 进行编码
String bs64 = Base64.getEncoder().encodeToString(string.getBytes("utf-8"));
System.out.println(bs64);
// 使用 getDecoder 进行解码
String oriString = new String(Base64.getDecoder().decode(bs64), "utf-8");
System.out.println(oriString);
// 某些标准的base64编码在url中会引起冲突,所以使用getUrlEncoder,其实就是将 + 号和 / 号换成 - 号和 _
bs64 = Base64.getUrlEncoder().encodeToString(string.getBytes("utf-8"));
System.out.println(bs64);
oriString = new String(Base64.getUrlDecoder().decode(bs64), "utf-8");
System.out.println(oriString);
1.2 摘要算法
摘要算法又叫哈希算法(Hash
)或数字指纹(Digest
):
- 它的目的是计算任意长度数据的摘要,这个摘要是固定长度
- 相同的输入数据始终得到相同的输出
- 不同的输入数据 尽量 得到不同的输出
- 常用语验证原始数据是否被篡改
Java的 Object.hashCode()
方法就是一个摘要方法,它输入任意数据,输出固定长度的数据,输出的数字实际上是一个 int
类型,可以看成是一个 4字节的 byte
数组。
当两个不同的输入得到相同的输出时,这就叫做 碰撞,碰撞是不能避免的,这是因为输入的范围是无限的,输出的范围是有限的。
常用的摘要算法有:
- MD5:输出长度
128bits
,即16 个字节bytes
- SHA-1:输出长度
160bits
,即20 个字节bytes
- SHA-256:输出长度
256bits
,即32 个字节bytes
- RipeMD-160:输出长度
160bits
,即20 个字节bytes
1.2.1 MD5
在 Java 中使用 MD5 需要使用 MessageDigest
这个类,具体步骤:
- 使用
getInstance("MD5")
这个方法获取MessageDigest
关于 MD5 的实例 - 该实例使用
update(btye[] input)
接受需要加密的字节数组,update
方法可以多次添加字节数组,这和一次添加字节数组是一样的 - 最后使用
digest()
方法获取最终加密好的一个含有16个btye
的数组,将其转换为数字就可以互相比较了,而可以使用String.format
将其转化为 16 进制的数字易于显示
eg:
String str = "Hello,world";
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(str.getBytes("utf-8"));
byte[] bytes = md5.digest(); // 获取字节数
// 字节数组转换为十六进制数字显示,其中 `%032x` 是数字显示格式,`%x`表示使用十六进制显示数字,`032`表示使用该十六进制的位数为32个,数字小则开头自动补零
System.out.println(String.format("%032x", new BigInteger(1, bytes))); // new BigInteger 的第一个参数是表示用大于0
// update 可以分开使用,效果一样
md5 = null;
md5 = MessageDigest.getInstance("MD5");
md5.update("Hello,".getBytes("utf-8"));
md5.update("world".getBytes("utf-8"));
byte[] bytes2 = md5.digest();
System.out.println(String.format("%032x", new BigInteger(bytes)).equals(String.format("%032x", new BigInteger(bytes2))));
MD5 可常用于验证数据的完整性,如将登陆密码储存在数据库,避免明文储存,也常用于查看文件下载是否有缺失。
- 使用 MD5 储存密码时,要预防彩虹表攻击,即黑客把常用密码用MD5加密好的数据表,此时可以使用加盐(通常为
Secret Key
)的方法预防,即在编码密码前添加一个字符串- 在
MessageDigest.getInstance("MD5")
中将MD5
改成SHA-1
就可以改成SHA-1
算法加密了,需要注意的是SHA-1
算法的输出长度是20 个字节bytes
1.3 对称加密算法
对称加密算法就是:加密和解密都是用同一个秘钥,即对称加密算法在加密的时候需要输入一个 原文 和一个 密匙 从而得出一个 密文,而解密的时候就需要输入 密文 和正确的 密匙 从而得到明文。
常用的对称加密方法有:
- DES:秘钥长度
56/64
,工作模式:ECB/CBC/PCBC/CTR/...
,填充模式:NoPadding/PKCS5Padding/....
(秘钥过短,短时间能被暴力破解) - AES:秘钥长度
128/192/256
,工作模式:ECB/CBC/PCBC/CTR/...
,填充模式:NoPadding/PKCS5Padding/PKCS7Padding/....
(使用256
位秘钥,需要修改JDK的policy
文件) - IDEA:秘钥长度
128
,工作模式:ECB
,填充模式:PKCS5Padding/PKCS7Padding/....
它们的秘钥长度各不相同,秘钥的长度决定了加密的长度,使用对称加密算法需要指定 算法名称 / 工作模式 / 填充模式
,其中工作模式和填充模式可以看做对称加密算法的参数和格式的选择,JDK 内部并没有包含全部的工作模式和填充模式。
1.3.1 AES(Advanced Encryption Standard)
下例代码是使用工作模式为 ECB
,填充模式为 PKCS5Padding
的 AES 方法:
public class About_AES_ECB {
// 使用 AES 算法,指定工作模式为 ECB,填充模式为 PKCS5Padding
static final String CIPHER_NAME = "AES/ECB/PKCS5Padding";
// 加密
public static byte[] encrypt(byte[] secretKey, byte[] input) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
// 使用 Cipher.getInstance() 方法,传入加密算法的名字,工作模式以及填充模式,从而得到一个 Cipher 实例
Cipher cipher = Cipher.getInstance(CIPHER_NAME);
// 使用传入的密匙创建 AES 的 SecretKeySpec 实例
SecretKeySpec keySpec = new SecretKeySpec(secretKey, "AES");
// cipher 使用 init 方法初始化为加密模式,并传入密匙
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
// 最后使用 doFinal 传入明文,得到加密后的密文的字节数组
return cipher.doFinal(input);
}
// 解密,仅需要在初始化模式时,初始为 解密模式即可
public static byte[] decrypt(byte[] secretKey, byte[] input) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
// 使用 Cipher.getInstance() 方法,传入加密算法的名字,工作模式以及填充模式,从而得到一个 Cipher 实例
Cipher cipher = Cipher.getInstance(CIPHER_NAME);
// 使用传入的密匙创建 AES 的 SecretKeySpec 实例
SecretKeySpec keySpec = new SecretKeySpec(secretKey, "AES");
// cipher 使用 init 方法初始化为解密模式,并传入密匙
cipher.init(Cipher.DECRYPT_MODE, keySpec);
// 最后使用 doFinal 传入明文,得到加密后的密文的字节数组
return cipher.doFinal(input);
}
public static void main(String[] args) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException {
// 明文
String input = "Hello.world";
// abs 的密匙长度最短是 128 位,即最短需要 16 个字符
// 可以换成 192 或 256 位,即 24 个字符或 32 个字符
String secretKey = "1234567890abcdef";
System.out.println("加密明文是:" + input + ",秘钥是:" + secretKey);
// 加密, 注意传入参数类型是字节数组,注意 utf-8 编码
byte[] cipherText = About_AES_ECB.encrypt(secretKey.getBytes("utf-8"),input.getBytes("utf-8"));
// 用 base64 输出密文字节数组
System.out.println("base64编码过后的密文:" + Base64.getEncoder().encodeToString(cipherText));
// 解码
String decryptString = new String(About_AES_ECB.decrypt(secretKey.getBytes("utf-8"), cipherText), "utf-8");
System.out.println("解码:" + decryptString);
}
}
1.3.2 PBE(Password Base Encryption)
PBE (口令加密算法)内部使用的仍然是标准对称加密算法(如 AES),不同在于如 AES 直接生成密匙然后直接使用,而 PBE 是通过 用户口令 和 随机salt
计算秘钥后再进行加密。
如果把随机 salt
存放在U盘中,就得到了 “口令 + USB key” 的加密软件,此时即使口令非常弱,没有 USB key 也无法解密。
1.3.3 密钥交换算法
在使用对称加密算法的时候,加解密都是使用同一个密钥 key
,那么问题是如何在两个不同的终端传递秘钥?
密钥交换算法(DH 算法,全称为“Diffie-Hellman”),他是一种密钥交换协议,通信双方通过 协商 的密钥,然后进行加密传输。如下图中的 secretKeyA
就是通信双方协商过后的密钥,它是由自身的私匙和传送过来对方的公匙计算得出。
eg:
/*
* 使用 DH 算法
*
*/
class Person {
public Person(String name) {
this.name = name;
}
String name;
// 公匙
public PublicKey publicKey;
// 私匙
public PrivateKey privateKey;
// 双方协商过后通用的密匙,用于 AES 加密的
public SecretKey secretKey;
// 生成密匙对
public void generateKeyPair() throws NoSuchAlgorithmException {
// 使用 KeyPariGenerator.getInstance 方法,传入算法 DH
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DH");
// 表示输出的密钥长度是 1024 位
keyPairGenerator.initialize(512);
// 获取密钥对
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// 获取私匙
this.privateKey = keyPair.getPrivate();
// 获取公匙
this.publicKey = keyPair.getPublic();
}
// 接受对方公匙,生成密匙
public void generateSecretKey(byte[] receivedPublicKey)
throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
KeyFactory keyFactory = KeyFactory.getInstance("DH");
// 将对方发过来的公匙 byte 数组恢复为 PublicKey,使用 X509EncodedKeySpec
X509EncodedKeySpec pkSpec = new X509EncodedKeySpec(receivedPublicKey);
PublicKey receivedPK = keyFactory.generatePublic(pkSpec);
// 生成本地密匙
KeyAgreement keyAgreement = KeyAgreement.getInstance("DH");
// 传入私匙
keyAgreement.init(this.privateKey);
// 对方公匙
keyAgreement.doPhase(receivedPK, true);
// 生成本地密钥,传入 AES 表示要生成一个 AES 加密的密钥
this.secretKey = keyAgreement.generateSecret("AES");
}
// 发送加密信息
public String sendMessage(String message) throws NoSuchAlgorithmException, NoSuchPaddingException,
InvalidKeyException, IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException {
// 使用本地密钥对信息进行 AES 加密
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, this.secretKey);
byte[] sendData = cipher.doFinal(message.getBytes("utf-8"));
return Base64.getEncoder().encodeToString(sendData);
}
// 解密接受信息
public String getMessage(String message) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException {
// 使用本地密钥对信息进行 AES 接密
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, this.secretKey);
byte[] getData = cipher.doFinal(Base64.getDecoder().decode(message));
return new String(getData, "utf-8");
}
// 输出相关信息
public void print() {
System.out.println("当前类为:" + this.name);
System.out.println("私匙为:" + Base64.getEncoder().encodeToString(this.privateKey.getEncoded()));
System.out.println("公匙为:" + Base64.getEncoder().encodeToString(this.publicKey.getEncoded()));
System.out.println("本地私匙为:" + Base64.getEncoder().encodeToString(this.secretKey.getEncoded()));
System.out.println();
}
}
public class About_DH {
public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException {
// 模拟两个终端使用 DH 算法
Person bob = new Person("Bob");
Person alex = new Person("Alex");
// 生成公匙和私匙
bob.generateKeyPair();
alex.generateKeyPair();
// 协议生成本地密匙,传入对方的公匙,publickey 实例使用 getEncoded 方法可以获取 byte 数组
alex.generateSecretKey(bob.publicKey.getEncoded());
bob.generateSecretKey(alex.publicKey.getEncoded());
// 输出相关信息,只有本地密匙相同
alex.print();
bob.print();
// Alex 向 Bob 发送加密过的密文, Bob 要解密
String alexToBob = alex.sendMessage("Hello,world");
System.out.println("Bob解密后的明文:" + bob.getMessage(alexToBob));
}
}
1.4 非对称加密算法
非对称加密就是加密和解密都是用不同的密钥,分别是公钥和私钥。需要注意的一点,这个公钥和私钥必须是一对的,如果用公钥对数据进行加密,那么只有使用对应的私钥才能解密,反之亦然。
eg:
public class RSAkeyPair {
// 公钥
PublicKey publicKey;
// 私钥
PrivateKey privateKey;
// 无参构造方法,生成公钥和私钥对
public RSAkeyPair() throws NoSuchAlgorithmException {
// 使用 KeyPariGenerator.getInstance 方法,传入算法 RSA
KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA");
// 表示输出的密钥长度是 1024 位
kpGen.initialize(1024);
// 输出密钥对
KeyPair keyPair = kpGen.generateKeyPair();
// 获取私匙
this.privateKey = keyPair.getPrivate();
// 获取公匙
this.publicKey = keyPair.getPublic();
}
// 有参构造函数,通过传入的密钥对字节数组恢复密钥对(读取保存文件中的密钥对)
public RSAkeyPair(byte[] sk, byte[] pk) throws NoSuchAlgorithmException, InvalidKeySpecException {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
// 恢复公钥,使用 X509EncodedKeySpec
X509EncodedKeySpec pkSpec = new X509EncodedKeySpec(pk);
this.publicKey = keyFactory.generatePublic(pkSpec);
// 恢复私钥,使用 PKCS8EncodedKeySpec
PKCS8EncodedKeySpec skSpec = new PKCS8EncodedKeySpec(sk);
this.privateKey = keyFactory.generatePrivate(skSpec);
}
// 把私钥导出为字节
public byte[] getSk() {
return this.privateKey.getEncoded();
}
// 把公钥导出为字节
public byte[] getPk() {
return this.publicKey.getEncoded();
}
// 使用公钥加密,使用了公匙加密就必须使用私匙解密,否则会报错
public byte[] encrypt(byte[] message) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
// 创建 Cipher 实例,声明使用 RSA
Cipher cipher = Cipher.getInstance("RSA");
// 使用公匙加密
cipher.init(Cipher.ENCRYPT_MODE, this.publicKey);
return cipher.doFinal(message);
}
// 使用私匙解密,使用了公匙加密就必须使用私匙解密,否则会报错
public byte[] decrypt(byte[] input) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
// 创建 Cipher 实例,声明使用 RSA
Cipher cipher = Cipher.getInstance("RSA");
// 使用公匙加密
cipher.init(Cipher.DECRYPT_MODE, this.privateKey);
return cipher.doFinal(input);
}
public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException, InvalidKeySpecException {
String message = "Hello.world!";
RSAkeyPair keyPair = new RSAkeyPair();
byte[] cipherText = keyPair.encrypt(message.getBytes("utf-8"));
System.out.println("加密后使用Base64编码后的密文:" + Base64.getEncoder().encodeToString(cipherText));
byte[] decryptText = keyPair.decrypt(cipherText);
System.out.println("解密后的明文:" + new String(decryptText, "utf-8"));
// 模仿读取文件密匙对字节,创建密匙对
byte[] sk = keyPair.getSk();
byte[] pk = keyPair.getPk();
RSAkeyPair keyPair2 = new RSAkeyPair(sk, pk);
System.out.println("使用字节流创建的私匙能否解密:" + new String(keyPair2.decrypt(cipherText), "utf-8"));
}
}
1.4.1 签名算法
当A和B使用非对称加密进行数据传输,此时如果C拥有B的公钥,从而冒充A向B发送信息,此时要怎么办呢?
使用签名算法,它是指发送方在发送密文的同时,也发送由自身 私钥 对原始数据进行签名过后的签名数据,接受方可以使用该签名数据和发送方的 公钥 进行签名认证,从而确保该信息是否发送方发出的并且信息有否被篡改(内部大概就是使用了摘要算法确保信息没有被篡改)。这样一来发送方使用私钥对原始数据进行签名的过程就叫做 数字签名 。
它的目的:
- 它防止了不怀好意的人侵略了接受方的服务器并篡改了发送方的公匙为自己的公匙,从而可以冒充发送方给接收方发送信息,即防伪造发送方
- 防止发送方抵赖发送过的信息
- 防止信息在传送过程中被篡改
常用的方法有: MD5withRSA
、SHA1withRSA
、SHA256withRSA
eg:
public class RSASignture {
// 公钥、私钥
PublicKey pk;
PrivateKey sk;
// 无参构造方法,生成公钥和私钥对
public RSASignture() throws NoSuchAlgorithmException {
// 使用 KeyPariGenerator.getInstance 方法,传入算法 RSA
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
// 表示输出的密钥长度是 1024 位
keyPairGenerator.initialize(1024);
// 获取密钥对,获取公钥,私钥
KeyPair keyPair = keyPairGenerator.generateKeyPair();
this.pk = keyPair.getPublic();
this.sk = keyPair.getPrivate();
}
// 把私钥导出为字节
public byte[] getSk() {
return this.sk.getEncoded();
}
// 把公钥导出为字节
public byte[] getPk() {
return this.pk.getEncoded();
}
// 定义 sign 方法对数据进行签名
public byte[] sign(byte[] message) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
// 使用 Signature.getInstance 方法传入 签名算法名称如 SHA1withRSA,获取 signature 实例
Signature signature = Signature.getInstance("SHA1withRSA");
// 传入私匙,初始化签名
signature.initSign(this.sk);
// 传入明文,对明文进行签名
signature.update(message);
// 获得明文签名后的字节
return signature.sign();
}
// 验证签名,需要传入原始信息,验证数据
public boolean verify(byte[] message, byte[] sign) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
// 使用 Signature.getInstance 方法传入 签名算法名称如 SHA1withRSA,获取 signature 实例
Signature signature = Signature.getInstance("SHA1withRSA");
// 传入公匙初始化签名,表示要验证签名,只能传入公匙
signature.initVerify(this.pk);
signature.update(message);
// 验证签名
return signature.verify(sign);
}
public static void main(String[] args) throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException, SignatureException {
byte[] message = "Hello,world".getBytes("utf-8");
RSASignture rsaSignture = new RSASignture();
// 获取签名
byte[] sign = rsaSignture.sign(message);
System.out.println("签名:" + Base64.getEncoder().encodeToString(sign));
boolean verified = rsaSignture.verify(message, sign);
System.out.println("验证结果:" + verified);
// 使用别的公匙验证
boolean verified2 = new RSASignture().verify(message, sign);
System.out.println("使用别的公匙验证的结果:" + verified2);
// 修改发送信息
message[0] = 1;
boolean verified3 = rsaSignture.verify(message, sign);
System.out.println("修改发送信息后的验证结果:" + verified3);
}
}