Verification implementation of open source AES/SM4/3DES symmetric encryption algorithm

Verification implementation of AES/SM4/3DES symmetric encryption algorithm

  • In the open source encryption component, the introduction and implementation of AES/SM4/3DES symmetric key encryption are introduced, but the conclusion of the summary is not explained. Now, from the excerpted unit test class, do a full round of verification instructions on symmetric encryption, so as to deepen everyone’s understanding of symmetric encryption;

  • All the codes listed in this article can be obtained from the unit test class of the bq-encryptor component open source code;

  • Encryption component introduction method:

    <dependency>
        <groupId>com.biuqu</groupId>
        <artifactId>bq-encryptor</artifactId>
        <version>1.0.1</version>
    </dependency>
    
  • The github address of the bq-encryptor component: https://github.com/woolllay/bq-encryptor

1. Symmetric encryption and decryption verification design

  • In the introduction and implementation of AES/SM4/3DES symmetric key encryption, the secret key length and packet length of symmetric encryption are introduced, so it needs to be verified. Under different key lengths, is the corresponding key length fixed?
  • Symmetric encryption has multiple working modes and padding algorithms, will there be any difference when they are combined;
  • The symmetric encryption algorithm supports the salt value (encryption offset vector), whether the salt value has any effect on the encryption of the above combination;

2. Verification of generated secret key

2.1 Verification of AES generated secret key

2.1.1 Generate binary verification from secret key object SecretKey

  • AES encryption algorithm (including AesEncryption and AesSecureEncryption, the difference between the two is that the former does not have a salt value, and the latter has a salt value, but it does not affect the generation of the secret key, so it is not one by one Listed), the key length supports 128/192/256 (there is no AES encryption algorithm of 512 at present, you can read Wikipedia yourself)
    , it is said that the length of the generated secret key is fixed, such as 128bit->16byte, 192bit->24byte, 256bit->32byte;
  • In the same way, it can be assumed that SM4/3DES is the same;
  • Based on the above analysis, the abstract unit test base class of symmetric encryption (BaseSingleEncryptionTest) can be extracted, and its secret key generation judgment logic is as follows:
    //test1: Use any initial value to create a secret key, secret key Always a fixed length (same length as the encryption algorithm) (except 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);
    
  • The complete secret key generation unit test method in the AES unit test class is:
    @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 Construct SecretKey key object verification from binary

  • When we use encryption and decryption, we usually save the secret key data (usually in hexadecimal or file storage), but in the process of using the Java language, only the secret key object is recognized, so the secret key data must be reversed into a secret key object;
  • What kind of key length data can be converted into a legal key object?
  • Same as the previous section, the verification code in the abstract class BaseSingleEncryptionTest is as follows:
    public void toKey(BaseSingleEncryption encryption, int encryptLen)
    {<!-- -->
        //test1: You can use any length value to get the secret key object (only the secret key can be generated), but only legal length can be encrypted
        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: An error will be reported if the key length is higher than the legal key length
            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: An error will be reported if the key length is lower than the legal key length
            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);
        }
    }
    
  • The verification code in the AES unit test class is as follows:
    @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. In this verification, it can be known from the exception: when the length of the secret key is not a legal value, the following exception will be thrown:
      Key length not 128/192/256 bits., which also indirectly shows that the maximum key length is only 256 bits;
    2. In the verification process, it can actually be found that no matter what length the key data is reversed into a key object, no error will be reported, but an error will be reported when using the corresponding encryption and decryption method;

2.2 Verification of the secret key generated by SM4

2.2.1 Generate binary verification from the secret key object SecretKey

  • Same as the unit test implementation of the AES encryption algorithm, only need to define the test method:
    @Test
    public void createKey()
    {<!-- -->
        BaseSingleEncryption encryption = new Sm4Encryption();
        super.createKey(encryption, 128);
    }
    
  • The verification effect is also the same as the unit test effect of the AES encryption algorithm;

2.2.2 Construct SecretKey key object verification from binary

  • Same as the unit test implementation of the AES encryption algorithm, only need to define the test method:
    @Test
    public void toKey()
    {<!-- -->
        BaseSingleEncryption encryption = new Sm4Encryption();
        super.toKey(encryption, 16);
    }
    
  • The verification effect is also the same as the unit test effect of the AES encryption algorithm;

2.3 3DES generated key verification

