AES加解密使用总结

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AES加解密使用总结相关的知识,希望对你有一定的参考价值。

参考技术A AES, 高级加密标准, 是采用区块加密的一种标准, 又称Rijndael加密法. 严格上来讲, AES和Rijndael又不是完全一样, AES的区块长度固定为128比特, 秘钥长度可以是128, 192或者256. Rijndael加密法可以支持更大范围的区块和密钥长度, Rijndael使用的密钥和区块长度均可以是128,192或256比特. AES是对称加密最流行的算法之一.

我们不去讨论具体的AES的实现, 因为其中要运用到大量的高等数学知识, 单纯的了解AES流程其实也没什么意义(没有数学基础难以理解), 所以我们今天着重来总结一些使用过程中的小点.

当然了分组密码的加密模式不仅仅是ECB和CBC这两种, 其他的我们暂不涉及.

上面说的AES是一种区块加密的标准, 那加密模式其实可以理解为处理不同区块的方式和联系.

ECB可以看做最简单的模式, 需要加密的数据按照区块的大小分为N个块, 并对每个块独立的进行加密

此种方法的缺点在于同样的明文块会被加密成相同的密文块, 因此, 在某些场合, 这种方法不能提供严格的数据保密性. 通过下面图示例子大家就很容易明白了

我们的项目中使用的就是这种模式, 在CBC模式中, 每个明文块与前一个块的加密结果进行异或后, 在进行加密, 所以每个块的加密都依赖前面块的加密结果的, 同时为了保证第一个块的加密, 在第一个块中需要引入初始化向量iv.

CBC是最常用的模式. 他的缺点是加密过程只能是串行的, 无法并行, 因为每个块的加密要依赖到前一个块的加密结果, 同时在加密的时候明文中的细微改变, 会导致后面所有的密文块都发生变化. 但此种模式也是有优点的, 在解密的过程中, 每个块的解密依赖上一个块的加密结果, 所以我们要解密一个块的时候, 只需要把他前面一个块也一起读取, 就可以完成本块的解密, 所以这个过程是可以并行操作的.

AES加密每个块blockSize是128比特, 那如果我们要加密的数据不是128比特的倍数, 就会存在最后一个分块不足128比特, 那这个块怎么处理, 就用到了填充模式. 下面是常用的填充模式.

PKCS7可用于填充的块大小为1-255比特, 填充方式也很容易理解, 使用需填充长度的数值paddingSize 所表示的ASCII码 paddingChar = chr(paddingSize)对数据进行冗余填充. (后面有解释)

PKCS5只能用来填充8字节的块

我们以AES(128)为例, 数据块长度为128比特, 16字节, 使用PKCS7填充时, 填充长度为1-16. 注意, 当加密长度是16整数倍时, 反而填充长度是最大的, 要填充16字节. 原因是 "PKCS7" 拆包时会按协议取最后一个字节所表征的数值长度作为数据填充长度, 如果因真实数据长度恰好为16的整数倍而不进行填充, 则拆包时会导致真实数据丢失.

举一个blockSize为8字节的例子

第二个块中不足8字节, 差4个字节, 所以用4个4来填充

严格来讲 PKCS5不能用于AES, 因为AES最小是128比特(16字节), 只有在使用DES此类blockSize为64比特算法时, 考虑使用PKCS5

我们的项目最开始加解密库使用了CryptoSwift, 后来发现有性能问题, 就改为使用IDZSwiftCommonCrypto.

这里咱们结合项目中边下边播边解密来提一个点, 具体的可以参考之前写的 边下边播的总结 . 因为播放器支持拖动, 所以我们在拖拽到一个点, 去网络拉取对应数据时, 应做好range的修正, 一般我们都会以range的start和end为基准, 向前后找到包含这个range的所有块范围. 打比方说我们需要的range时10-20, 这是我们应该修正range为0-31, 因为起点10在0-15中, 20 在16-31中. 这是常规的range修正.(第一步 找16倍数点).

但是在实际中, 我们请求一段数据时, 还涉及到解密器的初始化问题, 如果我们是请求的0-31的数据, 因为是从0开始, 所以我们的解密器只需要用key和初始的iv来进行初始化, 那如果经过了第一步的基本range修正后, 我们请求的数据不是从0开始, 那我们则还需要继续往前读取16个字节的数据, 举个例子, 经过第一步修正后的range为16-31, 那我们应该再往前读取16字节, 应该是要0-31 这32个字节数据, 拿到数据后,使用前16个字节(上一个块的密文)当做iv来初始化解密器.

还有一个要注意的点是, 数据解密的过程中, 还有可能会吞掉后面16个字节的数据, 我暂时没看源码, 不知道具体因为什么, 所以保险起见, 我们的range最好是再向后读取6个字节.

感谢阅读

参考资料

https://zh.wikipedia.org/zh-cn/%E9%AB%98%E7%BA%A7%E5%8A%A0%E5%AF%86%E6%A0%87%E5%87%86
https://segmentfault.com/a/1190000019793040
https://ithelp.ithome.com.tw/articles/10250386

