java 实现AES加密、解密

  • Post author:
  • Post category:java


一、什么是AES?

高级加密标准(英语:Advanced Encryption Standard,缩写:AES),是一种区块加密标准。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。

那么为什么原来的DES会被取代呢,,原因就在于其使用56位密钥,比较容易被破解。而AES可以使用128、192、和256位密钥,并且用128位分组加密和解密数据,相对来说安全很多。完善的加密算法在理论上是无法破解的,除非使用穷尽法。使用穷尽法破解密钥长度在128位以上的加密数据是不现实的,仅存在理论上的可能性。统计显示,即使使用目前世界上运算速度最快的计算机,穷尽128位密钥也要花上几十亿年的时间,更不用说去破解采用256位密钥长度的AES算法了。

目前世界上还有组织在研究如何攻破AES这堵坚厚的墙,但是因为破解时间太长,AES得到保障,但是所用的时间不断缩小。随着计算机计算速度的增快,新算法的出现,AES遭到的攻击只会越来越猛烈,不会停止的。

AES现在广泛用于金融财务、在线交易、无线通信、数字存储等领域,经受了最严格的考验,但说不定哪天就会三步DES的后尘。

二、AES加密方式简析

AES加密是对称加密 128 192 256 分别表示密钥的长度

* AES的加密方式会将明文拆分成不同的块进行加密,例如一个256 位的数据用128的密钥加密,则分成

明文1(128位)    明文2(128位)

加密

密文1(128位)    密文2(128位)

填充:

如果明文不是128位(16字节)的则需要填充,即在明文某个地方补充到16个字节整数倍的长度,加解密时需要采用同样的填充方式,否则无法解密成功,以下是几种填充方式

**

NoPadding


不进行填充,但是这里要求明文必须要是16个字节的整数倍,这个可以使用者本身自己去实现填充,除了该种模式以外的其他填充模式,如果已经是16个字节的数据的话,会再填充一个16字节的数据

**

PKCS5Padding(默认)


在明文的末尾进行填充,填充的数据是当前和16个字节相差的数量,例如:

未填充明文

1,2,3,4,5,6,7,8,9,10,11

填充明文(缺少五个满足16个字节)

1,2,3,4,5,6,7,8,9,10,11,5,5,5,5,5


由于使用PKCS7Padding/PKCS5Padding填充时,最后一个字节肯定为填充数据的长度,所以在解密后可以准确删除填充的数据

**

ISO10126Padding


在明文的末尾进行填充,当前和16个字节相差的数量填写在最后,其余字节填充随机数,例如:

未填充明文

1,2,3,4,5,6,7,8,9,10,11

填充明文(缺少五个满足16个字节)

1,2,3,4,5,6,7,8,9,10,11,c,b,4,1,5

模式

模式是需要制定AES对明文进行加密时使用的模式(这里并不涉及具体的加密方法,只是加密步骤上的不同模式,在加解密时同样需要相同的模式,否则无法成功),一共提供了五种模式,模式的基本原理是近似的,但是细节上会有一些变化,如下:

**

ECB模式(默认)

电码本模式 Electronic Codebook Book

这个模式是默认的,就只是根据密钥的位数,将数据分成不同的块进行加密,加密完成后,再将加密后的数据拼接起来,过程如下:

明文(64字节) 密钥(16字节)

明文1(16字节)  明文2(16字节)  明文3(16字节)  明文4(16字节)

密文1(16字节)  密文2(16字节)  密文3(16字节)  密文4(16字节)

密文(64字节)

优点:简单、速度快、可并行

缺点:如果明文块相同,则生成的密文块也相同,这样会导致安全性降低


**

CBC模式

密码分组链接模式 Cipher Block Chaining

为了解决ECB模式的密文块相同的缺点,CBC的模式引入了一个初始向量概念,该向量必须是一个与密钥长度相等的数据,在第一次加密前,会使用初始化向量与第一块数据做异或运算,生成的新数据再进行加密,加密第二块之前,会拿第一块的密文数据与第二块明文进行异或运算后再进行加密,以此类推,解密时也是在解密后,进行异或运算,生成最终的明文。过程如下:

明文(63字节) 密钥 (16字节) 初始向量iv(16字节)

