AES加密解密介绍

加密算法

在密码学中,加密算法分为单向加密和双向加密。

  • 单向加密包括MD5、SHA等摘要算法,它们是不需要密钥、不可逆的。
  • 双向加密包括对称加密和非对称加密。双向加密是需要密钥、可逆的,存在密文的密钥。
    • 对称加密是指加密和解密使用相同的密钥,包括AES加密、DES加密等。
    • 非对称加密是指加密和解密使用不同的密钥,包括RSA加密等。

一、AES加密模式

  1. ECB(Electronic Codebook,电子密码本)模式

将每个分组独立加密,适用于对称性较弱的数据。

ECB是最简单的AES加密模式之一。它使用相同的密钥对数据进行多次加密,每次加密都是对整个数据块进行的。由于每次加密都使用相同的密钥,因此对于单个数据块而言,ECB模式是一种非常安全的加密方式。然而,当面对大量数据时,ECB模式的加密速度会非常慢。此外,如果攻击者能够解密先前的数据块,那么他们就能够破解后续的数据块。

  1. CBC(Cipher Block Chaining,串行密钥传输)模式

前一个分组的密文与当前分组的明文进行异或操作后再加密,增加了分组之间的关联性。

CBC是一种比ECB更加安全的加密模式。在CBC模式中,每个数据块都被分成两个部分:明文和密钥。第一个数据块被称为“前向块”,第二个数据块被称为“后向块”。前向块的密文是由密钥和前向块一起计算得到的,而后向块的密文则是由密钥和后向块一起计算得到的。这种模式可以确保即使攻击者能够获得密钥的一部分,也无法解密整个数据块。但是,CBC模式的速度较慢,因为需要处理大量的中间结果。

  1. CTR(Counter,计数器)模式

通过使用计数器生成密钥流,将密钥流与明文进行异或操作得到密文。

CTR模式与CBC模式非常相似,但是它使用了不同的密钥分配方法。在CTR模式中,每个数据块都由一个随机数生成器产生,该随机数生成器用于确定数据的索引位置。这使得CTR模式非常适合于流式数据传输,例如视频和音频流媒体。CTR模式也非常快,因为它只需要处理每个数据块的中间结果,而不需要像CBC模式那样处理整个数据块。

  1. CFB(可逆计数器)模式
    CFB模式类似于CTR模式,但是它使用的是可变长度的密钥。在CFB模式中,每个数据块都有一个偏移量,这个偏移量是根据密钥计算出来的。这使得CFB模式非常适合于多通道应用程序,例如网络路由表。然而,由于密钥的长度可变,所以CFB模式不如CTR模式快速。

二、填充算法

填充算法用于填充输入数据以匹配AES模式的要求。下面是一些常用的填充算法及其优缺点:

  1. NoPadding

    不做任何填充,但是要求明文必须是16字节的整数倍。

    NoPadding是一种不使用填充算法的加密模式。不做任何填充,但是要求明文必须是16字节的整数倍。这意味着输入数据将被直接编码为字节序列,而不会受到填充的影响。虽然NoPadding模式非常安全,但它会导致填充算法无法正确地处理数据。

  2. ZerosPadding
    ZerosPadding是一种简单的填充算法,它将输入数据的最后一个字节替换为0。这种填充方式适用于所有AES加密模式,包括ECB、CBC和CTR模式。缺点是填充后的字节数量可能超过原始输入数据的大小。

  3. PKCS5padding(默认)

    如果明文块少于16个字节(128bit),在明文块末尾补足相应数量的字符,且每个字节的值等于缺少的字符数。

    比如明文:{1,2,3,4,5,a,b,c,d,e},缺少6个字节,则补全为{1,2,3,4,5,a,b,c,d,e,6,6,6,6,6,6}。

    PKCS5padding是一种常见的填充算法,用于填充数据以符合AES模式的要求。这种填充方式使用一个固定长度的填充字节序列,以确保输入数据被完全填充。PKCS#5 padding的优点是它可以适应各种输入数据的大小,但缺点是填充过程可能很慢。

  4. PKCS7padding
    PKCS7padding也是一种填充算法,它使用一个固定长度的填充字节序列来填充数据。这种填充方式的优点是可以适应各种输入数据的大小,并且填充过程相对较快。但是,它与PKCS#5 padding一样,可能需要较长的填充时间。

  5. ISO10126Padding

    如果明文块少于16个字节(128bit),在明文块末尾补足相应数量的字节,最后一个字符值等于缺少的字符数,其他字符填充随机数。

    比如明文:{1,2,3,4,5,a,b,c,d,e},缺少6个字节,则可能补全为{1,2,3,4,5,a,b,c,d,e,5,c,3,G,$,6}。

三、AES的加密过程(AES处理单位:字节)

AES的加解密过程和DES一样,都是通过分组加密、分组解密。所谓分组加密,就是将待加解密的内容按照128位进行分组,可以将密钥按照128位、192位、256位进行分组,分别将分组后的明文与相应分组后的密钥进行加解密。

加密: 明文与密钥分组后,对每组:明文组与密钥组处理 -> 轮密钥加 -> 10轮加密 -> 密文组