Java使用AES加解密


Java使用AES加解密

目录

1.1生成密钥

1.2密钥的存储

1.3获取存储的密钥

1.4加解密

1.5使用存储的密钥进行加解密示例

 

AES是一种对称的加密算法,可基于相同的密钥进行加密和解密。Java采用AES算法进行加解密的逻辑大致如下:

1、生成/获取密钥

2、加/解密

 

1.1生成密钥

密钥的生成是通过KeyGenerator来生成的。通过获取一个KeyGenerator实例,然后调用其generateKey()方法即可生成一个SecretKey对象。大致逻辑一般如下:

 


  1. private SecretKey geneKey() throws Exception   
  2.     //获取一个密钥生成器实例  
  3.     KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM);  
  4.     SecureRandom random = new SecureRandom();  
  5.     random.setSeed("123456".getBytes());//设置加密用的种子,密钥  
  6.     keyGenerator.init(random);  
  7.     SecretKey secretKey = keyGenerator.generateKey();  
  8.     return secretKey;  
  9.   

 

 

上述生成密钥的过程中指定了固定的种子,每次生成出来的密钥都是一样的。还有一种形式,我们可以通过不指定SecureRandom对象的种子,即不调用其setSeed方法,这样每次生成出来的密钥都可能是不一样的。


  1. private SecretKey geneKey() throws Exception   
  2.     //获取一个密钥生成器实例  
  3.     KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM);  
  4.     SecureRandom random = new SecureRandom();  
  5.     keyGenerator.init(random);  
  6.     SecretKey secretKey = keyGenerator.generateKey();  
  7.     return secretKey;  
  8.   

 

 

通过KeyGenerator的init(keySize)方法进行初始化,而不是通过传递SecureRandom对象进行初始化也可以达到上面的效果,每次生成的密钥都可能是不一样的。但是对应的keySize的指定一定要正确,AES算法的keySize是128。


  1. private SecretKey geneKey() throws Exception   
  2.     //获取一个密钥生成器实例  
  3.     KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM);  
  4.     keyGenerator.init(128);  
  5.     SecretKey secretKey = keyGenerator.generateKey();  
  6.     return secretKey;  
  7.   

 

 

但是这种每次生成出来的密钥都是不同的情况下,我们需要把加密用的密钥存储起来,以供解密的时候使用,不然就没法进行解密了。

 

1.2密钥的存储

密钥SecretKey里面最核心的内容就是其中的密钥对应的字节数组,可以通过SecretKey的getEncoded()方法获取。然后把它存储起来即可。最简单的方式就是直接写入一个文件中。


  1. //把上面的密钥存起来  
  2. Path keyPath = Paths.get("D:/aes.key");  
  3. Files.write(keyPath, secretKey.getEncoded());  

 

 

1.3获取存储的密钥

获取存储的密钥的核心是把密钥的字节数组转换为对应的SecretKey。这可以通过SecretKeySpec来获取,其实现了SecretKey接口,然后构造参数里面将接收密钥的字节数组。


  1. private SecretKey readKey(Path keyPath) throws Exception   
  2.     //读取存起来的密钥  
  3.     byte[] keyBytes = Files.readAllBytes(keyPath);  
  4.     SecretKeySpec keySpec = new SecretKeySpec(keyBytes, ALGORITHM);  
  5.     return keySpec;  
  6.   

 

 

1.4加解密

Java采用AES算法进行加解密的过程是类似的,具体如下:

1、指定算法,获取一个Cipher实例对象


  1. Cipher cipher = Cipher.getInstance(ALGORITHM);//算法是AES  

 

2、生成/读取用于加解密的密钥


  1. SecretKey secretKey = this.geneKey();  

 

3、用指定的密钥初始化Cipher对象,同时指定加解密模式,是加密模式还是解密模式。


  1. cipher.init(Cipher.ENCRYPT_MODE, secretKey);  

 

4、通过update指定需要加密的内容,不可多次调用。


  1. cipher.update(content.getBytes());  

 

5、通过Cipher的dofinal()进行最终的加解密操作。


  1. byte[] result = cipher.doFinal();//加密后的字节数组  

 

通过以上几步就完成了使用AES算法进行加解密的操作了。其实第4、5步是可以合在一起的,即在进行doFinal的时候传递需要进行加解密的内容。但是如果update指定了加密的内容,而doFinal的时候也指定了加密的内容,那最终加密出来的结果将是两次指定的加密内容的和对应的加密结果。


  1. byte[] result = cipher.doFinal(content.getBytes());  

 

