数据结构和算法(三):常用对称加密算法之AES

摘要加密算法一般是用来保证数据的完整性和一致性,上一篇文章中提到了两个常用的摘要算法分别是SHA和MD5。在这篇里面,一起来学习一下一个常用的对称加密算法AES,它的主要作用是保证私密信息不被泄漏。对于这个应该都很熟悉,只要使用到对称加密,相信百分八十以上都是用AES。

接下来将会根据其不同的组成部分、特性、使用以及存在的坑做一下详细的说明。

本篇文章借鉴于小灰公众号文章:
什么是AES算法?(整合版)

使用场景

在数据加密传输的时候经常会用到AES,接收方可以根据秘钥对密文进行解密获取其内容。使用步骤如下:

  • 数据输出方通过秘钥"123456"将一段明文数据加密处理
  • 将密文通过网络发送给接收方
  • 接收方通过解密获得明文数据

如图示意:

在这里插入图片描述

了解AES在什么时候用到,下面来看一下AES重要的组成部分。

重要组成部分:秘钥

秘钥是非常重要的组成部分,也是决定数据安全性的重要部分。在AES中秘钥的长度有三种分别是128、192、256,所以AES算法又被分为AES128、AES192、AES256。秘钥的长度决定计算的复杂程度和安全性。一般使用到AES128即可。(使用后两种需要做特殊处理,后面有坑)

在AES加密的时候,并不是把整个AES加密成一个密文串,而是根据秘钥的长度,将明文切分成若干个明文块进行加密,得到若干个密文块,然后对密文块进行组装,最终形成一个密文串。当秘钥长度是128bit的时候,就会将明文切分成若干个128bit的明文块。对应192bit、256bit秘钥长度,也是类似切分。

示意图如下:

在这里插入图片描述

需要注意的问题:

秘钥不等于传入的key,因为这个key的长度是有要求的。如果是使用AES128,秘钥的长度必须是128bit,那么key的长度必须是128bit,在正常的过程中,很少能设置秘钥刚好长度是128bit,这个时候应该怎么办呢?在java中提供了生成秘钥的类。可以通过不同长度的key最终生成符合需求的秘钥。实现如下:

KeyGenerator generator = KeyGenerator.getInstance("AES");//指定加密方式
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");//指定SecureRandom的加密方式
secureRandom.setSeed("123456".getBytes());//将传入的key作为种子传入
generator.init(128, secureRandom);//指定长度
return generator.generateKey();//最终生成秘钥

更好的说法是将传入的key理解为秘钥种子。当然要是能做到长度刚刚好符合要求,称为秘钥也不是不可以。

重要组成部分:填充

填充,是一个很重要的概念,因为不同的明文长度变化很大,不可能保证长度刚好是128、192、256的整数倍。这个时候就需要对不满足长度的明文块进行填充,保证长度符合要求。在AES中有Nopadding、PKCS5Padding、ISO10126Padding三种方式,其中PKCS5Padding是默认的方式。下面了解一下。

NoPadding

从名字就看的出来,不填充,要求明文就是指定长度的整数倍。这种模式基本用不到,对传入明文长度要求太高。可用性不高,知道即可。

PKCS5Padding

这种模式是默认的模式。当最后一个明文块长度不满足要求的时候,会以缺少长度的值来填充。比如切分长度按照128bit来算,最后一个明文块的长度是112bit,缺少16bit,对应的字节数是2,此时后面2个字节都是用2来填充。如下图所示:

在这里插入图片描述

ISO10126Padding

此模式也会根据实际缺少的部分进行填充,但不是填充固定值而是随机值,最后一位是填充的位数。如下所示:

在这里插入图片描述

总共填充4位,最后一位值是4,其他三个是随机值。

注意点:这里填充的过程需要注意,除了NoPadding外,其他两种填充模式,即使出现明文长度是对应要求长度的整数倍,也会再填充一个明文块。(从秘钥的那张示意图就可以看出来,明文是4倍的128bit,但是在图中总共画了5个明文块)

重要组成部分:工作模式

工作模式总共有5种,但是常被使用到的就ECB和CBC两种模式。其中ECB是默认的工作模式,相对于效率最高,一般在开发中使用的比较多的还有一种是CBC,效率比ECB低一点,但是比ECB安全。

5种模式

  • ECB:电码本模式(Electronic Codebook Book)
  • CBC:密码分组链接模式(Cipher Block Chaining)
  • CTR:计算器模式(Counter)
  • CFB:密码反馈模式(Cipher Feedback)
  • OFB:输出反馈模式(Output Feedback)
ECB(电码本模式)

多个明文块分别和秘钥进行加密操作,最终生成多个密文块,明文块组装成对应的密文信息。在加密的过程是并发操作,不会互相干扰,加密过程用时比较长。加密示意图如下:

在这里插入图片描述

优缺点:

  • 并行加密,加密速度快,效率高
  • 相同明文块加密结果相同,安全性差
CBC(密码分组链接模式)

在CBC模式下会引入一个初始向量的概念,先看一下其加密流程图如下:

在这里插入图片描述

在加密的过程中,将前一个密文块和当前明文块做异或操作,将操作的结果进行加密得到对应的密文块,以此类推。但是当加密第一个明文块的时候,前面没有对应的密文块与其进行异或操作,这个时候就需要引入一个初始向量,作为与第一个明文块做异或操作的值。整个加密过程都是依赖上一个加密结果,所以只能串行,无法并行操作。

