谁适合看这篇文章
- 找不到AEAD_AES_256_GCM 解密过程或者算法的小伙伴
- 想了解一下AES加密算法的人
- 其他感兴趣的小伙伴
- 了解文件加解密的大概过程
一、前言
目前在对接微信的微信硬件平台( iot.weixin.qq.com ),可以从微信发送一个文件给到绑定的设备上。
大致流程: 微信绑定设备 → 在微信中发送文件到设备 → 微信会回调用户配置的回调地址 → 接收端将文件下载发给用户
我已经跑完绑定流程,现在想试试把文件发给用户试试看,回调我们服务器了,结果文件无法打开。
发送内容为一个excel,内容见:
下面是微信返回回调传过来文件相关信息:
"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长度必须是 32 位
128 = 16 * 8, key长度必须是 16 位
4、AES-GCM
GCM是认证加密模式中的一种,GCM中的G就是指GMAC,C就是指CTR,能同时确保数据的保密性、完整性及真实性。
| 运算标示 | 运算过程 |
|---|---|
| Ek | 使用秘钥k对输入做对称加密运算 |
| XOR | 异或运算 |
| Mh | 将输入与秘钥h在有限域GF(2^128)上做乘法 |
大致流程可以见下方:
- 通过IV作为初始偏移量, 保证相同的明文和相同秘钥加密的结果是不一样的;(微信穿过来的IV)
- 最后对比发送端发过来的MAC值跟我们的解密过程生成的MAC值是否相关从而保证数据的完整性,不会被篡改。(微信传过来的tag)
三、加密和解密过程
四、实操
微信提供的参考文档: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文件,打开跟我们上传的一样。
五、后记
从信息安全的角度,给第三方传输信息都需要进行加密,防止因为我们链接泄露,导致用户的私人数据被恶意盗用。 加密的返回不仅包括传输的信息,现有的加密算法也支持对文件进行加密。