以下是一次加解密操作的完整示例。


  1. public class AESTest   
  2.   
  3.     private static final String ALGORITHM = "AES";  
  4.       
  5.     /** 
  6.      * 生成密钥 
  7.      * @return 
  8.      * @throws Exception 
  9.      */  
  10.     private SecretKey geneKey() throws Exception   
  11.         //获取一个密钥生成器实例  
  12.         KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM);  
  13.         SecureRandom random = new SecureRandom();  
  14.         random.setSeed("123456".getBytes());//设置加密用的种子,密钥  
  15.         keyGenerator.init(random);  
  16.         SecretKey secretKey = keyGenerator.generateKey();  
  17.         //把上面的密钥存起来  
  18.         Path keyPath = Paths.get("D:/aes.key");  
  19.         Files.write(keyPath, secretKey.getEncoded());  
  20.         return secretKey;  
  21.       
  22.       
  23.     /** 
  24.      * 读取存储的密钥 
  25.      * @param keyPath 
  26.      * @return 
  27.      * @throws Exception 
  28.      */  
  29.     private SecretKey readKey(Path keyPath) throws Exception   
  30.         //读取存起来的密钥  
  31.         byte[] keyBytes = Files.readAllBytes(keyPath);  
  32.         SecretKeySpec keySpec = new SecretKeySpec(keyBytes, ALGORITHM);  
  33.         return keySpec;  
  34.       
  35.       
  36.     /** 
  37.      * 加密测试 
  38.      */  
  39.     @Test  
  40.     public void testEncrypt() throws Exception   
  41.         //1、指定算法、获取Cipher对象  
  42.         Cipher cipher = Cipher.getInstance(ALGORITHM);//算法是AES  
  43.         //2、生成/读取用于加解密的密钥  
  44.         SecretKey secretKey = this.geneKey();  
  45.         //3、用指定的密钥初始化Cipher对象,指定是加密模式,还是解密模式  
  46.         cipher.init(Cipher.ENCRYPT_MODE, secretKey);  
  47.         String content = "Hello AES";//需要加密的内容  
  48.         //4、更新需要加密的内容  
  49.         cipher.update(content.getBytes());  
  50.         //5、进行最终的加解密操作  
  51.         byte[] result = cipher.doFinal();//加密后的字节数组  
  52.         //也可以把4、5步组合到一起,但是如果保留了4步,同时又是如下这样使用的话,加密的内容将是之前update传递的内容和doFinal传递的内容的和。  
  53. //      byte[] result = cipher.doFinal(content.getBytes());  
  54.         String base64Result = Base64.getEncoder().encodeToString(result);//对加密后的字节数组进行Base64编码  
  55.         System.out.println("Result: " + base64Result);  
  56.       
  57.       
  58.     /** 
  59.      * 解密测试 
  60.      */  
  61.     @Test  
  62.     public void testDecrpyt() throws Exception   
  63.         Cipher cipher = Cipher.getInstance(ALGORITHM);  
  64.         SecretKey secretKey = this.geneKey();  
  65.         cipher.init(Cipher.DECRYPT_MODE, secretKey);  
  66.         String content = "pK9Xw4zqTMXYraDadSGJE3x/ftrDxIg2AM/acq0uixA=";//经过Base64加密的待解密的内容  
  67.         byte[] encodedBytes = Base64.getDecoder().decode(content.getBytes());  
  68.         byte[] result = cipher.doFinal(encodedBytes);//对加密后的字节数组进行解密  
  69.         System.out.println("Result: " + new String(result));  
  70.       
  71.       
  72.   

 

1.5使用存储的密钥进行加解密示例


  1. @Test  
  2. public void test() throws Exception   
  3.     Cipher cipher = Cipher.getInstance(ALGORITHM);  
  4.     KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM);  
  5.     keyGenerator.init(128);  
  6.     SecretKey secretKey = keyGenerator.generateKey();  
  7.     //把上面的密钥存起来  
  8.     Path keyPath = Paths.get("D:/aes.key");  
  9.     Files.write(keyPath, secretKey.getEncoded());  
  10.       
  11.     //读取存起来的密钥  
  12.     SecretKey key = this.readKey(keyPath);  
  13.     cipher.init(Cipher.ENCRYPT_MODE, key);  
  14.     cipher.update("Hello World".getBytes());  
  15.     //密文  
  16.     byte[] encryptBytes = cipher.doFinal();  
  17.     System.out.println(Base64.getEncoder().encodeToString(encryptBytes));  
  18.       
  19.     //用取出来的密钥进行解密  
  20.     cipher.init(Cipher.DECRYPT_MODE, key);  
  21.     //明文  
  22.     byte[] decryptBytes = cipher.doFinal(encryptBytes);  
  23.     System.out.println(new String(decryptBytes));  
  24.   

 

在上面的示例中,我们先生成了一个密钥,然后把它保存到本地文件中,然后再把它读出来,分别用以加密和解密。而且我们加密和解密都是用的同一个Cipher对象,但是在使用前需要重新通过init方法初始化加解密模式。

 

(Elim写于2017年4月20日星期四)

 

 

以上是关于AES加解密使用总结的主要内容,如果未能解决你的问题,请参考以下文章

百付宝支付通道加解密和加签验签总结

PHP 加密:AES & RSA

数字证书的原理总结

李迟2021年10月知识总结

李迟2021年10月知识总结

Java使用AES加解密