优缺点:

  • 安全性高
  • 引入初始向量,加密过程复杂度高
  • 整个加密过程无法并行计算,效率低

代码使用、坑和解决方案

代码实现

首先来看一下我自己对AES的封装,JDK自带的加密方法是有的,但是不够简洁,每次加密都要写一大串代码,因此在使用的时候必须做一下封装,作为一个工具类存在。下面看一下代码:

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;

/**
 * 填充方式:PKCS5Padding和NoPadding,默认使用PKCS5Padding,慎用NoPadding,因为用NoPadding要求传入的加密原文长度是16的倍数,条件台苛刻
 * 工作模式:ECB和CBC,建议使用CBC,因为CBC加密更安全
 * 长度:128,256,192,一般使用128即可
 */
public class AesUtils {
    private final static String KEY = "123456";
    private final static String IV = "654321";
    private final static String ALGORITHM = "AES";

    public static String encode(String content) throws Exception {
        return encode(content, PaddingModel.PKCS5, WorkModel.CBC, Length.AES128);
    }

    public static String decode(String content) throws Exception {
        return decode(content, PaddingModel.PKCS5, WorkModel.CBC, Length.AES128);
    }

    public static String encode(String content, PaddingModel paddingModel, WorkModel workModel, Length length) throws Exception {
        Cipher cipher = cipher(paddingModel, workModel);
        if (WorkModel.CBC.equals(workModel)) {
            cipher.init(Cipher.ENCRYPT_MODE, keySpec(length), parameterSpec(length));
        } else {
            cipher.init(Cipher.ENCRYPT_MODE, keySpec(length));
        }
        byte[] bytes = cipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(bytes);
    }

    public static String decode(String secret, PaddingModel paddingModel, WorkModel workModel, Length length) throws Exception {
        Cipher cipher = cipher(paddingModel, workModel);
        if (WorkModel.CBC.equals(workModel)) {
            cipher.init(Cipher.DECRYPT_MODE, keySpec(length), parameterSpec(length));
        } else {
            cipher.init(Cipher.DECRYPT_MODE, keySpec(length));
        }
        byte[] bytes = cipher.doFinal(Base64.getDecoder().decode(secret));
        return new String(bytes, StandardCharsets.UTF_8);
    }

    private static Cipher cipher(PaddingModel paddingModel, WorkModel workModel) throws Exception {
        return Cipher.getInstance(ALGORITHM + "/" + workModel.name() + "/" + paddingModel.getName());
    }

    private static SecretKeySpec keySpec(Length length) throws Exception {
        return new SecretKeySpec(keySpecBytes(length), ALGORITHM);//真正使用的秘钥
    }

    private static IvParameterSpec parameterSpec(Length length) throws Exception {
        return new IvParameterSpec(ivSpecBytes(length));
    }

    private static byte[] keySpecBytes(Length length) throws Exception {
        return secretKey(SourceType.KEY, length).getEncoded();
    }

    private static byte[] ivSpecBytes(Length length) throws Exception {
        return secretKey(SourceType.IV, length).getEncoded();
    }

    private static SecretKey secretKey(SourceType sourceType, Length length) throws Exception {
        KeyGenerator generator = KeyGenerator.getInstance(ALGORITHM);
        /*SecureRandom secureRandom =  new SecureRandom(IV.getBytes());
        if (SourceType.KEY.equals(sourceType)) secureRandom = new SecureRandom(KEY.getBytes());*/
        SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
        secureRandom.setSeed(IV.getBytes());
        if (SourceType.KEY.equals(sourceType)) secureRandom.setSeed(KEY.getBytes());
        generator.init(length.getValue(), secureRandom);
        return generator.generateKey();
    }

    private enum SourceType {
        KEY, IV
    }
}

测试使用的main方法:

public static void main(String[] args) throws Exception {
    String encode = encode("6543217865432178", PaddingModel.PKCS5, WorkModel.CBC, Length.AES128);
    System.out.println(encode);
    System.out.println(decode(encode, PaddingModel.PKCS5, WorkModel.CBC,Length.AES128));
}

具体代码里的内容就不做解释了,其中使用到自定义的枚举,这列也具体贴出来,文末会给出源码地址。

坑和解决方案

在调试过程有遇到两个坑。第一个是AES支持的位数问题,因为Java本身只支持128,导致使用192或者256位的时候报错(异常信息:java.security.InvalidKeyException: Illegal key size)。具体解决方案参考下面这篇博客。

java.security.InvalidKeyException: Illegal key size

第二个坑就是根据key(秘钥种子)生成符合位数要求的秘钥时出现的,SecureRandom中使用的PRNG是根据当前环境提供的PRNG获取的,由于不同的电脑,可能会出现PRNG不同,又或者提供的PRNG不支持,就会导致出现异常问题,不能正常的加解密。在调试过程,代码在win10系统下正常,同样的代码在mac中就会抛出异常。

通过了解一下SecureRandom中获取PRNG的过程可以知道,首先它是自动获取本地提供的PRNG,如果获取不到就是用默认的PRNG,也就是SHA1PRNG。所以为了避免不同系统,不同环境出现问题,这里统一在构建SecureRandom的时候就指定PRNG为SHA1PRNG即可解决。

这个问题可能会导致在本地运行正常,进入到测试或者生产环境出现异常,个人还是要重视一下的。

Source Code

码云(gitee):https://gitee.com/itcrud/itcrud-note/tree/master/itcrud-note-1-6

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序猿洞晓

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值