明文1(16字节) 明文2(16字节) 明文3(16字节) 明文4+一个0(16字节)

异或  +初始向量        +密文1         +密文2         +密文3

密文1(16字节) 密文2(16字节) 密文3(16字节) 密文4(16字节)

密文(64字节)

这里需要注意如下几点:

1.向量必须是一个与密钥长度相等的数据

2.由于在加密前和解密后都会做异或运算,因此我们的明文可以不用补全,不是16个字节的倍数也可以,CBC中会自动用0补全进行异或运算

3.在解密时是解密后才会再做异或运算,保证数据解密成功

4.由于自动进行了补全,所以解密出的数据也会在后面补全0,因此获取到数据时,需要将末尾的0去除,或者根据源数据长度来截取解密后的数据

优点:每次加密密钥不同,加强了安全性

CBC的方式解决了EBC的缺点,但是也有其缺点:

1.加密无法并行运算,但是解密可以并行,必须在前一个块加密完成后,才能加密后块,并且也需要填充0在后面,所以并不适合流数据(不适合的原因可能是,需要满足128位的数据之后才能进行加密,这样后面才不会有0的补全)

2.如果前一个数据加密错误,那么后续的数据都是错的了

3.两端需要同时约定初始向量iv


** CFB模式: 密码反馈模式 Cipher FeedBack

这个模式只使用了加密方法,原理是用到了一个数值异或运算之后再进行一次异或运算,值不改变的原理。并且在加密的时候,如果数据并不满足一个密钥的字节,那么只做保存,待满足一个密钥的字节后再进行加密 过程如下:

加密:

明文(260个字节) iv(128个字节)

明文1(128个字节)    明文2(128个字节)      明文3(4个字节)

(iv+key)异或 明文1 (密文1+key)异或 明文1  (密文1+key)异或明文3

密文1(128个字节)     密文2(128个字节)      密文3(4个字节)


解密:

密文(260个字节) iv(128个字节)密钥(128字节)

密文1(128个字节)      密文2(128个字节)      密文3(4个字节)

(iv+key)异或密文1   (密文1+key)异或密文2    (密文1+key)异或密文3

明文1 (128个字节)     明文2  (128个字节)      明文3(4个字节)

这里需要注意如下几点:

1.加解密时会返回一个num,这个num表示还需要几个数字,才会使用上一个密文加密,否则一直使用上上一个

2.加解密时也需要传入字符串的长度

3.由于解密时使用的都是密文来进行解密,并没有使用上一次解密的明文,因此解密也可以并行

4.由于CFB模式并不需要补全,或者一个完整的128字节才能加解密,综合第三点,所以适合流数据的传输。

5.CFB模式不止有CFB128(即与密钥长度一致),还有CFB1 和CFB8 即加解密1或8位后,再调用一次加密器生成新的值,这样可以使加密更安全,但是就会处理更多的运算,CFB1的运算时间是CFB8的八倍 CFB128的128倍

6.使用CFB128或者CFB8的时候传入的length单位是字节,CFB1是length的单位是位。

7.使用CFB1和CFB8的时候,num值会始终为0

优点:解密可同步,可以传入非16字节倍数的数据,适合流数据

CFB模式当然也有一个缺点,解密的时候可以并行解密,但是加密的时候并不可以并行加密。并且也需要选择iv


** OFB模式: 输出反馈模式 Output FeedBack

该模式与CFB类似,但是是将iv或者上一个iv加密后的数据加密,生成的key与明文做异或运算,解密时采用的是同样的方法,利用了异或运算的对称性来进行加解密,除了这一点,其余与CFB一致

加密/解密:

CFB:

(iv+key)异或 明文1 (密文1+key)异或 明文1       (密文1+key)异或明文3

OFB

(iv+key)异或明文1 ((iv+key)+key)异或明文1  (((iv+key)+key)+key)异或明文3

优点:与CFB一样,方便传输流数据

缺点:由于依赖上一次的加密结果,所以并不能并行处理,特性是解密步骤完全一致,因此使用方法上不会有区别。


** CTR模式: 计算器模式 Counter

OFB不能并行的原因就在于需要上一次的iv进行加密后的结果,因此在CTR中我们将(iv+key)+key替换成了(iv+1)+key,这样我们就不需要依赖上一次的加密结果了。对比如下:

