冷湘宇 Coldxiangyu's blog

AES加密算法

2017-06-27
coldxiangyu
   

今天,领导给我发微信,目前云核心客户信息云端存储需要进行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

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

    算法/模式/填充                   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);
    }
}

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

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

然而在我们实际的应用场景中,客户信息肯定不能以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,打印结果如下: 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转换的统一标准。 如下:

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");

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

image_1bjjsl9ct1v461ap81g58bdk42p13.png-11.4kB

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


Comments

Content