3.开源AES/SM4/3DES对称加密算法的验证说明

前期内容导读:

  1. Java开源RSA/AES/SHA1/PGP/SM2/SM3/SM4加密算法介绍
  2. Java开源AES/SM4/3DES对称加密算法介绍及其实现
  • 在开源加密组件中,介绍了开源AES/SM4/3DES对称加密算法介绍及其实现 ,但是并没有对总结的结论做说明。现在从摘抄的单元测试类中,对对称加密做一轮充分的验证说明,以便加深大家对对称加密的理解;

  • 本文所列举的所有代码均可从bq-encryptor组件 开源代码的单元测试类中获取;

  • 加密组件引入方法:

    <dependency>
        <groupId>com.biuqu</groupId>
        <artifactId>bq-encryptor</artifactId>
        <version>1.0.5</version>
    </dependency>
    
  • bq-encryptor组件 的github地址:https://github.com/woollay/bq-encryptor

1.对称加解密验证设计

  • 开源AES/SM4/3DES对称加密算法介绍及其实现 中,介绍了对称加密的秘钥长度分组长度,那就需要验证下,在各种秘钥长度下,对应的秘钥长度是不是固定的;
  • 对称加密有多种工作模式、多种填充算法,它们组合起来会不会有所区别;
  • 对称加密算法支持盐值(加密偏移向量),盐值对加密上述组合是否有影响;

2.生成秘钥的验证

2.1 AES生成秘钥的验证

2.1.1 从秘钥对象SecretKey生成二进制验证
  • AES加密算法(包括AesEncryptionAesSecureEncryption,二者区别是前者不带盐值,后者带盐值,但是不影响秘钥生成,就不一一列举了),秘钥长度支持128/192/256(当下没有512的AES加密算法,不信去看下维基百科),按说生成的秘钥长度都是固定的,如128bit->16byte,192bit->24byte,256bit->32byte;
  • 同理可假定SM4/3DES也是如此;
  • 基于上述分析,可提取对称加密的抽象单元测试基类(BaseSingleEncryptionTest),其秘钥生成判断逻辑如下:
    //test1:使用任意初始值创建秘钥,秘钥始终是固定长度(和加密算法的长度相同)(3DES除外)
    SecretKey key = encryption.createKey(RandomUtils.nextBytes(32));
    Assert.assertTrue(key.getEncoded().length == keyLen / 8);
    SecretKey key2 = encryption.createKey(RandomUtils.nextBytes(64));
    Assert.assertTrue(key2.getEncoded().length == keyLen / 8);
    SecretKey key3 = encryption.createKey(RandomUtils.nextBytes(1));
    Assert.assertTrue(key3.getEncoded().length == keyLen / 8);
    
  • AES单元测试类中的完整秘钥生成单元测试方法为:
    @Test
    public void createKey()
    {
        int[] keyLenList = {128, 192, 256};
        for (int keyLen : keyLenList)
        {
            BaseSingleEncryption encryption = new AesEncryption();
            encryption.setEncryptLen(keyLen);
            super.createKey(encryption, keyLen);
        }
    }
    