解密: 对每组:密文组 -> 轮密钥加 -> 10轮解密 -> 明文组

明文分组: 每组长度相等,都是128位(16字节);

密钥分组: 有128位、192位、256位,推荐加密轮数分别为 10、12、14

密钥组处理: 以密钥分组每组128位为例(则推荐加密轮数为10,前9次执行操作一样,第十次有所不同) 类似地,128位密钥也是用字节为单位的矩阵表示,通过密钥编排函数,形成具有44个元素的序列W[0],W[1], … ,W[43](每个元素4个字节);其中,W[0],W[1],W[2],W[3]为原始密钥,其余40个元素分为10组,每组4个元素(4*4=16字节),分别用于10轮加密。

AES加密算法涉及4种操作: 字节替代(SubBytes)、行移位(ShiftRows)、列混淆(MixColumns)和轮密钥加(AddRoundKey)。

下图给出了AES加解密的流程:

  • AddRoundKey (轮密钥加)— 矩阵中的每一个字节都与该次轮密钥(round key)做XOR运算;每个子密钥由密钥生成方案产生。
  • SubBytes(字节替代) — 通过非线性的替换函数,用查找表的方式把每个字节替换成对应的字节。
  • ShiftRows(行移位) — 将矩阵中的每个横列进行循环式移位。
  • MixColumns (列混淆)— 为了充分混合矩阵中各个直行的操作。这个步骤使用线性转换来混合每列的四个字节。

四、注意

现加密结果不一致的原因可能是多种多样的。以下是一些可能导致结果不一致的原因:

  1. 加密工具使用的是不同的加密算法或加密模式,例如CBC、CFB、OFB等等。
  2. 加密工具使用的是不同的填充方案,例如PKCS#5、PKCS#7、ISO 10126等等。
  3. 加密工具对原始数据进行了预处理或后处理,例如添加了Salt或IV。
  4. 加密工具在进行加密时使用了不同的编码方式,例如Base64、Hex、UTF-8等等。

五、JAVA示例代码

注意:AES加密包含Base64加密

加密: AES加密 -> Base64加密 -> 密文

解密: Base64解密 -> AES解密 -> 明文

package com.study.util;


import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.Base64Utils;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Random;


public class AESUtil {
    private static final String AES_KEY = "AnjiPLUSAjReport";
    /**
     * 编码
     */
    private static final String ENCODING = "UTF-8";
    /**
     * 算法定义
     */
    private static final String AES_ALGORITHM = "AES";
    /**
     * ECB填充模式
     */
    private static final String CIPHER_ECB_PADDING = "AES/ECB/PKCS5Padding";
    /**
     * 使用CBC模式,需要加入额外的Iv参数。
     */
    private static final String CIPHER_CBC_PADDING = "AES/CBC/PKCS5Padding";
    /**
     * 偏移量(CBC中使用,增强加密算法强度)
     */
    private static final String IV_SEED = "1191168741434234";

    /**
     * 密钥生成工具,生成key
     *
     * @return
     */
    public static String generateKey() {
        KeyGenerator kgen = null;
        try {
            kgen = KeyGenerator.getInstance(AES_ALGORITHM);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        kgen.init(128);
        return Base64.getEncoder().encodeToString(kgen.generateKey().getEncoded());
    }

    /**
     * 获取长度为 6 的随机字母+数字
     *
     * @return 随机数字
     */
    public static String getRandomNumber(int length) {
        String symbols = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; // 数字和26个字母组成
        Random random = new SecureRandom();
        char[] nonceChars = new char[length];  //指定长度为6位/自己可以要求设置
        for (int index = 0; index < nonceChars.length; ++index) {
            nonceChars[index] = symbols.charAt(random.nextInt(symbols.length()));
        }
        return new String(nonceChars);
    }

    /**
     * 获取随机key
     *
     * @return
     */
    public static String getKey() {
        return AES_KEY;
    }

    /**
     * 将byte[]转为各种进制的字符串
     *
     * @param bytes byte[]
     * @param radix 可以转换进制的范围,从Character.MIN_RADIX到Character.MAX_RADIX,超出范围后变为10进制
     * @return 转换后的字符串
     */
    public static String binary(byte[] bytes, int radix) {
        return new BigInteger(1, bytes).toString(radix);// 这里的1代表正数
    }

    /**
     * 填充key
     * key的字节长度只能是16位。
     *
     * @return 填充后的key
     */
    public static byte[] fillKey(String message, int length) {
        byte[] arrBTmp = message.getBytes();
        byte[] arrB = new byte[length];
        for (int i = 0; i < arrBTmp.length && i < arrB.length; ++i) {
            arrB[i] = arrBTmp[i];
        }
        return arrB;
    }

    /**
     * AES加密(AES加密 -> Base64加密 -> 密文)
     *
     * @param content    待加密的内容
     * @param encryptKey 加密密钥
     * @return 加密后的base 64 code
     * @throws Exception
     */
    public static String aesEncrypt(String content, String encryptKey) throws Exception {
        if (StringUtils.isBlank(encryptKey) || StringUtils.isBlank(content)) {
            return content;
        }

        Cipher cipher = Cipher.getInstance(CIPHER_ECB_PADDING);
        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(fillKey(encryptKey, 16), AES_ALGORITHM));
        byte[] encryptBytes = cipher.doFinal(content.getBytes(ENCODING));
        //return Base64.encodeBase64String(encryptBytes);
        return Base64.getEncoder().encodeToString(encryptBytes);
    }