OFB

(iv+key)异或明文1 ((iv+key)+key)异或明文1  (((iv+key)+key)+key)异或明文3

CTR

(iv+key)异或明文1 ((iv+1)+key)异或明文1    (((iv+1)+1)+key)异或明文3

优点:由于加解密可以并行,因此CTR模式的加解密速度也很快

缺点:iv+1的获取比较负责,需要获取瞬时iv


三、实例:


使用AES ECB加密解密:

package com.nig.demo.utils;

import com.nig.demo.entity.ImageDepot;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * @Description :   描述
 * @program: lion      程序
 * @ClassName AesEncodeUtil.java       类名
 * @author: Mr.Wang               作者
 * @date: 2021-12-11 09:24 // 生成时间
 **/
public class AesEncodeUtil {

    //偏移量
    public static final String VIPARA = "1234567876543210";

    //编码方式
    public static final String CODE_TYPE = "UTF-8";
    //public static final String CODE_TYPE = "GBK";

    //填充类型
    public static final String AES_TYPE = "AES/ECB/PKCS5Padding";
    //public static final String AES_TYPE = "AES/CBC/PKCS7Padding";
    //此类型 加密内容,密钥必须为16字节的倍数位,否则抛异常,需要字节补全再进行加密
    //public static final String AES_TYPE = "AES/ECB/NoPadding";
    //java 不支持ZeroPadding
    //public static final String AES_TYPE = "AES/CBC/ZeroPadding";

    //私钥 AES固定格式为128/192/256 bits.即:16/24/32bytes。DES固定格式为128bits,即8bytes。
    private static final String AES_KEY = "1111222233334444";