2.1.2 从二进制构造SecretKey秘钥对象验证
  • 我们使用加解密时,通常会保存秘钥数据(一般是16进制或者文件保存),但是在Java语言的使用过程中,只认秘钥对象,所以必须要把秘钥数据反转成秘钥对象;
  • 什么样的秘钥长度数据可以转成合法的秘钥对象呢?
  • 同上节,在BaseSingleEncryptionTest抽象类中的验证代码如下:
    public void toKey(BaseSingleEncryption encryption, int encryptLen)
    {
        //test1:可以使用任意长度值获取秘钥对象(仅能生成秘钥),但是仅合法长度可以加密
        SecretKey secretKey = encryption.toKey(RandomUtils.nextBytes(encryptLen));
        Assert.assertNotNull(secretKey);
        byte[] data1 = RandomUtils.nextBytes(encryptLen - 1);
        byte[] encBytes1 = encryption.encrypt(data1, secretKey.getEncoded(), null);
        System.out.println("data1 len=" + data1.length + ",enc len=" + encBytes1.length);
        Assert.assertTrue(encBytes1.length == encryptLen);
        Assert.assertNotNull(encryption.toKey(RandomUtils.nextBytes(1)));
        Assert.assertNotNull(encryption.toKey(RandomUtils.nextBytes(2 * encryptLen)));
        Assert.assertNotNull(encryption.toKey(RandomUtils.nextBytes(3 * encryptLen + 1)));
    
        try
        {
            //test2:秘钥长度高于合法秘钥长度会报错
            byte[] key2 = RandomUtils.nextBytes(encryptLen + 1);
            SecretKey secretKey2 = encryption.toKey(key2);
            System.out.println("secretKey2 len=" + secretKey2.getEncoded().length);
            byte[] data2 = RandomUtils.nextBytes(encryptLen);
            byte[] encBytes2 = encryption.encrypt(data2, secretKey2.getEncoded(), null);
            System.out.println("data2 len=" + data2.length + ",enc len=" + encBytes2.length);
            Assert.fail();
        }
        catch (Exception e)
        {
            e.printStackTrace();
            Assert.assertTrue(true);
        }
    
        try
        {
            //test3:秘钥长度低于合法秘钥长度会报错
            byte[] key3 = RandomUtils.nextBytes(encryptLen - 1);
            SecretKey secretKey3 = encryption.toKey(key3);
            System.out.println("secretKey3 len=" + secretKey3.getEncoded().length);
            byte[] data3 = RandomUtils.nextBytes(encryptLen);
            byte[] encBytes3 = encryption.encrypt(data3, secretKey3.getEncoded(), null);
            System.out.println("data3 len=" + data3.length + ",enc len=" + encBytes3.length);
            Assert.fail();
        }
        catch (Exception e)
        {
            e.printStackTrace();
            Assert.assertTrue(true);
        }
    }
    
  • AES单元测试类中的验证代码如下:
    @Test
    public void toKey()
    {
        int[] keyLenList = {128, 192, 256};
        for (int keyLen : keyLenList)
        {
            BaseSingleEncryption encryption = new AesEncryption();
            encryption.setEncryptLen(keyLen);
            super.toKey(encryption, 16);
        }
    }
    
    1. 在此次验证中,可根据异常得知:在秘钥长度不为合法值时,会抛出异常:
      Key length not 128/192/256 bits.,这也间接说明秘钥长度最大只有256位;
    2. 在验证过程中,其实还可以发现无论什么长度的秘钥数据反转成秘钥对象,都不会报错,但是在使用对应的加解密方法时就会报错;

2.2 SM4生成秘钥的验证

2.2.1 从秘钥对象SecretKey生成二进制验证
  • 同AES加密算法的单元测试实现相同,仅需要定义测试方法即可:
    @Test
    public void createKey()
    {
        BaseSingleEncryption encryption = new Sm4Encryption();
        super.createKey(encryption, 128);
    }
    
  • 验证效果也同AES加密算法的单元测试效果相同;
2.2.2 从二进制构造SecretKey秘钥对象验证
  • 同AES加密算法的单元测试实现相同,仅需要定义测试方法即可:
    @Test
    public void toKey()
    {
        BaseSingleEncryption encryption = new Sm4Encryption();
        super.toKey(encryption, 16);
    }
    
  • 验证效果也同AES加密算法的单元测试效果相同;

2.3 3DES生成秘钥的验证

