微信AEAD_AES_256_GCM加密文件解密

3,441 阅读5分钟

谁适合看这篇文章

  • 找不到AEAD_AES_256_GCM 解密过程或者算法的小伙伴
  • 想了解一下AES加密算法的人
  • 其他感兴趣的小伙伴
  • 了解文件加解密的大概过程

一、前言

目前在对接微信的微信硬件平台( iot.weixin.qq.com ),可以从微信发送一个文件给到绑定的设备上。

大致流程: 微信绑定设备 →  在微信中发送文件到设备 → 微信会回调用户配置的回调地址 →  接收端将文件下载发给用户

我已经跑完绑定流程,现在想试试把文件发给用户试试看,回调我们服务器了,结果文件无法打开。 发送内容为一个excel,内容见: 22.png

下面是微信返回回调传过来文件相关信息:

"type": "xlsx", "download_url": "https://mmae.qpic.cn/204/20303/stodownload?filekey=30340201010420301e020200cc040253480410fdbdfa68107f86ef77746addde017c1e0202200c040d00000004627466730000000131&hy=SH&storeid=32303232303430323135343330363030306139376539306232356232373734313337623030623030303030306363&bizid=1023", 
"iv_base64": "e3fXzamUIuath/Hp", 
"encrypt_algo": "AEAD_AES_256_GCM", 
"name": "2222222.xlsx", 
"tag_base64": "KtCd2YFsoG92GcW5YT1utg==", 
"key_base64": "58Doa0yn2xRiHiy5TFk4p42iR8JmYNCAZ8oxAqh8erE="

直接通过download_url下载,下载下来是一个“stodownload”的文件,加上excel的后缀名,无法正常打开。

从返回的报文可以看出,微信返回的信息中,有提示文件使用了AEAD_AES_256_GCM算法进行加密,我们接下来看看先AES相关的知识。

AEAD_AES_256_GCM : AES算法的GCM认证加密模式,key的长度为256bit。

二、AES相关知识

1、对称加密

AES是对称加密的一种,即加密和解密使用相同的key。

2、对称加密的相关知识

  • 明文P(plainText):未经加密的数据
  • 密钥K(key):用来加密明文的密码。在对称加密算法中,加密与解密的密钥是相同的,由双方协商产生,绝不可以泄漏
  • 密文C(cipherText): 经过加密的数据
  • 加密函数E(encrypt):C = E(K, P),即将明文和密钥作为参数,传入加密函数中,就可以获得密文
  • 解密函数D(decrypt):P = D(K, C),即将密文和密钥作为参数,传入解密函数中,就可以获得明文

3、AES相关

分组(或者叫块) :AES是一种分组加密技术,分组加密就是把明文分成一组一组的,每组长度相等,每次加密一组数据,直到加密完整个明文。。在AES标准规范中,分组长度只能是128 bits,也就是每个分组为16个bytes

初始向量(IV,Initialization Vector) :它的作用和MD5的“加盐”有些类似,目的是防止同样的明文块,始终加密成同样的密文块。

密钥长度:AES支持的密钥长度可以是128 bits或256 bits。

假如使用的AEAD_AES_256_GCM进行加密,则key的长度必须是32位 
秘钥长度 = key.length * 8 
256 = 32 * 8, key长度必须是 32128 = 16 * 8, key长度必须是 16

4、AES-GCM

GCM是认证加密模式中的一种,GCM中的G就是指GMAC,C就是指CTR,能同时确保数据的保密性、完整性及真实性。

运算标示运算过程
Ek使用秘钥k对输入做对称加密运算
XOR异或运算
Mh将输入与秘钥h在有限域GF(2^128)上做乘法

大致流程可以见下方:

11.png

  • 通过IV作为初始偏移量, 保证相同的明文和相同秘钥加密的结果是不一样的;(微信穿过来的IV)
  • 最后对比发送端发过来的MAC值跟我们的解密过程生成的MAC值是否相关从而保证数据的完整性,不会被篡改。(微信传过来的tag)

三、加密和解密过程

333.png

四、实操

微信提供的参考文档:pay.weixin.qq.com/wiki/doc/ap… ,里面有demo,但是只有解密的部分。

下面给出完整的过程,为了验证功能,把相关的内容都写在同一个类里面了。

ps:为了方便调试了先手动把加密文件下载到了本地。

package com.seewo.station.common.util;

