[关闭]
@coldxiangyu 2017-06-27T12:26:30.000000Z 字数 4497 阅读 3676

AES加密算法

javatec


今天,领导给我发微信,目前云核心客户信息云端存储需要进行AES加解密,因为客户信息都是存到数据库中的,加密之后的字段长度需要跟加密前保持一致,避免扩充字段。
之前搞过MD5、DES加解密,AES还真没搞过,于是我先把手头的工作放一放,研究了几个小时的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

我们如何选择这些模式呢,我们来看一下加解密前后长度对比:

  1. 算法/模式/填充 16字节加密后数据长度 不满16字节加密后长度
  2. AES/CBC/NoPadding 16 不支持
  3. AES/CBC/PKCS5Padding 32 16
  4. AES/CBC/ISO10126Padding 32 16
  5. AES/CFB/NoPadding 16 原始数据长度
  6. AES/CFB/PKCS5Padding 32 16
  7. AES/CFB/ISO10126Padding 32 16
  8. AES/ECB/NoPadding 16 不支持
  9. AES/ECB/PKCS5Padding 32 16
  10. AES/ECB/ISO10126Padding 32 16
  11. AES/OFB/NoPadding 16 原始数据长度
  12. AES/OFB/PKCS5Padding 32 16
  13. AES/OFB/ISO10126Padding 32 16
  14. AES/PCBC/NoPadding 16 不支持
  15. AES/PCBC/PKCS5Padding 32 16
  16. AES/PCBC/ISO10126Padding 32 16

这时候就比较明显了,我们要实现AES加解密前后长度一致,只有CFB、OFB两种模式。

实战

  1. package com.lxy.coder;
  2. import javax.crypto.Cipher;
  3. import javax.crypto.spec.IvParameterSpec;
  4. import javax.crypto.spec.SecretKeySpec;
  5. /**
  6. * Created by coldxiangyu on 2017/6/26.
  7. */
  8. public class AESTest {
  9. public static byte[] encrypt(String content, String key) {
  10. try {
  11. Cipher aesECB = Cipher.getInstance("AES/CFB/NoPadding");
  12. SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
  13. //SecureRandom r = new SecureRandom();
  14. //byte[] ivBytes = new byte[16];
  15. //r.nextBytes(ivBytes);
  16. IvParameterSpec ivSpec = new IvParameterSpec(key.getBytes());
  17. //IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
  18. aesECB.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
  19. byte[] result = aesECB.doFinal(content.getBytes());
  20. return result;
  21. } catch (Exception e) {
  22. e.printStackTrace();
  23. }
  24. return null;
  25. }
  26. public static byte[] decrypt(String key, byte[] ciphertext) throws Exception{
  27. Cipher AESCipher = Cipher.getInstance("AES/CFB/NoPadding");
  28. IvParameterSpec IVSpec = new IvParameterSpec(key.getBytes());
  29. SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
  30. AESCipher.init(Cipher.DECRYPT_MODE, keySpec, IVSpec);
  31. byte[] plaintext = AESCipher.doFinal(ciphertext);
  32. return plaintext;
  33. }
  34. public static void main(String[] args) throws Exception {
  35. String data = "coldxiangyu";
  36. String data2 = "123456789";
  37. System.out.println("加密前数据:" + data + ",加密前长度:" + data.length());
  38. System.out.println("加密前数据:" + data2 + ",加密前长度:" + data2.length());
  39. String key = "1234567890123456";
  40. byte[] enresult = encrypt(data, key);
  41. byte[] enresult2 = encrypt(data2, key);
  42. String enresultStr = new String(enresult, "ISO_8859_1");
  43. String enresultStr2 = new String(enresult2, "ISO_8859_1");
  44. System.out.println("加密后数据:" + enresultStr + ",加密后长度:" + enresultStr.length());
  45. System.out.println("加密后数据:" + enresultStr2 + ",加密后长度:" + enresultStr2.length());
  46. String deresult = new String(decrypt(key, enresult), "ISO_8859_1");
  47. String deresult2 = new String(decrypt(key, enresult2), "ISO_8859_1");
  48. System.out.println("解密后数据:" + deresult);
  49. System.out.println("解密后数据:" + deresult2);
  50. }
  51. }

可以看到,加解密的过程还是非常简单的。
运行结果如下:
image_1bjhr4m211lq65lveqci79o6s9.png-20.6kB

加密前后数据长度保持一致。

然而在我们实际的应用场景中,客户信息肯定不能以byte数组的形式存储在字段中,我们需要对它进行转换为String之后再存储到数据库。这样的话我们在解密的时候,对象就不是byte数组了,而是一个string。

  1. String data = "弖虒_000";
  2. byte[] enresult = encrypt(data, key);
  3. String enresultStr = new String(enresult, "ISO_8859_1");
  4. String deresult = new String(decrypt(key, enresultStr.getBytes()), "ISO-8859-1");

同样的代码,我们通过将加密之后的string转换为byte数组再解码,再转换为String,打印结果如下:
image_1bjjpia1i10g7hm61c6q1b94rgv9.png-18.2kB
这时候我们看到,解密后的数据并不是我们的原数据了。
这是因为加密后的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转换的统一标准。
如下:

  1. String data = "弖虒_000";
  2. byte[] enresult = encrypt(data, key);
  3. String enresultStr = new String(enresult, "ISO_8859_1");
  4. String deresult = new String(decrypt(key, enresultStr.getBytes("ISO-8859-1")), "UTF-8");

运行结果如下:
image_1bjjscp69c9ov9u64snbso3qm.png-14.9kB
我们可以看到,可以成功解密了。
这样还不够,接下来,我们在mysql中模拟一下实际场景:
首先创建test表,进行密文的存储,保证数据库为UTF-8编码。
基础的JDBC代码就不再贴了,运行结果与预想的一致:
image_1bjjsl9ct1v461ap81g58bdk42p13.png-11.4kB

至此,AES实现字段加密前后长度不变,数据库存储敏感客户信息的需求,就实现了。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注