【网络安全之易混淆概念---AES详解】

本文详细介绍了AES算法,包括其128/192/256位密钥的使用,填充方式(如NoPadding、PKCS5Padding和ISO10126Padding),以及CBC模式的加密与解密过程。重点讲解了AES的工作原理和在实际开发中的应用,涉及密钥生成、盐值和加密模式的选择。
摘要由CSDN通过智能技术生成

背景

上一期我们谈到网络安全常用的加密算法分类,
本次重点分享我们开发中常见的对称加密-AES算法

算法AES简介–秘钥/填充/模式

AES(Advanced Encryption Standard)是一种对称加密算法(也叫共享密钥),对称加密算法的意思是加密和解密都是用同一个密钥(密钥和秘钥是同义词)同时也是一种分组加密算法,通常来说,对称加密算法效率要优于非对称加密算法,它用来代替DES(Data Encryption Standard,56位密钥)。那么为什么原来的DES会被取代呢,原因就在于其使用56位密钥,比较容易被破解。而AES可以使用128、192、和256位密钥,并且用128位分组加密和解密数据,相对来说安全很多。即使使用目前世界上运算速度最快的计算机,穷尽128位密钥也要花上几十亿年的时间
AES有三个关键点:密钥、填充、模式。

  • 秘钥
    密钥分为128位(16字节)、192位(24字节)、256位(32字节),位数越多,解密和加密的运算量越大,相应的越安全,可以折中使用192位兼顾效率和安全。
    在这里插入图片描述

  • 填充
    AES算法在对明文加密的时候,并不是把整个明文一股脑的加密成一整段密文,而是把明文拆分成一个个独立的明文块,每一个明文块长度128bit(16字节)。然后对明文块分别加密;这些明文块经过AES加密器复杂处理,生成一个个独立的密文块,这些密文块拼接在一起,就是最终的AES加密的结果,当然加密时与解密时的填充必须一致,否则无法解密。
    在这里插入图片描述

1、NoPadding—不填充,但要求明文必须是128位的整数倍;如果使用NoPadding,程序要对明文预处理进行填充,以达到16字节整数倍。

2、PKCS5Padding(默认)—如果明文块少于128位,则补足相应数量的字符,且字符的值等于缺少的字节数。比如明文:{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}。

