3.Java开源AES/SM4/3DES对称加密算法的验证说明
1.基于Java的BouncyCastle开源库做了二次封装,已开源。支持AES(256/192/128)/SM4/3DES加密算法;2.支持上述秘钥长度与ECB/CBC/CTR/CFB工作模式以及NoPadding/PKCS5Padding填充算法的组合验证;
·
3.开源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加密算法(包括
AesEncryption
和AesSecureEncryption
,二者区别是前者不带盐值,后者带盐值,但是不影响秘钥生成,就不一一列举了),秘钥长度支持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); } }
- 在此次验证中,可根据异常得知:在秘钥长度不为合法值时,会抛出异常:
Key length not 128/192/256 bits.
,这也间接说明秘钥长度最大只有256位; - 在验证过程中,其实还可以发现无论什么长度的秘钥数据反转成秘钥对象,都不会报错,但是在使用对应的加解密方法时就会报错;
- 在此次验证中,可根据异常得知:在秘钥长度不为合法值时,会抛出异常:
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); } } }
- 在验证过程中,会发现
NoPadding
填充模式下会和其它填充模式的密文不同,非NoPadding
下会多一个分组秘钥
长度的密文; - 在验证过程中,还会发现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); } }
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
已为社区贡献3条内容
所有评论(0)