    //字符补全
    private static final String[] consult = new String[]{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G"};


    /**
     * 加密
     *
     * @param cleartext
     * @return
     */
    public static String encrypt(String cleartext) {
        try {
//            IvParameterSpec zeroIv = new IvParameterSpec(VIPARA.getBytes());
            SecretKeySpec key = new SecretKeySpec(AES_KEY.getBytes(), "AES");
            Cipher cipher = Cipher.getInstance(AES_TYPE);
            //CBC类型的可以在第三个参数传递偏移量zeroIv,ECB没有偏移量
            cipher.init(Cipher.ENCRYPT_MODE, key);

            byte[] encryptedData = cipher.doFinal(cleartext.getBytes(CODE_TYPE));
            byte[] encode = Base64.getEncoder().encode(encryptedData);

            return new String(encode);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }


    /**
     * 解密
     *
     * @param encrypted
     * @return
     */
    public static String decrypt(String encrypted) {
        try {
            byte[] byteMi = new BASE64Decoder().decodeBuffer(encrypted);
//            IvParameterSpec zeroIv = new IvParameterSpec(VIPARA.getBytes());
            SecretKeySpec key = new SecretKeySpec(
                    AES_KEY.getBytes(), "AES");
            Cipher cipher = Cipher.getInstance(AES_TYPE);
            //与加密时不同MODE:Cipher.DECRYPT_MODE
            //CBC类型的可以在第三个参数传递偏移量zeroIv,ECB没有偏移量
            cipher.init(Cipher.DECRYPT_MODE, key);
            byte[] decryptedData = cipher.doFinal(byteMi);
            return new String(decryptedData, CODE_TYPE);
        } catch (Exception e) {
            e.printStackTrace();
            return "";
        }
    }

    /**
     * 测试
     *
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        String str = "今天天气真不错";
        test(str);

    }

    public static String test(String content) throws UnsupportedEncodingException {
        System.out.println("加密内容:" + content);
        //字节数
        int num = content.getBytes(CODE_TYPE).length;
        System.out.println("加密内容字节数: " + num);
        System.out.println("加密内容是否16倍数: " + (num % 16 == 0 ? true : false));

        //字节补全
        if (AES_TYPE.equals("AES/ECB/NoPadding")) {
            System.out.println();
            content = completionCodeFor16Bytes(content);
            System.out.println("加密内容补全后: " + content);
        }

        // 加密
        String encryptResult = encrypt(content);
        content = encryptResult;
        System.out.println("加密后:" + content);

        // 解密
        String decryptResult = decrypt(encryptResult);
        content = decryptResult;
        //还原
        if (AES_TYPE.equals("AES/ECB/NoPadding")) {
            System.out.println("解密内容还原前: " + content);
            content = resumeCodeOf16Bytes(content);
        }

        System.out.println("解密完成后:" + content);
        return content;
    }

    public static String AESencrypt(String content) throws UnsupportedEncodingException {
        //字节补全
        if (AES_TYPE.equals("AES/ECB/NoPadding")) {
            System.out.println();
            content = completionCodeFor16Bytes(content);
            System.out.println("加密内容补全后: " + content);
        }
        // 加密
        String encryptResult = encrypt(content);
        content = encryptResult;
        System.out.println(content);
        return content;
    }


    //NoPadding
    //补全字符
    public static String completionCodeFor16Bytes(String str) throws UnsupportedEncodingException {
        int num = str.getBytes(CODE_TYPE).length;
        int index = num % 16;
        //进行加密内容补全操作, 加密内容应该为 16字节的倍数, 当不足16*n字节是进行补全, 差一位时 补全16+1位
        //补全字符 以 $ 开始,$后一位代表$后补全字符位数,之后全部以0进行补全;
        if (index != 0) {
            StringBuffer sbBuffer = new StringBuffer(str);
            if (16 - index == 1) {
                sbBuffer.append("$" + consult[16 - 1] + addStr(16 - 1 - 1));
            } else {
                sbBuffer.append("$" + consult[16 - index - 1] + addStr(16 - index - 1 - 1));
            }
            str = sbBuffer.toString();
        }
        return str;
    }

    //追加字符
    public static String addStr(int num) {
        StringBuffer sbBuffer = new StringBuffer("");
        for (int i = 0; i < num; i++) {
            sbBuffer.append("0");
        }
        return sbBuffer.toString();
    }


    //还原字符(进行字符判断)
    public static String resumeCodeOf16Bytes(String str) {
        int indexOf = str.lastIndexOf("$");
        if (indexOf == -1) {
            return str;
        }
        String trim = str.substring(indexOf + 1, indexOf + 2).trim();
        int num = 0;
        for (int i = 0; i < consult.length; i++) {
            if (trim.equals(consult[i])) {
                num = i;
            }
        }
        if (num == 0) {
            return str;
        }
        return str.substring(0, indexOf).trim();
    }
}


使用AES CBC加密解密:

package com.nig.demo.utils;

import com.nig.demo.entity.ImageDepot;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * @Description :   描述
 * @program: lion      程序
 * @ClassName AesEncodeUtil.java       类名
 * @author: Mr.Wang               作者
 * @date: 2021-12-11 09:24 // 生成时间
 **/
public class AesEncodeUtil {

    //偏移量
    public static final String VIPARA = "1234567876543210";

    //编码方式
    public static final String CODE_TYPE = "UTF-8";
    //public static final String CODE_TYPE = "GBK";

    //填充类型
    public static final String AES_TYPE = "AES/CBC/PKCS5Padding";
    //public static final String AES_TYPE = "AES/CBC/PKCS7Padding";
    //此类型 加密内容,密钥必须为16字节的倍数位,否则抛异常,需要字节补全再进行加密
    //public static final String AES_TYPE = "AES/ECB/NoPadding";
    //java 不支持ZeroPadding
    //public static final String AES_TYPE = "AES/CBC/ZeroPadding";

    //私钥 AES固定格式为128/192/256 bits.即:16/24/32bytes。DES固定格式为128bits,即8bytes。
    private static final String AES_KEY = "1111222233334444";


    //字符补全
    private static final String[] consult = new String[]{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G"};


    /**
     * 加密
     *
     * @param cleartext
     * @return
     */
    public static String encrypt(String cleartext) {
        try {
            IvParameterSpec zeroIv = new IvParameterSpec(VIPARA.getBytes());
            SecretKeySpec key = new SecretKeySpec(AES_KEY.getBytes(), "AES");
            Cipher cipher = Cipher.getInstance(AES_TYPE);
            //CBC类型的可以在第三个参数传递偏移量zeroIv,ECB没有偏移量
            cipher.init(Cipher.ENCRYPT_MODE, key,zeroIv);

            byte[] encryptedData = cipher.doFinal(cleartext.getBytes(CODE_TYPE));
            byte[] encode = Base64.getEncoder().encode(encryptedData);

            return new String(encode);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }


    /**
     * 解密
     *
     * @param encrypted
     * @return
     */
    public static String decrypt(String encrypted) {
        try {
            byte[] byteMi = new BASE64Decoder().decodeBuffer(encrypted);
            IvParameterSpec zeroIv = new IvParameterSpec(VIPARA.getBytes());
            SecretKeySpec key = new SecretKeySpec(
                    AES_KEY.getBytes(), "AES");
            Cipher cipher = Cipher.getInstance(AES_TYPE);
            //与加密时不同MODE:Cipher.DECRYPT_MODE
            //CBC类型的可以在第三个参数传递偏移量zeroIv,ECB没有偏移量
            cipher.init(Cipher.DECRYPT_MODE, key,zeroIv);
            byte[] decryptedData = cipher.doFinal(byteMi);
            return new String(decryptedData, CODE_TYPE);
        } catch (Exception e) {
            e.printStackTrace();
            return "";
        }
    }

    /**
     * 测试
     *
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        String str = "今天天气真不错-CBC加密";
        test(str);

    }


    public static String test(String content) throws UnsupportedEncodingException {
        System.out.println("加密内容:" + content);
        //字节数
        int num = content.getBytes(CODE_TYPE).length;
        System.out.println("加密内容字节数: " + num);
        System.out.println("加密内容是否16倍数: " + (num % 16 == 0 ? true : false));

        //字节补全
        if (AES_TYPE.equals("AES/ECB/NoPadding")) {
            System.out.println();
            content = completionCodeFor16Bytes(content);
            System.out.println("加密内容补全后: " + content);
        }

        // 加密
        String encryptResult = encrypt(content);
        content = encryptResult;
        System.out.println("加密后:" + content);

        // 解密
        String decryptResult = decrypt(encryptResult);
        content = decryptResult;
        //还原
        if (AES_TYPE.equals("AES/ECB/NoPadding")) {
            System.out.println("解密内容还原前: " + content);
            content = resumeCodeOf16Bytes(content);
        }

        System.out.println("解密完成后:" + content);
        return content;
    }

    public static String AESencrypt(String content) throws UnsupportedEncodingException {
        //字节补全
        if (AES_TYPE.equals("AES/ECB/NoPadding")) {
            System.out.println();
            content = completionCodeFor16Bytes(content);
            System.out.println("加密内容补全后: " + content);
        }
        // 加密
        String encryptResult = encrypt(content);
        content = encryptResult;
        System.out.println(content);
        return content;
    }


    //NoPadding
    //补全字符
    public static String completionCodeFor16Bytes(String str) throws UnsupportedEncodingException {
        int num = str.getBytes(CODE_TYPE).length;
        int index = num % 16;
        //进行加密内容补全操作, 加密内容应该为 16字节的倍数, 当不足16*n字节是进行补全, 差一位时 补全16+1位
        //补全字符 以 $ 开始,$后一位代表$后补全字符位数,之后全部以0进行补全;
        if (index != 0) {
            StringBuffer sbBuffer = new StringBuffer(str);
            if (16 - index == 1) {
                sbBuffer.append("$" + consult[16 - 1] + addStr(16 - 1 - 1));
            } else {
                sbBuffer.append("$" + consult[16 - index - 1] + addStr(16 - index - 1 - 1));
            }
            str = sbBuffer.toString();
        }
        return str;
    }

    //追加字符
    public static String addStr(int num) {
        StringBuffer sbBuffer = new StringBuffer("");
        for (int i = 0; i < num; i++) {
            sbBuffer.append("0");
        }
        return sbBuffer.toString();
    }


    //还原字符(进行字符判断)
    public static String resumeCodeOf16Bytes(String str) {
        int indexOf = str.lastIndexOf("$");
        if (indexOf == -1) {
            return str;
        }
        String trim = str.substring(indexOf + 1, indexOf + 2).trim();
        int num = 0;
        for (int i = 0; i < consult.length; i++) {
            if (trim.equals(consult[i])) {
                num = i;
            }
        }
        if (num == 0) {
            return str;
        }
        return str.substring(0, indexOf).trim();
    }
}



版权声明:本文为m0_57556230原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。