3、ISO10126Padding—如果明文块少于16字节,则补足相应数量的字符,且最后一个字符的值等于缺少的字节数,其他字符填充随机数;比如明文:{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加解密只管Block内的事情,而加密模式Mode管的是Block之间的关系。加密模式解决的是Block之间的混淆扩散问题,严格来讲它不属于AES的一部分,任何基于块加密的算法都可以套用这些模式。AES的工作提供了五种不同的工作模式:(ECB、CBC、CFB、OFB)

    1、ECB模式
    这个模式是默认的,就只是根据密钥的位数,将数据分成不同的块进行加密,加密完成后,再将加密后的数据拼接起来。如下图所示
    优点::简单、有利于并行计算、误差不会被传送;
    缺点:不能隐藏明文的模式、如果明文块相同,则生成的密文块也相同,这样会导致安全性降低。
    在这里插入图片描述

    2、CBC模式
    CBC的模式引入了一个初始向量概念,该向量必须是一个与密钥长度相等的数据,
    在第一次加密前,会使用初始化向量与第一块数据做异或运算,生成的新数据再进行加密,
    加密第二块之前,会拿第一块的密文数据与第二块明文进行异或运算后再进行加密,
    以此类推,解密时也是在解密后,进行异或运算,生成最终的明文
    优点:不容易主动攻击、安全性好于ECB、适合传输长度长的报文,是SSL、IPSec的标准、每次加密盐值不同,加强了安全性
    缺点:不利于并行计算、误差传递、需要初始化向量IV
    在这里插入图片描述

    3、CFB模式:
    优点:隐藏了明文模式、分组密码转化为流模式、可以及时加密传送小于分组的数据;
    缺点:不利于并行计算;、误差传送:一个明文单元损坏影响多个单元、唯一的IV;
    在这里插入图片描述

算法AES加密过程

上面我们也提到过AES是分组加密的,加密过程是在一个4×4的字节矩阵上运作的,矩阵初值是一个明文块,加密时根据密钥的长度,加密的轮数也会有变化(每一个明文块都要经过n轮加密),比如当密钥长度为128位时,加密轮数为10轮。
每一轮(最后一轮除外)加密均包含4个步骤:字节替代、行移位、列混淆、轮密钥加。最后一轮加密循环省略列混淆这一步骤.
在这里插入图片描述

AES加解密实现

  • 使用原生秘钥key,统一生成加密Key对象
    /**
     * 生成密钥对象
     * 1、根据指定的 RNG 算法, 创建安全随机数生成器,提供秘钥随机数来源
     * 2、设置 密钥key的字节数组 作为安全随机数生成器的种子
     *    加密没关系,SecureRandom是生成安全随机数序列, 只要种子相同,序列就一样
     * 3、创建 AES算法生成器, 初始化算法生成器
     * 4、生成 AES密钥对象,
     * 5、也可以直接创建密钥对象: new SecretKeySpec(sourceKey.getBytes(StandardCharsets.UTF_8),"AES")
     * 6、使用5操作方式sourceKey必须为16*2个的byte
     * 7、加密算法的秘钥长度一般都是有限的。jca 附录A; 对应算法允许的密钥长度
     *    Algorithm    Maximum Keysize
     *      DES           56
     *      DESede        112或168
     *      RC2           128
     *      RC4           128
     *      RC5           128
     *      RSA           128
     *      others        128
     */
    private static SecretKeySpec generateKey(String sourceKeyStr, String encryptType) throws Exception {
        SecureRandom random = new SecureRandom(sourceKeyStr.getBytes(StandardCharsets.UTF_8));

        KeyGenerator keyGen = KeyGenerator.getInstance(encryptType);
        keyGen.init(256, random);
        SecretKey secretKey = keyGen.generateKey();
        return new SecretKeySpec(secretKey.getEncoded(), encryptType);
    }
  • 使用动态盐值对原生内容加密
  • 加密时必须要使用安全随机数SecureRadom生成强随机数,保证每次生成的盐值salt不一样
  • 在输出时需要将盐值与结果做合并以便做解密
    public static String encryptCBCModeDynamicSalt(String content) throws Exception {
        SecureRandom random = SecureRandom.getInstanceStrong();
        byte[] seedByte = new byte[16];
        random.nextBytes(seedByte);

        SecretKey secretKey = generateKey(SOURCE_KEY, "AES");
        Cipher encryptCipher = Cipher.getInstance(CBC_PKCS5_PAD);
        encryptCipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(seedByte));

        byte[] encryptBytes = encryptCipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
        String encryptResult = String.format("%s*%s", byteArrayToHex(seedByte), byteArrayToHex(encryptBytes));

        System.out.println("encryptWithCBCPkcVi current content is =" + content);
        System.out.println("encryptWithCBCPkcVi encrypt result is =" + encryptResult);
        return encryptResult;
    }
  • 使用动态盐值做解密
  • 原生的加密串中一定要带着当前加密所需要的盐值,不然无法解密
    /**
     * encryptWithCBCPkcVi
     * 使用安全随机数生成种子
     */
    public static String decryptCBCModeDynamicSalt(String hexContent) throws Exception {
        String[] hexArray = hexContent.split("\\*");
        byte[] saltBytes = hex2ByteArray(hexArray[0]);
        byte[] encryptedBytes = hex2ByteArray(hexArray[1]);


        Cipher decryptCipher = Cipher.getInstance(CBC_PKCS5_PAD);
        SecretKey secretKey = generateKey(SOURCE_KEY, "AES");

        decryptCipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(saltBytes));
        byte[] decryptedBytes = decryptCipher.doFinal(encryptedBytes);
        String decryptResult = new String(decryptedBytes, StandardCharsets.UTF_8);

        System.out.println("decryptWithCBCPkcVi current encryptedData is =" + hexContent);
        System.out.println("decryptWithCBCPkcVi decryptResult =" + decryptResult);
        return decryptResult;
    }
  • 字节数组转换成Hex串,可以使用String.format
    private static String byteArrayToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        Stream.iterate(0, i -> i+1).limit(bytes.length)
            .forEach(i ->  sb.append(String.format("%02x", bytes[i])));
        return sb.toString();
    }
  • 一个字符表示两个字节、一个字节使用两个Hex位置
  • 因为一个十六进制字符是用半个字节长度实现,所以两位Hex会缩短成一个字节
  • 十六进制Hex字符需要先转成十进制,然后才能左右移动
    private static byte[] hex2ByteArray(String hexString) {
        hexString = hexString.replaceAll(" ", "");
        if (StringUtils.isEmpty(hexString) || hexString.length() % 2 != 0) {
            return new byte[0];
        }

        int hexLen = hexString.length();
        byte[] outBytes = new byte[hexLen / 2];

        for (int i = 0; i < hexLen; i += 2) {
            int high = Character.digit(hexString.charAt(i), 16) << 4;
            int low = Character.digit(hexString.charAt(i + 1), 16);
            outBytes[i/2] = (byte) ((high | low) & 0xff);
        }
        return outBytes;
    }
  • 测试结果
    在这里插入图片描述

