最近由于业务需求,需要给Kafka内的报文进行加密。Kafka的上游与下游都是我们自己的系统,分析过业务场景后,决定使用对称加密算法。
对称加密算法
对称加密(也叫私钥加密)指加密和解密使用相同密钥的加密算法。在大多数的对称算法中,加密密钥和解密密钥是相同的,所以也称这种加密算法为秘密密钥算法或单密钥算法。
优点:对称加密算法的特点是算法公开、计算量小、加密速度快、加密效率高。
缺点:交易双方都使用同样钥匙,安全性得不到保证。每对用户每次使用对称加密算法时,都需要使用其他人不知道的惟一钥匙,这会使得发收信双方所拥有的钥匙数量呈几何级数增长,密钥管理成为用户的负担。对称加密算法在分布式网络系统上使用较为困难,主要是因为密钥管理困难,使用成本较高。
非对称加密算法
非对称加密算法需要两个密钥:公开密钥(publickey:简称公钥)和私有密钥(privatekey:简称私钥)。公钥与私钥是一对,如果用公钥对数据进行加密,只有用对应的私钥才能解密。
优点:算法强度复杂、安全性强。相比于对称秘钥只有一个秘钥而言,非对称密钥体制有两种密钥,其中一个是公开的,这样就可以不需要像对称密码那样传输对方的密钥了。这样安全性就大了很多。
缺点:但是由于其算法复杂,而使得加密解密速度没有对称加密解密的速度快。
什么是AES?
AES算是比较基础的对称加密算法,原理简单。
高级加密标准(AES,Advanced Encryption Standard)为最常见的对称加密算法,AES最常见的有3种方案,分别是AES-128、AES-192和AES-256,它们的区别在于密钥长度不同,AES-128的密钥长度为16bytes(128bit/8),后两者分别为24bytes和32bytes。密钥越长,安全强度越高,但伴随运算轮数的增加,带来的运算开销就会更大。
AES算法在加密过程中分为四步:
- 字节代换
- 行移位
- 列混合
- 轮密钥加
字节代换
AES的字节代换其实就是一个简单的查表操作。AES定义了一个S盒和一个逆S盒。
行移位
行移位是一个简单的左循环移位操作。当密钥长度为128比特时,状态矩阵的第0行左移0字节,第1行左移1字节,第2行左移2字节,第3行左移3字节。
列混合
列混合变换是通过矩阵相乘来实现的,经行移位后的状态矩阵与固定的矩阵相乘,得到混淆后的状态矩阵。
轮密钥加
轮密钥加是将128位轮密钥同状态矩阵中的数据进行逐位异或操作。
AES128具体实现
Windows上的首次尝试
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
public class Encrypt1 {
private final String password;
private final KeyGenerator kgen;
private final SecretKey secretKey;
private final byte[] enCodeFormat;
private final SecretKeySpec key;
private Cipher cipher;
public Encrypt1(String password) throws NoSuchAlgorithmException, NoSuchPaddingException {
this.password = password;
kgen = KeyGenerator.getInstance("AES");
kgen.init(128, new SecureRandom(password.getBytes()));
secretKey = kgen.generateKey();
enCodeFormat = secretKey.getEncoded();
key = new SecretKeySpec(enCodeFormat, "AES");
cipher = Cipher.getInstance("AES");
}
/**
* AES加密字符串
*
* @param content 加密内容
*
* @return 密文
*/
public byte[] encrypt(String content) {
try {
byte[] byteContent = content.getBytes("utf-8");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] result = cipher.doFinal(byteContent);
return result;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return null;
}
/**
* 解密AES加密过的字符串
*
* @param content 解密密文
*
* @return 明文
*/
public byte[] decrypt(byte[] content) {
try {
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] result = cipher.doFinal(content);
return result;
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) {
String content = "麻溜地撸个加密程序";
String password = "123456";
Encrypt1 e1, e2;
try {
e1 = new Encrypt1(password);
e2 = new Encrypt1(password);
System.out.println("加密之前:" + content);
// 加密
byte[] encrypt = e1.encrypt(content);
System.out.println("加密后的内容:" + new String(encrypt));
// 解密
byte[] decrypt = e2.decrypt(encrypt);
System.out.println("解密后的内容:" + new String(decrypt));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
}
}
}
最初我在Windows上的电脑测试这段代码时还很好用。但是当我将相关代码部署到Linux服务器上时,解密出现了问题,在解密时抛出异常。类似下图:
![](https://img.haomeiwen.com/i12385672/19bc4390b160c51f.png)
报错指明是由于错误的秘钥导致,但是经过详细比较,我在加密和解密时的秘钥都是采用同一个,不可能是由于使用秘钥不同导致。由于业务流程上是Windows系统上的程序充当Producer对报文进行加密然后插入Kafka消息队列,Linux上的程序作为Consumer进行消费并对之前的密文解密。第一直觉误认为在进行插入过程中byte数组产生了问题,于是Producer改进为转为16进制进行插入,在Consumer进行消费时进行检查。非常奇妙的是,消费者拿到的加密报文与生产者产生的报文完完全全相同,而且将消费者拿到的报文复制到最初测试的程序中,可以正常解密。于是可以大致断定为是环境导致的解密失败。为了确认我用相同字符串在Windows和Linux环境下用相同秘钥进行了加密,对比加密后的字符串。根据AES加密算法的原理,如果使用相同秘钥,同一个字符串加密后的密文应该是相同的。但是在上述不同操作系统之间,加密后的内容是不同的。
错误原因分析:
SecureRandom 实现随操作系统本身的內部状态不同而不同,除非调用方在调用 getInstance 方法之后又调用了 setSeed 方法;该实现在 windows 上每次生成的 key 都相同,但是在 solaris 或部分 linux 系统上则不同。
真相大白后我们进行Linux版本修正。
Linux版本的AES128实现
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
public class Encrypt2 {
private final String password;
private final KeyGenerator kgen;
private final SecretKey secretKey;
private final byte[] enCodeFormat;
private final SecretKeySpec key;
private final Cipher cipher;
private final SecureRandom secureRandom;
public Encrypt2(String password) throws NoSuchAlgorithmException, NoSuchPaddingException {
this.password = password;
kgen = KeyGenerator.getInstance("AES");
secureRandom = SecureRandom.getInstance("SHA1PRNG");
secureRandom.setSeed(password.getBytes());
kgen.init(128, secureRandom);
secretKey = kgen.generateKey();
enCodeFormat = secretKey.getEncoded();
key = new SecretKeySpec(enCodeFormat, "AES");
cipher = Cipher.getInstance("AES");
}
/**
* AES加密字符串
*
* @param content 加密内容
*
* @return 密文
*/
public byte[] encrypt(String content) {
try {
byte[] byteContent = content.getBytes("utf-8");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] result = cipher.doFinal(byteContent);
return result;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return null;
}
/**
* 解密AES加密过的字符串
*
* @param content 解密密文
*
* @return 明文
*/
public byte[] decrypt(byte[] content) {
try {
cipher.init(Cipher.DECRYPT_MODE, key);// 初始化为解密模式的密码器
byte[] result = cipher.doFinal(content);
return result; // 明文
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) {
String content = "麻溜地撸个加密程序";
String password = "123456";
Encrypt2 e1, e2;
try {
e1 = new Encrypt2(password);
e2 = new Encrypt2(password);
System.out.println("加密之前:" + content);
// 加密
byte[] encrypt = e1.encrypt(content);
System.out.println("加密后的内容:" + new String(encrypt));
// 解密
byte[] decrypt = e2.decrypt(encrypt);
System.out.println("解密后的内容:" + new String(decrypt));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
}
}
}
经过测试,此加密工具在Windows和Linux系统中均表现良好。对于多系统串行加密解密也没有问题。
多线程进行加密解密试验
简单地对报文进行加密解密是不能满足实际情况的,该方法是否线程安全是个还需要确定的事情。对多线程进行了如下测试。加密解密方法不变,测试方法如下:
public static void main(String[] args) {
String content1 = "麻溜地撸个加密程序";
String content2 = "苟利国家生死以";
String content3 = "岂因福祸避趋之";
String password = "123456";
Encrypt3 e1, e2;
try {
e1 = new Encrypt3(password);
e2 = new Encrypt3(password);
Thread thread1, thread2, thread3;
thread1 = new Thread(() -> {
int i = 0;
while (true) {
System.out.println("线程1加密之前:" + content1 + i);
// 加密
byte[] encrypt = e1.encrypt(content1 + i++);
System.out.println("线程1加密后的内容:" + new String(encrypt));
// 解密
byte[] decrypt = e1.decrypt(encrypt);
System.out.println("线程1解密后的内容:" + new String(decrypt));
}
});
thread1.start();
thread2 = new Thread(() -> {
int i = 0;
while (true) {
System.out.println("线程2加密之前:" + content2 + i);
// 加密
byte[] encrypt = e1.encrypt(content2 + i++);
System.out.println("线程2加密后的内容:" + new String(encrypt));
// 解密
byte[] decrypt = e1.decrypt(encrypt);
System.out.println("线程2解密后的内容:" + new String(decrypt));
}
});
thread2.start();
thread3 = new Thread(() -> {
int i = 0;
while (true) {
System.out.println("线程3加密之前:" + content3 + i);
// 加密
byte[] encrypt = e1.encrypt(content3 + i++);
System.out.println("线程3加密后的内容:" + new String(encrypt));
// 解密
byte[] decrypt = e1.decrypt(encrypt);
System.out.println("线程3解密后的内容:" + new String(decrypt));
}
});
thread3.start();
} catch (NoSuchAlgorithmException e) {
System.out.println(e.getMessage());
e.printStackTrace();
} catch (NoSuchPaddingException e) {
System.out.println(e.getMessage());
e.printStackTrace();
}
}
测试结果果然有幺蛾子:
![](https://img.haomeiwen.com/i12385672/0e74bcc8208109d9.png)
每次启动都会最终只有一个线程留下来进行加密解密。其他两个不知道为何就消失了。
经过排查,确定是Cipher不是线程安全的。解决方法有两种,在加密和解密方法中给Cipher加锁,或者在每次使用Cipher时新实例化一个对象。我们选择后一种方式。如果加密解密不是很频繁可以使用第一种加锁方式。但是当加密解密密度很高时,使用第一种方式会影响性能。第二种方式会增加一定的内存使用,但是得益于Java8的gc内存回收做的很好,我们不用担心由此带来的内存增加问题。所以我们用空间换时间。
线程安全加密实例
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
public class Encrypt4 {
private final String password;
private final KeyGenerator kgen;
private final SecretKey secretKey;
private final byte[] enCodeFormat;
private final SecretKeySpec key;
private final SecureRandom secureRandom;
public Encrypt4(String password) throws NoSuchAlgorithmException, NoSuchPaddingException {
this.password = password;
kgen = KeyGenerator.getInstance("AES");
secureRandom = SecureRandom.getInstance("SHA1PRNG");
secureRandom.setSeed(password.getBytes());
kgen.init(128, secureRandom);
secretKey = kgen.generateKey();
enCodeFormat = secretKey.getEncoded();
key = new SecretKeySpec(enCodeFormat, "AES");
}
/**
* AES加密字符串
*
* @param content 加密内容
*
* @return 密文
*/
public byte[] encrypt(String content) {
try {
byte[] byteContent = content.getBytes("utf-8");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] result = cipher.doFinal(byteContent);
return result;
} catch (UnsupportedEncodingException e) {
System.out.println(e.getMessage());
e.printStackTrace();
} catch (InvalidKeyException e) {
System.out.println(e.getMessage());
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
System.out.println(e.getMessage());
e.printStackTrace();
} catch (BadPaddingException e) {
System.out.println(e.getMessage());
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
/**
* 解密AES加密过的字符串
*
* @param content 解密密文
*
* @return 明文
*/
public byte[] decrypt(byte[] content) {
try {
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, key);// 初始化为解密模式的密码器
byte[] result = cipher.doFinal(content);
return result; // 明文
} catch (InvalidKeyException e) {
System.out.println(e.getMessage());
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
System.out.println(e.getMessage());
e.printStackTrace();
} catch (BadPaddingException e) {
System.out.println(e.getMessage());
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) {
String content1 = "麻溜地撸个加密程序";
String content2 = "苟利国家生死以";
String content3 = "岂因福祸避趋之";
String password = "123456";
Encrypt4 e1, e2;
try {
e1 = new Encrypt4(password);
e2 = new Encrypt4(password);
Thread thread1, thread2, thread3;
thread1 = new Thread(() -> {
int i = 0;
while (true) {
System.out.println("线程1加密之前:" + content1 + i);
// 加密
byte[] encrypt = e1.encrypt(content1 + i++);
System.out.println("线程1加密后的内容:" + new String(encrypt));
// 解密
byte[] decrypt = e1.decrypt(encrypt);
System.out.println("线程1解密后的内容:" + new String(decrypt));
}
});
thread1.start();
thread2 = new Thread(() -> {
int i = 0;
while (true) {
System.out.println("线程2加密之前:" + content2 + i);
// 加密
byte[] encrypt = e1.encrypt(content2 + i++);
System.out.println("线程2加密后的内容:" + new String(encrypt));
// 解密
byte[] decrypt = e1.decrypt(encrypt);
System.out.println("线程2解密后的内容:" + new String(decrypt));
}
});
thread2.start();
thread3 = new Thread(() -> {
int i = 0;
while (true) {
System.out.println("线程3加密之前:" + content3 + i);
// 加密
byte[] encrypt = e1.encrypt(content3 + i++);
System.out.println("线程3加密后的内容:" + new String(encrypt));
// 解密
byte[] decrypt = e1.decrypt(encrypt);
System.out.println("线程3解密后的内容:" + new String(decrypt));
}
});
thread3.start();
} catch (NoSuchAlgorithmException e) {
System.out.println(e.getMessage());
e.printStackTrace();
} catch (NoSuchPaddingException e) {
System.out.println(e.getMessage());
e.printStackTrace();
}
}
}
写在最后的话
AES加密算法非常简单也非常常见,本文只是写一个备忘笔记。特别感谢在我学习过程中对我进行无私帮助的耿腾。
网友评论