import java.io.*;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class AesUtil {

   static final int KEY_LENGTH_BYTE = 32;
   static final int TAG_LENGTH_BIT = 128;
   private final byte[] aesKey;

   public AesUtil(byte[] key) {
      if (key.length != KEY_LENGTH_BYTE) {
         throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节");
      }
      this.aesKey = key;
   }

   /**
    *
    * @param associatedData  加密的字节流
    * @param nonce             偏移量
    * @param ciphertext     校验的tag
    * @return            解密后的文件流
    */
   public byte[] decryptToString(byte[] associatedData, byte[] nonce, String ciphertext)
   throws GeneralSecurityException, IOException {
      try {
         Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
         SecretKeySpec key = new SecretKeySpec(aesKey, "AES");
         GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);
         //GCMParameterSpec spec = new GCMParameterSpec(ciphertext.length() * Byte.SIZE, nonce);

         cipher.init(Cipher.DECRYPT_MODE, key, spec);
         cipher.update(associatedData);
         return cipher.doFinal(Base64.getDecoder().decode(ciphertext));
      } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
         throw new IllegalStateException(e);
      } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
         throw new IllegalArgumentException(e);
      }
   }

   public static void  main(String[] arg) throws IOException, GeneralSecurityException {
      String downloadUrl = "https://mmae.qpic.cn/204/20303/stodownload?filekey=30340201010420301e020200cc040253480410fdbdfa68107f86ef77746addde017c1e0202200c040d00000004627466730000000131&hy=SH&storeid=32303232303430323135343330363030306139376539306232356232373734313337623030623030303030306363&bizid=1023";
      String tag_base64 = "KtCd2YFsoG92GcW5YT1utg==";
      String iv_base64 = "e3fXzamUIuath/Hp";
      String key_base64 = "58Doa0yn2xRiHiy5TFk4p42iR8JmYNCAZ8oxAqh8erE=";

      // 将key_base64 通过base64 解密
      byte[] key = Base64.getDecoder().decode(key_base64);
      byte[] iv = Base64.getDecoder().decode(iv_base64);
      AesUtil  a = new AesUtil(key);

      //将文件下载到本地, 我目前是直接手动将文件下载下来了,实际实现需要些下载文件相关的代码
      //DownloadFileUtil.downloadFile(downloadUrl);
      byte[] fileData  = a.getContent("/Users/Downloads/stodownload (12)");
      save2File("/Users/Downloads/demo4.xlsx", a.decryptToString(fileData, iv ,    tag_base64 ));
   }


   /**
    * 读取每个路径上的文件
    * @param filePath    文件路径
    * @return          文件字节流
    * @throws IOException
    */
   public byte[] getContent(String filePath) throws IOException {
      File file = new File(filePath);
      long fileSize = file.length();
      if (fileSize > Integer.MAX_VALUE) {
         System.out.println("file too big...");
         return null;
      }
      FileInputStream fi = new FileInputStream(file);
      byte[] buffer = new byte[(int) fileSize];
      int offset = 0;
      int numRead = 0;
      while (offset < buffer.length
            && (numRead = fi.read(buffer, offset, buffer.length - offset)) >= 0) {
         offset += numRead;
      }
      // 确保所有数据均被读取
      if (offset != buffer.length) {
         throw new IOException("Could not completely read file "
               + file.getName());
      }
      fi.close();
      return buffer;
   }

   /**
    * 将字节流保存成文件
    * @param filename 文件名
    * @param msg     文件数据
    * @return
    */
   public static boolean save2File(String filename, byte[] msg){
      OutputStream fos = null;
      try{
         File file = new File(filename);
         File parent = file.getParentFile();
         boolean bool;
         if ((!parent.exists()) &&
               (!parent.mkdirs())) {
            return false;
         }
         fos = new FileOutputStream(file);
         fos.write(msg);
         fos.flush();
         return true;
      }catch (FileNotFoundException e){
         return false;
      }catch (IOException e){
         File parent;
         return false;
      }
      finally{
         if (fos != null) {
            try{
               fos.close();
            }catch (IOException e) {}
         }
      }
   }

}

跑完main函数,会发现,本地多了一个demo4.xlsx文件,打开跟我们上传的一样。

五、后记

从信息安全的角度,给第三方传输信息都需要进行加密,防止因为我们链接泄露,导致用户的私人数据被恶意盗用。 加密的返回不仅包括传输的信息,现有的加密算法也支持对文件进行加密。