安全随机数SecureRadom特性

  • 1、什么是安全的随机数
    在安全应用场景,随机数应该使用安全的随机数。密码学意义上的安全随机数,要求必须保证其不可预测性。

  • 2、怎么得到安全的随机数
    可以直接使用真随机数产生器产生的随机数。或者使用真随机数产生器产生的随机数做种子,输入密码学安全的伪随机数产生器产生密码学安全随机数。
    非物理真随机数产生器有:
    Linux操作系统的/dev/random设备接口
    Windows操作系统的CryptGenRandom接口
    密码学安全的伪随机数产生器,包括JDK的java.security.SecureRandom

  • 3、SecureRandom.getInstanceStrong()
    对随机性很强的场景才是用,依赖操作系统的随机操作,
    比如键盘输入, 鼠标点击, 等等, 当系统扰动很小时, 产生的随机数不够, 导致读取/dev/random的进程会阻塞等待.
    对性能有很大影响。推荐使用new SecureRandom()获取SecureRandom, linux下从/dev/urandom读取. 虽然是伪随机, 但大部分场景下都满足。如果使用SecureRandom.getInstanceStrong()这种方法初始化SecureRandom对象的话,会使用NativePRNGBlocking这个算法,完全依赖/dev/random,所以当这个文件随机数不够的时候,自然会导致卡顿了

  • 4、解决getInstanceStrong性能问题
    SecureRandom本身并不是伪随机算法的实现,而是使用了其他类提供的算法来获取伪随机数。
    Linux下的NativePRNG,如果调用generateSeed()方法,这个方法会读取Linux系统的/dev/random文件,这个文件在JAVA_HOME/jre/lib/securiy/java.security里面有默认定义。而/dev/random文件是动态生成的,如果没有数据,就会阻塞。
    如果简单的new一个SecureRandom对象的话,在不同的操作平台会获取到不同的算法,windows默认是SHA1PRNG,Linux的话是NativePRNG。
    避免getInstanceStrong阻塞,可以使用-Djava.security.egd=file:/dev/./urandom (这个文件名多个u)强制使用/dev/urandom这个文件。

结束

本期探讨了AES重要算法和实现,下期再探讨RSA算法,晚安,再见!!!!

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值