@coldxiangyu
2017-06-27T12:26:30.000000Z
字数 4497
阅读 3676
javatec
今天,领导给我发微信,目前云核心客户信息云端存储需要进行AES加解密,因为客户信息都是存到数据库中的,加密之后的字段长度需要跟加密前保持一致,避免扩充字段。
之前搞过MD5、DES加解密,AES还真没搞过,于是我先把手头的工作放一放,研究了几个小时的AES。
密码学中的高级加密标准(Advanced Encryption Standard,AES),又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。
关于AES加密的原理就很复杂了,毕竟密码学也是一门独立的学问,源码看的一脸懵逼。不过,这些都不是什么问题,毕竟我们用的是java,AES算法也早已写入JDK了,也就是JCE。
java AES一共有五种模式:
ECB
(电子密码本 (Electronic Code Book))
CBC
(密码块链接 (Cipher Block Chaining))
CFB
(密码反馈方式 (Cipher Feedback Mode))
OFB
(输出反馈方式 (Output Feedback Mode))
PCBC
(填充密码块链接 (Propagating Cipher Block Chaining))
不支持“NONE”模式。
此外,还支持三种填充:NoPadding,PKCS5Padding,ISO10126Padding,不支持SSL3Padding
默认使用ECB/PKCS5Padding
。
我们如何选择这些模式呢,我们来看一下加解密前后长度对比:
算法/模式/填充 16字节加密后数据长度 不满16字节加密后长度
AES/CBC/NoPadding 16 不支持
AES/CBC/PKCS5Padding 32 16
AES/CBC/ISO10126Padding 32 16
AES/CFB/NoPadding 16 原始数据长度
AES/CFB/PKCS5Padding 32 16
AES/CFB/ISO10126Padding 32 16
AES/ECB/NoPadding 16 不支持
AES/ECB/PKCS5Padding 32 16
AES/ECB/ISO10126Padding 32 16
AES/OFB/NoPadding 16 原始数据长度
AES/OFB/PKCS5Padding 32 16
AES/OFB/ISO10126Padding 32 16
AES/PCBC/NoPadding 16 不支持
AES/PCBC/PKCS5Padding 32 16
AES/PCBC/ISO10126Padding 32 16
这时候就比较明显了,我们要实现AES加解密前后长度一致,只有CFB、OFB两种模式。
package com.lxy.coder;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* Created by coldxiangyu on 2017/6/26.
*/
public class AESTest {
public static byte[] encrypt(String content, String key) {
try {
Cipher aesECB = Cipher.getInstance("AES/CFB/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
//SecureRandom r = new SecureRandom();
//byte[] ivBytes = new byte[16];
//r.nextBytes(ivBytes);
IvParameterSpec ivSpec = new IvParameterSpec(key.getBytes());
//IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
aesECB.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
byte[] result = aesECB.doFinal(content.getBytes());
return result;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static byte[] decrypt(String key, byte[] ciphertext) throws Exception{
Cipher AESCipher = Cipher.getInstance("AES/CFB/NoPadding");
IvParameterSpec IVSpec = new IvParameterSpec(key.getBytes());
SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
AESCipher.init(Cipher.DECRYPT_MODE, keySpec, IVSpec);
byte[] plaintext = AESCipher.doFinal(ciphertext);
return plaintext;
}
public static void main(String[] args) throws Exception {
String data = "coldxiangyu";
String data2 = "123456789";
System.out.println("加密前数据:" + data + ",加密前长度:" + data.length());
System.out.println("加密前数据:" + data2 + ",加密前长度:" + data2.length());
String key = "1234567890123456";
byte[] enresult = encrypt(data, key);
byte[] enresult2 = encrypt(data2, key);
String enresultStr = new String(enresult, "ISO_8859_1");
String enresultStr2 = new String(enresult2, "ISO_8859_1");
System.out.println("加密后数据:" + enresultStr + ",加密后长度:" + enresultStr.length());
System.out.println("加密后数据:" + enresultStr2 + ",加密后长度:" + enresultStr2.length());
String deresult = new String(decrypt(key, enresult), "ISO_8859_1");
String deresult2 = new String(decrypt(key, enresult2), "ISO_8859_1");
System.out.println("解密后数据:" + deresult);
System.out.println("解密后数据:" + deresult2);
}
}
可以看到,加解密的过程还是非常简单的。
运行结果如下:
加密前后数据长度保持一致。
然而在我们实际的应用场景中,客户信息肯定不能以byte数组的形式存储在字段中,我们需要对它进行转换为String之后再存储到数据库。这样的话我们在解密的时候,对象就不是byte数组了,而是一个string。
String data = "弖虒_000";
byte[] enresult = encrypt(data, key);
String enresultStr = new String(enresult, "ISO_8859_1");
String deresult = new String(decrypt(key, enresultStr.getBytes()), "ISO-8859-1");
同样的代码,我们通过将加密之后的string转换为byte数组再解码,再转换为String,打印结果如下:
这时候我们看到,解密后的数据并不是我们的原数据了。
这是因为加密后的byte数组是不能强制转换成字符串的,换言之:字符串和byte数组在这种情况下不是互逆的。
这时候应该怎么办呢,网上有人提出方案,将byte数组直接转成十六进制存储,取出的十六进制转换为byte数组然后解密。但是这样我们就不能保证加解密前后长度不变了。肯定还有其他的办法。
我在stackoverflow上找到了答案:https://stackoverflow.com/questions/24066679/java-aes-string-decrypting-given-final-block-not-properly-padded
When you use byte[] packet2 = packet.getBytes() you are converting the string based on the default encoding, which could be UTF-8, for example. That's fine. But then you convert the ciphertext back to a string like this: return packet = new String(encrypted) and this can get you into trouble if this does not round-trip to the same byte array later in decrypt() with another byte[] packet2 = packet.getBytes().
Try this instead: return packet = new String(encrypted, "ISO-8859-1"), and byte[] packet2 = packet.getBytes("ISO-8859-1") -- it's not what I would prefer, but it should round-trip the byte arrays.
我们可以通过ISO-8859-1
的编码方式来实现String与byte转换的统一标准。
如下:
String data = "弖虒_000";
byte[] enresult = encrypt(data, key);
String enresultStr = new String(enresult, "ISO_8859_1");
String deresult = new String(decrypt(key, enresultStr.getBytes("ISO-8859-1")), "UTF-8");
运行结果如下:
我们可以看到,可以成功解密了。
这样还不够,接下来,我们在mysql中模拟一下实际场景:
首先创建test表,进行密文的存储,保证数据库为UTF-8编码。
基础的JDBC代码就不再贴了,运行结果与预想的一致:
至此,AES实现字段加密前后长度不变,数据库存储敏感客户信息的需求,就实现了。