问题
java加密生成base64位字符串,前端使用Java提供的密钥,前端无法解密出数据。
java后台加密使用AES/ECB/PKCS5Padding
数据准备:
一个固定秘钥,互相约定
B7E1FF62FC0B4392B81432D44CE62F88
一个待加密字符串 ,待测的业务数据
{“SYSCODE”:“AES5500”,“SYSTIME”:“20221201105835”}
原始代码
java
public static String encrypt(String content, String key) throws Exception {
try {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
byte[] byteContent = content.getBytes("utf-8");
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(key));
byte[] result = cipher.doFinal(byteContent);
return byte2Base64(result);
} catch (Exception var5) {
return null;
}
}
public static String decrypt(String content, String key) {
try {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(key));
byte[] result = cipher.doFinal(base642Byte(content));
return new String(result, "utf-8");
} catch (Exception var4) {
return null;
}
}
/**
* 生成加密秘钥
* @param key
* @return
*/
private static SecretKeySpec getSecretKey(String key) {
try {
KeyGenerator kg = KeyGenerator.getInstance("AES");
// 指定算法名称,不同的系统上生成的key是相同的。
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
random.setSeed(key.getBytes());
//AES 要求密钥长度为 128
kg.init(128, random);
//生成一个密钥
SecretKey secretKey = kg.generateKey();
// 转换为AES专用密钥
return new SecretKeySpec(secretKey.getEncoded(), "AES");
} catch (NoSuchAlgorithmException var4) {
return null;
}
}
public static String byte2Base64(byte[] bytes) {
BASE64Encoder encoder = new BASE64Encoder();
return encoder.encode(bytes);
}
public static byte[] base642Byte(String base64Key) throws IOException {
BASE64Decoder decoder = new BASE64Decoder();
return decoder.decodeBuffer(base64Key);
}
前端代码
let obj = JSON.stringify({"SYSCODE":"AES5500","SYSTIME":"20221201105835"})
const key= "B7E1FF62FC0B4392B81432D44CE62F88"
/**
* 加密 --api
* @param word string
* @returns {*} string
*/
function encrypt(word){//word 为 string mode为加密规则
var secretKey = CryptoJS.enc.Utf8.parse(key);//key 为你的秘钥
var srcs = CryptoJS.enc.Utf8.parse(word);
var encrypted = CryptoJS.AES.encrypt(srcs, secretKey, {mode:CryptoJS.mode.ECB,padding: CryptoJS.pad.Pkcs7});
return encrypted.toString();
}
/**
* 解密 --api
* @param word string
* @returns {*} string
*/
function decrypt(word){
var secretKey = CryptoJS.enc.Utf8.parse(key);
var decrypt = CryptoJS.AES.decrypt(word, secretKey, {mode:CryptoJS.mode.ECB,padding: CryptoJS.pad.Pkcs7});
return CryptoJS.enc.Utf8.stringify(decrypt).toString();
}
网上基本上大同小异,如果按照上述代码,无法解密成功。
原因分析
在Java中,AES的实际密钥需要用到KeyGenerator 和 SecureRandom。而这个秘钥生成器只有java才有,其他语言没有,当然前端使用的CryptoJS也不知道这个是啥玩意。
问题出在getSecretKey 这个方法,使用了java特有的KeyGenerator ,对秘钥进行了指定算法SHA1PRNG再次生成一个新秘钥。导致前端使用的秘钥它不是真正的秘钥。
解决方法
1、去除KeyGenerator
第一种解决思路,既然是java特有的秘钥生成器,那我不用就好了,不要再次生成。
直接用约定的秘钥就好了。后台java把代码删减一下,前端代码不需要修改。
大部分人也是这样用的,网上有一堆声明了KeyGenerator 但是实际上却未使用的示例代码。
private static SecretKeySpec getSecretKey(String key) {
// KeyGenerator kg = KeyGenerator.getInstance("AES");
// // 指定算法名称,不同的系统上生成的key是相同的。
// SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
// random.setSeed(key.getBytes());
// //AES 要求密钥长度为 128
// kg.init(128, random);
// //生成一个密钥
// SecretKey secretKey = kg.generateKey();
// 转换为AES专用密钥
return new SecretKeySpec(key.getBytes(), "AES");
}
2、解读KeyGenerator的执行结果
大部分都是第一种解决思路,但是有时候在某种情况下总是因为某些原因无法去修改这个加解密方式,只能去适应他。
KeyGenerator生成的秘钥最终也是转换成了byte数组。那就直接将最终转换的byte数组打印生成Hex字符串出来,看看长什么样子。
SecretKey secretKey = kg.generateKey();
//将最终的秘钥以16进制字符串打印到控制台
toHexString(secretKey.getEncoded());
打印结果:fb39ab6cea8300f6a9e115a46d3bb78a
前端改造
知道java对秘钥进行了什么操作之后,前端复刻java的操作就行。
- 前端手上是一个原始秘钥,需要使用SHA1PRNG指定算法进行解析
let hexStr=CryptoJS.SHA1(CryptoJS.SHA1('B7E1FF62FC0B4392B81432D44CE62F88')).toString().substring(0, 32);
此时得到了最终的hex字符串,跟java中秘钥byte数组转成Hex字符串一致。
此时还不能直接用 CryptoJS.AES.decrypt 进行解密,直接解密会失败。
- hex字符串转成Hex
还需要进行特定的数据类型转换,此时还是字符串,解密时需要真实的HEX
secretKey = CryptoJS.enc.Hex.parse(hexStr)
此时的secretKey 才是最终的秘钥,CryptoJS才能正确识别
/**
* 解密 --api
* @param word string
* @returns {*} string
*/
function decrypt(word){
var secretKey = CryptoJS.enc.Utf8.parse(key);
var hexStr=CryptoJS.SHA1(CryptoJS.SHA1(key)).toString().substring(0, 32);
secretKey = CryptoJS.enc.Hex.parse(hexStr);
var decrypt = CryptoJS.AES.decrypt(word, secretKey, {mode:CryptoJS.mode.ECB,padding: CryptoJS.pad.Pkcs7});
return CryptoJS.enc.Utf8.stringify(decrypt).toString();
}
完整的前端代码
let base = 'B7E1FF62FC0B4392B81432D44CE62F88';
function decrypt(word){ //word 为base64字符串
var secretKey = CryptoJS.enc.Utf8.parse(base);
//==================java使用了KeyGenerator SHA1PRNG 算法 就加这2行代码,没加就去掉=======
var hexStr=CryptoJS.SHA1(CryptoJS.SHA1(base)).toString().substring(0, 32);
secretKey = CryptoJS.enc.Hex.parse(hexStr);
//======================================================================
var decrypt = CryptoJS.AES.decrypt(word, secretKey, {mode:CryptoJS.mode.ECB,padding: CryptoJS.pad.Pkcs7});
return CryptoJS.enc.Utf8.stringify(decrypt).toString();
}
function encrypt(word){//word 为 string
var secretKey = CryptoJS.enc.Utf8.parse(base);//key 为你的秘钥
//==================java使用了KeyGenerator SHA1PRNG 算法 就加这2行代码,没加就去掉=======
var hexStr=CryptoJS.SHA1(CryptoJS.SHA1(base)).toString().substring(0, 32);
secretKey = CryptoJS.enc.Hex.parse(hexStr);
//======================================================================
var srcs = CryptoJS.enc.Utf8.parse(word);
var encrypted = CryptoJS.AES.encrypt(srcs, secretKey, {mode:CryptoJS.mode.ECB,padding: CryptoJS.pad.Pkcs7});
return encrypted.toString();
}
简单页面测试打印一下,解密正常。拿前端的加密串在java后台解密也正常
拓展
其他的语言,像C#之类的也是按照类似的思路进行加解密即可。可以使用HEX数组进行加解密
遇到的问题
1、CryptoJS.SHA1进行2次,为什么进行2次?
后续研究…
2、java加密给出的密文是base64加密之后的,密文中携带换行字符,导致解密失败。
Base64编码包有很多,使用不同标准的Base64编码包,会导致密文有所区别。
根据RFC822规定,BASE64Encoder编码每76个字符,还需要加上一个回车换行
部分Base64编码的java库还按照这个标准实行
sun.mis包:encode有换行符
Java 8的java.util包:encode无换行符
Apache Commons Codec:encode无换行符
后台使用的sun.mis包里面base64,导致密文存在换行。
从java后台获取到密文数据时,要进行\r\n的字符处理,将其换行符替换为空拼接在一起。直接解密会读取到\r\n导致解密失败。
建议使用Java 8的java.util包
//编码
String encodeToString = Base64.getEncoder().encodeToString(message.getBytes());
//解码
byte[] decode = Base64.getDecoder().decode(encodeToString);