我们的游戏充值平台马上要到货一批充值码,需要入库。之前充值码发奖相关的需求都是我做的,但在存储充值码的时候没有加密,是明文存储的。
现在的需求是,数据库中的充值码需要密文存储。这就涉及到:
这是我第一次做加密相关的需求。一开始(几周前吧)图省事,想着在 StackOverflow 上搜一下,一两行代码就搞定了,不就是个加密嘛。后来发现,怎么都这么复杂啊,一直搜到我身心俱疲,也没找到简单的方法。
这回我静下心来,好好读了一篇介绍 AES 的文章,终于大体上搞明白了。
参考链接:Java AES Encryption and Decryption
简单来说就是,AES 分为很多模式,但大家基本上都用 CBC。
在 CBC 模式下,除了秘钥 key
之外,为了增强安全性,还需要一个 iv
。(最基础的 ECB 模式不需要 iv
,只需要 key
,但该模式不提倡使用)。
key
有 128、192、256 位三种选择,iv
固定是 128 位,因为加密块固定是 128 位,需要加密的信息需要先分成 128 位大小的块,如果最后一块不足 128 位需要填充到 128 位(padding)。实际上并不需要用户自己填充,指定参数就行。
生成一个新的 key
(默认 128 位):
public static SecretKey generateKey(int n) throws NoSuchAlgorithmException {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(n);
SecretKey key = keyGenerator.generateKey();
return key;
}
生成一个新的 iv
:
public static IvParameterSpec generateIv() {
byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv);
return new IvParameterSpec(iv);
}
当然,生成之后最终是要用字符串的格式保存和传送 key
和 iv
的。下面使用 base64
格式保存:
// 使用上面的方法生成 key 并转换为 base64 格式
SecretKey key = EncryptUtils.generateKey(128);
String keyBase64 = Base64.getEncoder().encodeToString(key.getEncoded());
// 使用上面的方法生成 iv 并转换为 base64 格式
IvParameterSpec ivParameterSpec = EncryptUtils.generateIv();
String ivBase64 = Base64.getEncoder().encodeToString(ivParameterSpec.getIV());
首先,可以把 base64
格式的 key
和 iv
转换回 Java 中的类型(参考链接:Converting Secret Key into a String and Vice Versa)
代码如下:
// 导入 key
byte[] keyBytes = Base64.getDecoder().decode(keyBase64);
SecretKey key = new SecretKeySpec(keyBytes, 0, keyBytes.length, "AES");
// 导入 iv
byte[] ivBytes = Base64.getDecoder().decode(ivBase64);
IvParameterSpec iv = new IvParameterSpec(ivBytes);
之后就是用 key
和 iv
来进行加密和解密了:
String algorithm = "AES/CBC/PKCS5Padding";
// 加密,input 是要加密的明文,返回的是一个 base64 格式的密文:
public static String encrypt(String algorithm, String input, SecretKey key,
IvParameterSpec iv) throws NoSuchPaddingException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(algorithm);
cipher.init(Cipher.ENCRYPT_MODE, key, iv);
byte[] cipherText = cipher.doFinal(input.getBytes());
return Base64.getEncoder()
.encodeToString(cipherText);
}
// 解密,cipherText 是 base64格式的密文
public static String decrypt(String algorithm, String cipherText, SecretKey key,
IvParameterSpec iv) throws NoSuchPaddingException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(algorithm);
cipher.init(Cipher.DECRYPT_MODE, key, iv);
byte[] plainText = cipher.doFinal(Base64.getDecoder()
.decode(cipherText));
return new String(plainText);
}
当然,实际使用时还是会做一些封装什么的,比如我把 cipher 单独存起来了。但大体就是这样了。
JS 这边使用 CryptoJS
这个库来解密。
我没找到介绍 JS AES 加解密的特别好的文章,CryptoJS
的文档我感觉写的也不是很好。后来一路磕磕绊绊,看了好多个 StackOverflow 和 JSFiddle 之类的,花了得有俩小时,终于找到了解密方法:
参考链接:AES encryption using Java and decryption using Javascript
// 从 base64 格式导入 key 和 iv
var key = CryptoJS.enc.Base64.parse('nlCdv7/wqRIsf1iWzqz96Q==');
var iv = CryptoJS.enc.Base64.parse('n9CvQB/1quXtItsdhnel2g==');
function decrypt(encrypted) {
var cipherParams = CryptoJS.lib.CipherParams.create({
// 从 base64 格式导入密文
ciphertext: CryptoJS.enc.Base64.parse(encrypted)
});
// 解密
return CryptoJS.AES.decrypt(cipherParams, key, {
iv: iv,
padding: CryptoJS.pad.Pkcs7,
mode: CryptoJS.mode.CBC
}).toString(CryptoJS.enc.Utf8);
}
用 Java 进行 AES CBC 加密/解密还是比较简单的,之前只因我太急躁,错误地留下了“这件事很难”的印象。
先把加密的基本原理和流程搞清楚,再做就好多了。