最近由于業(yè)務(wù)需求郎嫁,需要給Kafka內(nèi)的報(bào)文進(jìn)行加密。Kafka的上游與下游都是我們自己的系統(tǒng)统台,分析過業(yè)務(wù)場(chǎng)景后奄容,決定使用對(duì)稱加密算法冰更。
對(duì)稱加密算法
對(duì)稱加密(也叫私鑰加密)指加密和解密使用相同密鑰的加密算法。在大多數(shù)的對(duì)稱算法中嫩海,加密密鑰和解密密鑰是相同的冬殃,所以也稱這種加密算法為秘密密鑰算法或單密鑰算法。
優(yōu)點(diǎn):對(duì)稱加密算法的特點(diǎn)是算法公開叁怪、計(jì)算量小审葬、加密速度快、加密效率高奕谭。
缺點(diǎn):交易雙方都使用同樣鑰匙涣觉,安全性得不到保證。每對(duì)用戶每次使用對(duì)稱加密算法時(shí)血柳,都需要使用其他人不知道的惟一鑰匙官册,這會(huì)使得發(fā)收信雙方所擁有的鑰匙數(shù)量呈幾何級(jí)數(shù)增長,密鑰管理成為用戶的負(fù)擔(dān)难捌。對(duì)稱加密算法在分布式網(wǎng)絡(luò)系統(tǒng)上使用較為困難膝宁,主要是因?yàn)槊荑€管理困難鸦难,使用成本較高。
非對(duì)稱加密算法
非對(duì)稱加密算法需要兩個(gè)密鑰:公開密鑰(publickey:簡稱公鑰)和私有密鑰(privatekey:簡稱私鑰)员淫。公鑰與私鑰是一對(duì)合蔽,如果用公鑰對(duì)數(shù)據(jù)進(jìn)行加密,只有用對(duì)應(yīng)的私鑰才能解密介返。
優(yōu)點(diǎn):算法強(qiáng)度復(fù)雜拴事、安全性強(qiáng)。相比于對(duì)稱秘鑰只有一個(gè)秘鑰而言圣蝎,非對(duì)稱密鑰體制有兩種密鑰刃宵,其中一個(gè)是公開的,這樣就可以不需要像對(duì)稱密碼那樣傳輸對(duì)方的密鑰了徘公。這樣安全性就大了很多牲证。
缺點(diǎn):但是由于其算法復(fù)雜,而使得加密解密速度沒有對(duì)稱加密解密的速度快步淹。
什么是AES从隆?
AES算是比較基礎(chǔ)的對(duì)稱加密算法诚撵,原理簡單缭裆。
高級(jí)加密標(biāo)準(zhǔn)(AES,Advanced Encryption Standard)為最常見的對(duì)稱加密算法,AES最常見的有3種方案寿烟,分別是AES-128澈驼、AES-192和AES-256,它們的區(qū)別在于密鑰長度不同筛武,AES-128的密鑰長度為16bytes(128bit/8)缝其,后兩者分別為24bytes和32bytes。密鑰越長徘六,安全強(qiáng)度越高内边,但伴隨運(yùn)算輪數(shù)的增加,帶來的運(yùn)算開銷就會(huì)更大待锈。
AES算法在加密過程中分為四步:
- 字節(jié)代換
- 行移位
- 列混合
- 輪密鑰加
字節(jié)代換
AES的字節(jié)代換其實(shí)就是一個(gè)簡單的查表操作漠其。AES定義了一個(gè)S盒和一個(gè)逆S盒。
行移位
行移位是一個(gè)簡單的左循環(huán)移位操作竿音。當(dāng)密鑰長度為128比特時(shí)和屎,狀態(tài)矩陣的第0行左移0字節(jié),第1行左移1字節(jié)春瞬,第2行左移2字節(jié)柴信,第3行左移3字節(jié)。
列混合
列混合變換是通過矩陣相乘來實(shí)現(xiàn)的宽气,經(jīng)行移位后的狀態(tài)矩陣與固定的矩陣相乘随常,得到混淆后的狀態(tài)矩陣潜沦。
輪密鑰加
輪密鑰加是將128位輪密鑰同狀態(tài)矩陣中的數(shù)據(jù)進(jìn)行逐位異或操作。
AES128具體實(shí)現(xiàn)
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 加密內(nèi)容
*
* @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 = "麻溜地?cái)]個(gè)加密程序";
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("加密后的內(nèi)容:" + new String(encrypt));
// 解密
byte[] decrypt = e2.decrypt(encrypt);
System.out.println("解密后的內(nèi)容:" + new String(decrypt));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
}
}
}
最初我在Windows上的電腦測(cè)試這段代碼時(shí)還很好用绪氛。但是當(dāng)我將相關(guān)代碼部署到Linux服務(wù)器上時(shí)止潮,解密出現(xiàn)了問題,在解密時(shí)拋出異常钞楼。類似下圖:
報(bào)錯(cuò)指明是由于錯(cuò)誤的秘鑰導(dǎo)致喇闸,但是經(jīng)過詳細(xì)比較,我在加密和解密時(shí)的秘鑰都是采用同一個(gè)询件,不可能是由于使用秘鑰不同導(dǎo)致燃乍。由于業(yè)務(wù)流程上是Windows系統(tǒng)上的程序充當(dāng)Producer對(duì)報(bào)文進(jìn)行加密然后插入Kafka消息隊(duì)列,Linux上的程序作為Consumer進(jìn)行消費(fèi)并對(duì)之前的密文解密宛琅。第一直覺誤認(rèn)為在進(jìn)行插入過程中byte數(shù)組產(chǎn)生了問題刻蟹,于是Producer改進(jìn)為轉(zhuǎn)為16進(jìn)制進(jìn)行插入,在Consumer進(jìn)行消費(fèi)時(shí)進(jìn)行檢查嘿辟。非常奇妙的是舆瘪,消費(fèi)者拿到的加密報(bào)文與生產(chǎn)者產(chǎn)生的報(bào)文完完全全相同,而且將消費(fèi)者拿到的報(bào)文復(fù)制到最初測(cè)試的程序中红伦,可以正常解密英古。于是可以大致斷定為是環(huán)境導(dǎo)致的解密失敗。為了確認(rèn)我用相同字符串在Windows和Linux環(huán)境下用相同秘鑰進(jìn)行了加密昙读,對(duì)比加密后的字符串召调。根據(jù)AES加密算法的原理,如果使用相同秘鑰蛮浑,同一個(gè)字符串加密后的密文應(yīng)該是相同的唠叛。但是在上述不同操作系統(tǒng)之間,加密后的內(nèi)容是不同的沮稚。
錯(cuò)誤原因分析:
SecureRandom 實(shí)現(xiàn)隨操作系統(tǒng)本身的內(nèi)部狀態(tài)不同而不同艺沼,除非調(diào)用方在調(diào)用 getInstance 方法之后又調(diào)用了 setSeed 方法;該實(shí)現(xiàn)在 windows 上每次生成的 key 都相同蕴掏,但是在 solaris 或部分 linux 系統(tǒng)上則不同障般。
真相大白后我們進(jìn)行Linux版本修正。
Linux版本的AES128實(shí)現(xiàn)
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 加密內(nèi)容
*
* @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 = "麻溜地?cái)]個(gè)加密程序";
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("加密后的內(nèi)容:" + new String(encrypt));
// 解密
byte[] decrypt = e2.decrypt(encrypt);
System.out.println("解密后的內(nèi)容:" + new String(decrypt));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
}
}
}
經(jīng)過測(cè)試囚似,此加密工具在Windows和Linux系統(tǒng)中均表現(xiàn)良好剩拢。對(duì)于多系統(tǒng)串行加密解密也沒有問題。
多線程進(jìn)行加密解密試驗(yàn)
簡單地對(duì)報(bào)文進(jìn)行加密解密是不能滿足實(shí)際情況的饶唤,該方法是否線程安全是個(gè)還需要確定的事情徐伐。對(duì)多線程進(jìn)行了如下測(cè)試。加密解密方法不變募狂,測(cè)試方法如下:
public static void main(String[] args) {
String content1 = "麻溜地?cái)]個(gè)加密程序";
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加密后的內(nèi)容:" + new String(encrypt));
// 解密
byte[] decrypt = e1.decrypt(encrypt);
System.out.println("線程1解密后的內(nèi)容:" + 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加密后的內(nèi)容:" + new String(encrypt));
// 解密
byte[] decrypt = e1.decrypt(encrypt);
System.out.println("線程2解密后的內(nèi)容:" + 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加密后的內(nèi)容:" + new String(encrypt));
// 解密
byte[] decrypt = e1.decrypt(encrypt);
System.out.println("線程3解密后的內(nèi)容:" + 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();
}
}
測(cè)試結(jié)果果然有幺蛾子:
每次啟動(dòng)都會(huì)最終只有一個(gè)線程留下來進(jìn)行加密解密办素。其他兩個(gè)不知道為何就消失了角雷。
經(jīng)過排查,確定是Cipher不是線程安全的性穿。解決方法有兩種勺三,在加密和解密方法中給Cipher加鎖,或者在每次使用Cipher時(shí)新實(shí)例化一個(gè)對(duì)象需曾。我們選擇后一種方式吗坚。如果加密解密不是很頻繁可以使用第一種加鎖方式。但是當(dāng)加密解密密度很高時(shí)呆万,使用第一種方式會(huì)影響性能商源。第二種方式會(huì)增加一定的內(nèi)存使用,但是得益于Java8的gc內(nèi)存回收做的很好谋减,我們不用擔(dān)心由此帶來的內(nèi)存增加問題牡彻。所以我們用空間換時(shí)間。
線程安全加密實(shí)例
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 加密內(nèi)容
*
* @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 = "麻溜地?cái)]個(gè)加密程序";
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加密后的內(nèi)容:" + new String(encrypt));
// 解密
byte[] decrypt = e1.decrypt(encrypt);
System.out.println("線程1解密后的內(nèi)容:" + 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加密后的內(nèi)容:" + new String(encrypt));
// 解密
byte[] decrypt = e1.decrypt(encrypt);
System.out.println("線程2解密后的內(nèi)容:" + 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加密后的內(nèi)容:" + new String(encrypt));
// 解密
byte[] decrypt = e1.decrypt(encrypt);
System.out.println("線程3解密后的內(nèi)容:" + 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加密算法非常簡單也非常常見出爹,本文只是寫一個(gè)備忘筆記庄吼。特別感謝在我學(xué)習(xí)過程中對(duì)我進(jìn)行無私幫助的耿騰。