2.3.1 Generate binary verification from the secret key object SecretKey

  • The 3DES encryption algorithm is different from the above two standard and safe encryption algorithms. It is a 3-overlapping addition of the DES algorithm, and the length of each key is 64bit (8byte), so the total key length must not be less than 192bit (24byte) , after sharing the BaseSingleEncryptionTest abstract class, its unit test code is as follows:
    @Test
    public void createKey() throws NoSuchAlgorithmException
    {<!-- -->
        BaseSingleEncryption encryption = new Des3Encryption();
        try
        {<!-- -->
            //3DES does not allow keys lower than 24byte, because three 8byte DES keys cannot be parsed
            super. createKey(encryption, 192);
        }
        catch (Exception e)
        {<!-- -->
            e.printStackTrace();
        }
    }
    
  • When the secret key is less than 192bit (24byte), the following exception will be thrown: Caused by: java.security.InvalidKeyException: Wrong key size

2.3.2 Construct SecretKey key object verification from binary

  • Since the secret key of 3DES encryption is a combination, it is quite special, and the abstract detection logic of benefits cannot be reused, so it is designed separately. The code is as follows:
    @Test
    public void testGetKey()
    {<!-- -->
        BaseSingleEncryption encryption = new Des3Encryption();
    
        byte[] keyBytes = RandomUtils. nextBytes(24);
    
        //test1: Any 24byte content can be used as a 3DES key
        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: The binary of the secret key object is not the same as the binary of the original secret key
        Assert.assertFalse(Hex.toHexString(secretKey.getEncoded()).equals(Hex.toHexString(keyBytes)));
    
        byte[] keyBytes2 = RandomUtils. nextBytes(25);
        //test3: Any content larger than 24bytes can be used as a 3DES key, and only the first 24bytes will be intercepted
        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())));
    }
    
  • From the above-mentioned use cases that can pass the test, it can be seen that only the first 192bit (24byte) of the 3DES key is valid, and the key that is too long is the same as the key that intercepted its 192bit;

3. Combination verification of working mode and filling mode

3.1 AES encryption and decryption combination verification

  • After the encryption and decryption logic of the AES encryption algorithm is abstracted, the test code is:
    @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);
        }
    }
    
  • The encryption and decryption verification logic of the abstract class BaseSingleEncryptionTest is as follows:
    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: Segmentation (grouping) encrypts plaintext data with a plaintext length of n. When there is padding, the ciphertext length is a multiple of (n/encGroupLen + 1)*encGroupLen (rounded by division), and when there is no padding, it is (n/ multiple of encGroupLen)*encGroupLen (division is rounded, and n must be a multiple of 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. During the verification process, it will be found that NoPadding padding mode will be different from other padding mode ciphertexts, and there will be an additional group key under non-NoPadding > length of the ciphertext;
    2. During the verification process, it will also be found that the ECB mode does not support the salt value, and this logic will be directly put into the open source source code later;

3.2 SM4 encryption and decryption combination verification

  • After the encryption and decryption logic of the SM4 encryption algorithm is abstracted, the test code is:
    @Test
    public void testEncryptPadding()
    {<!-- -->
        int[] keyLenList = {<!-- -->128};
        String[] modes = {<!-- -->"ECB", "CBC", "CTR", "CFB"};
        String[] paddings = {<!-- -->"NoPadding", "PKCS5Padding"};
        //test1:sm4 can segment (group) encrypt plaintext data with a plaintext length of n, and the ciphertext length is a multiple of (n/16 + 1)*16 (rounded by division)
        for (int len : keyLenList)
        {<!-- -->
            BaseSingleEncryption encryption = new Sm4Encryption();
            super.doCipher(encryption, len, paddings, modes);
        }
    }
    

3.3 3DES encryption and decryption combination verification

  • After abstracting the encryption and decryption logic of the 3DES encryption algorithm, the test code is:
    @Test
    public void testEncrypt()
    {<!-- -->
        int[] keyLenList = {<!-- -->192};
        String[] modes = {<!-- -->"ECB", "CBC", "CTR", "CFB"};
        String[] paddings = {<!-- -->"NoPadding", "PKCS5Padding"};
        //test1: Segmentation (grouping) encrypts plaintext data with a plaintext length of n, and the ciphertext length is a multiple of (n/encGroupLen + 1)*encGroupLen (rounded up by division)
        for (int len : keyLenList)
        {<!-- -->
            BaseSingleEncryption encryption = new Des3Encryption();
            super.doCipher(encryption, len, 8, paddings, modes);
        }
    }