    /**
     * AES解密(Base64解密 -> AES解密 -> 明文)
     *
     * @param encryptStr 待解密的base 64 code
     * @param decryptKey 解密密钥
     * @return 解密后的string
     * @throws Exception
     */
    public static String aesDecrypt(String encryptStr, String decryptKey) throws Exception {
        if (StringUtils.isBlank(decryptKey) || StringUtils.isEmpty(encryptStr)) {
            return encryptStr;
        }
        Cipher cipher = Cipher.getInstance(CIPHER_ECB_PADDING);
        cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(fillKey(decryptKey, 16), AES_ALGORITHM));
        byte[] decryptBytes = cipher.doFinal(Base64.getDecoder().decode(encryptStr));
        return new String(decryptBytes);
    }

    /**
     * AES_CBC加密
     *
     * @param content 待加密内容
     * @param aesKey  密码
     * @return
     */
    public static String cbcEncrypt(String content, String aesKey) {
        if (StringUtils.isBlank(content) || StringUtils.isBlank(aesKey)) {
            return null;
        }
        try {
            //对密码进行编码
            byte[] bytes = fillKey(aesKey, 16);
            //设置加密算法,生成秘钥
            SecretKeySpec skeySpec = new SecretKeySpec(bytes, AES_ALGORITHM);
            // "算法/模式/补码方式"
            Cipher cipher = Cipher.getInstance(CIPHER_CBC_PADDING);
            //偏移
            IvParameterSpec iv = new IvParameterSpec(IV_SEED.getBytes(ENCODING));
            //选择加密
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
            //根据待加密内容生成字节数组
            byte[] encrypted = cipher.doFinal(content.getBytes(ENCODING));
            //返回base64字符串
            return Base64Utils.encodeToString(encrypted);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * AES_CBC解密
     *
     * @param content 待解密内容
     * @param aesKey  密码
     * @return
     */
    public static String cbcDecrypt(String content, String aesKey) {
        if (StringUtils.isBlank(content) || StringUtils.isBlank(aesKey)) {
            return null;
        }
        try {
            //对密码进行编码
            byte[] bytes = fillKey(aesKey, 16);
            //设置解密算法,生成秘钥
            SecretKeySpec skeySpec = new SecretKeySpec(bytes, AES_ALGORITHM);
            //偏移
            IvParameterSpec iv = new IvParameterSpec(IV_SEED.getBytes(ENCODING));
            // "算法/模式/补码方式"
            Cipher cipher = Cipher.getInstance(CIPHER_CBC_PADDING);
            //选择解密
            cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
            //先进行Base64解码
            byte[] decodeBase64 = Base64Utils.decodeFromString(content);
            //根据待解密内容进行解密
            byte[] decrypted = cipher.doFinal(decodeBase64);
            //将字节数组转成字符串
            return new String(decrypted, ENCODING);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 测试
     */
    public static void main(String[] args) throws Exception {
//        String randomString = getKey();
        System.out.println("密钥生成key:" + generateKey());
        System.out.println("随机key:" + getRandomNumber(16));
        System.out.println("--------AES_ECB加密解密---------");
        String randomString = "y6KQFlk7OD/H4bms1fOn8g==";
        String content = "[{AAC002=350xxx199812110111, AccountStatus=A1, AverageAmountPaidInTheLast3Years=B2}]";
        System.out.println("加密前:" + content);
        System.out.println("加密密钥和解密密钥:" + randomString);
        String encrypt = aesEncrypt(content, randomString);
        System.out.println("加密后:" + encrypt);
//        System.out.println("加密后:" + aesEncrypt(null, randomString));
//        System.out.println("加密后:" + aesEncrypt("", randomString));
        String decrypt = aesDecrypt(encrypt, randomString);
        System.out.println("解密后:" + decrypt);
        System.out.println();

        // AES支持三种长度的密钥:128位、192位、256位。
        // 代码中这种就是128位的加密密钥,16字节 * 8位/字节 = 128位。
        String random = RandomStringUtils.random(16, "abcdefghijklmnopqrstuvwxyz1234567890");
        System.out.println("--------AES_CBC加密解密---------");
        String cbcContent= "测试AES加密12456";
        System.out.println("加密前:" + cbcContent);
        System.out.println("加密密钥和解密密钥:" + random);
        String cbcEncrypt = cbcEncrypt(cbcContent, random);
        System.out.println("加密后:" + cbcEncrypt);
        String cbcDecrypt = cbcDecrypt(cbcEncrypt, random);
        System.out.println("解密后:" + cbcDecrypt);
        System.out.println();
    }

}
posted @ 2024-01-26 15:21  海韵༒听心  阅读(82)  评论(0编辑  收藏  举报