2.3.1 从秘钥对象SecretKey生成二进制验证
  • 3DES加密算法如上述2个标准的、安全的加密算法不同,它是DES算法的3重叠加,每重秘钥长度是64bit(8byte),所以总的秘钥长度一定不能低于192bit(24byte),共用BaseSingleEncryptionTest抽象类后,其单元测试代码如下:
    @Test
    public void createKey() throws NoSuchAlgorithmException
    {
        BaseSingleEncryption encryption = new Des3Encryption();
        try
        {
            //3DES不允许低于24byte的秘钥,因为无法解析出3个8byte的DES秘钥
            super.createKey(encryption, 192);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }
    
  • 在秘钥低于192bit(24byte)时,会抛出如下异常:Caused by: java.security.InvalidKeyException: Wrong key size
2.3.2 从二进制构造SecretKey秘钥对象验证
  • 由于3DES加密的秘钥是组合的,比较特殊,无法复用复杂的抽象检测逻辑,故单独设计,代码如下:
    @Test
    public void testGetKey()
    {
        BaseSingleEncryption encryption = new Des3Encryption();
    
        byte[] keyBytes = RandomUtils.nextBytes(24);
    
        //test1: 任意24byte的内容均可以作为3DES的秘钥
        SecretKey secretKey = encryption.toKey(RandomUtils.nextBytes(24));
        System.out.println("init key=" + Hex.toHexString(keyBytes));
        System.out.println("3des key=" + Hex.toHexString(secretKey.getEncoded()));
        Assert.assertTrue(secretKey.getEncoded().length == 24);
    
        //test2:秘钥对象的二进制和原始秘钥的二进制并不相同
        Assert.assertFalse(Hex.toHexString(secretKey.getEncoded()).equals(Hex.toHexString(keyBytes)));
    
        byte[] keyBytes2 = RandomUtils.nextBytes(25);
        //test3: 任意大于24byte的内容均可以作为3DES的秘钥,而且只会截取前24byte
        SecretKey secretKey2 = encryption.toKey(keyBytes2);
        Assert.assertTrue(secretKey2.getEncoded().length == 24);
        byte[] subBytes2 = ArrayUtils.subarray(keyBytes2, 0, 24);
        Assert.assertTrue(Hex.toHexString(secretKey2.getEncoded()).equals(Hex.toHexString(encryption.toKey(subBytes2).getEncoded())));
    }
    
  • 从上述可测试通过的用例可知:3DES秘钥是只有前192bit(24byte)有效,且过长的秘钥和截取其192bit的秘钥等同;

3.工作模式和填充模式的组合验证

3.1 AES加解密组合验证

  • AES加密算法的加解密逻辑在抽象后,其测试代码为:
    @Test
    public void testEncryptPadding()
    {
        int[] keyLenList = {128, 192, 256};
        String[] modes = {"ECB", "CBC", "CTR", "CFB"};
        String[] paddings = {"NoPadding", "PKCS5Padding"};
        for (int keyLen : keyLenList)
        {
            BaseSingleEncryption encryption = new AesEncryption();
            super.doCipher(encryption, keyLen, paddings, modes);
        }
    }
    
  • BaseSingleEncryptionTest抽象类的加解密验证逻辑如下:
    public void doCipher(BaseSingleEncryption encryption, int keyLen, String[] paddings, String[] modes)
    {
        this.doCipher(encryption, keyLen, 16, paddings, modes);
    }
    
    public void doCipher(BaseSingleEncryption encryption, int keyLen, int encGroupLen, String[] paddings,
        String[] modes)
    {
        //test1:分段(分组)加密明文长度为n的明文数据,存在填充时,密文长度为(n/encGroupLen+1)*encGroupLen的倍数(除法取整),无填充时为(n/encGroupLen)*encGroupLen的倍数(除法取整,且n必须为encGroupLen的倍数)
        encryption.setEncryptLen(keyLen);
        SecretKey secretKey = encryption.toKey(RandomUtils.nextBytes(keyLen / 8));
        Assert.assertEquals(secretKey.getEncoded().length, keyLen / 8);
        for (String mode : modes)
        {
            for (String padding : paddings)
            {
                StringBuilder alg = new StringBuilder(encryption.getAlgorithm());
                alg.append("/").append(mode);
                alg.append("/").append(padding);
                encryption.setPaddingMode(alg.toString());
    
                int paddingLen = 0;
                if (!"NoPadding".equals(padding))
                {
                    paddingLen = encGroupLen;
                }
                System.out.println("[" + keyLen + "]padding-1=" + alg);
    
                byte[] salt = RandomUtils.nextBytes(16);
    
                if (paddingLen > 0)
                {
                    byte[] data1 = RandomUtils.nextBytes(1);
                    byte[] encBytes1 = encryption.encrypt(data1, secretKey.getEncoded(), salt);
                    byte[] decBytes1 = encryption.decrypt(encBytes1, secretKey.getEncoded(), salt);
                    System.out.println("[" + keyLen + "]padding-1=" + alg + ",enc len=" + encBytes1.length);
                    System.out.println("[" + keyLen + "]padding-1=" + alg + ",dec len=" + decBytes1.length);
                    Assert.assertTrue(encBytes1.length == (data1.length / encGroupLen) * encGroupLen + paddingLen);
                    Assert.assertArrayEquals(data1, decBytes1);
                }
    
                byte[] data2 = RandomUtils.nextBytes(encGroupLen);
                byte[] encBytes2 = encryption.encrypt(data2, secretKey.getEncoded(), salt);
                byte[] decBytes2 = encryption.decrypt(encBytes2, secretKey.getEncoded(), salt);
                System.out.println("[" + keyLen + "]padding-2=" + alg + ",enc len=" + encBytes2.length);
                System.out.println("[" + keyLen + "]padding-2=" + alg + ",dec len=" + decBytes2.length);
                Assert.assertTrue(encBytes2.length == (data2.length / encGroupLen) * encGroupLen + paddingLen);
                Assert.assertArrayEquals(data2, decBytes2);
    
                byte[] data3 = RandomUtils.nextBytes(keyLen * 2);
                byte[] encBytes3 = encryption.encrypt(data3, secretKey.getEncoded(), salt);
                byte[] decBytes3 = encryption.decrypt(encBytes3, secretKey.getEncoded(), salt);
                System.out.println("[" + keyLen + "]padding-3=" + alg + ",enc len=" + encBytes3.length);
                System.out.println("[" + keyLen + "]padding-3=" + alg + ",dec len=" + decBytes3.length);
                Assert.assertTrue(encBytes3.length == (data3.length / encGroupLen) * encGroupLen + paddingLen);
                Assert.assertArrayEquals(data3, decBytes3);
            }
        }
    }
    
    1. 在验证过程中,会发现NoPadding填充模式下会和其它填充模式的密文不同,非NoPadding下会多一个分组秘钥长度的密文;
    2. 在验证过程中,还会发现ECB模式不支持盐值,后面直接把这个逻辑放到开源源码中去了;

3.2 SM4加解密组合验证

  • SM4密算法的加解密逻辑在抽象后,其测试代码为:
    @Test
    public void testEncryptPadding()
    {
        int[] keyLenList = {128};
        String[] modes = {"ECB", "CBC", "CTR", "CFB"};
        String[] paddings = {"NoPadding", "PKCS5Padding"};
        //test1:sm4可以分段(分组)加密明文长度为n的明文数据,密文长度为(n/16+1)*16的倍数(除法取整)
        for (int len : keyLenList)
        {
            BaseSingleEncryption encryption = new Sm4Encryption();
            super.doCipher(encryption, len, paddings, modes);
        }
    }
    

3.3 3DES加解密组合验证

  • 3DES密算法的加解密逻辑在抽象后,其测试代码为:
    @Test
    public void testEncrypt()
    {
        int[] keyLenList = {192};
        String[] modes = {"ECB", "CBC", "CTR", "CFB"};
        String[] paddings = {"NoPadding", "PKCS5Padding"};
        //test1:分段(分组)加密明文长度为n的明文数据,密文长度为(n/encGroupLen+1)*encGroupLen的倍数(除法取整)
        for (int len : keyLenList)
        {
            BaseSingleEncryption encryption = new Des3Encryption();
            super.doCipher(encryption, len, 8, paddings, modes);
        }
    }
    
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