You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
wx-dump-MeiFox/docs/decrypt.md

211 lines
9.5 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

## 微信SQLite数据库文件解密实现
- [微信SQLite数据库文件解密实现](#微信sqlite数据库文件解密实现)
- [SQLite页结构描述](#sqlite页结构描述)
- [SQLite页结构图](#sqlite页结构图)
- [解密实现过程](#解密实现过程)
- [源码地址](#源码地址)
#### SQLite页结构描述
微信`SQLite`数据库文件以每个页面4096字节的大小进行划分这些页面组成了文件的基本结构。每个页面内部包含了关键的元素包括盐值、加密后的内容、初始化向量`IV`)、哈希值(`hashMac`),以及用于保留的空字节。这个结构在整个数据库文件中起到了关键的组织和安全保障的作用。
首个页面是独特的,它包含了生成加密密钥所需的盐值、加密后的内容、初始化向量(`IV`)、哈希值(`hashMac`),以及用于保留的空字节。盐值在加密密钥生成过程中发挥着关键的作用,而哈希值则用于验证密钥的正确性,提供了额外的安全层。空字节的存在为未来可能的拓展或变化预留了空间。
随后的页面结构与首个页面相似,仍然包括了加密后的内容、初始化向量(`IV`)以及用于保留的空字节。这种一致性的结构确保了整个数据库文件的统一性和安全性,使得每个页面都能够独立存储经过加密的数据块,并在需要时通过初始化向量进行解密。这种巧妙的设计使得`SQLite`数据库文件在存储和保护数据方面表现出色。
#### SQLite页结构图
~~~mermaid
graph TD
A[SQLite Database File]
A -->|第一页| C1[4096字节]
C1 -->|盐值| C1A[16字节]
C1 -->|内容| C1C[4032字节]
C1 -->|IV| C1IV[16字节]
C1 -->|hashMac| C1H[20字节]
C1 -->|空字节| C1R[12字节]
A -->|第二页| C2[4096字节]
C2 -->|内容| C2C[4048字节]
C2 -->|IV| C2IV[16字节]
C2 -->|空字节| C2R[32字节]
A -->|第N页| C3[4096字节]
C3 -->|内容| C3C[4048字节]
C3 -->|IV| C3IV[16字节]
C3 -->|空字节| C3R[32字节]
~~~
#### 解密实现过程
`DecryptServiceImpl`类是一个解密服务的实现,专门用于解密`SQLite`数据库文件。首先,它定义了`SQLite`数据库文件的文件头、加密算法(`HmacSHA1`、页面大小4096字节、迭代次数64000次以及密钥长度32字节。主要功能由`wechatDecrypt`方法实现,该方法接收密码和解密业务对象作为参数。
在`wechatDecrypt`方法内部,首先通过`FileInputStream`读取数据库文件内容提取文件头、盐值、第一页信息包括内容、IV、`hashMac`和保留字段。随后,利用`PBKDF2`算法和用户提供的密码生成密钥,并根据提取的盐值和`hashMac`验证密钥的正确性。
如果密钥验证成功,便创建输出文件,写入`SQLite`文件头,并对第一页进行解密,写入解密后的内容和保留字段。接着,循环处理后续数据块,逐一解密并写入到输出文件。整个过程保证了对`SQLite`数据库文件的完整性和保密性的维护。
在解密过程中,`doDecrypt`方法使用`AES/CBC/NoPadding`模式对数据进行解密。为了处理大文件,`splitDataPages`方法将文件内容分割成多个页面进行逐一处理。
最后,通过`checkKey`方法检查密钥的有效性,确保解密过程中密钥的正确性。该类结合了多重密码学技术,保障了对`SQLite`数据库文件的高效且安全的解密操作。
```java
public class DecryptServiceImpl implements DecryptService {
/**
* SQLite数据库的文件头
*/
private static final String SQLITE_FILE_HEADER = "SQLite format 3\u0000";
/**
* 算法
*/
private static final String ALGORITHM = "HmacSHA1";
/**
* 一页的大小
*/
private static final int DEFAULT_PAGESIZE = 4096;
/**
* 迭代次数
*/
private static final int ITERATIONS = 64000;
/**
* Key长度
*/
private static final int HASH_KEY_LENGTH = 32;
@Override
public void wechatDecrypt(String password, DecryptBO decryptBO) {
// 创建File文件
File file = new File(decryptBO.getInput());
try (FileInputStream fis = new FileInputStream(file)) {
// 文件大小
byte[] fileContent = new byte[(int) file.length()];
// 读取内容
fis.read(fileContent);
// 提取盐值
byte[] salt = Arrays.copyOfRange(fileContent, 0, 16);
// 提取第一页
byte[] firstPage = Arrays.copyOfRange(fileContent, 16, DEFAULT_PAGESIZE);
// 提取第一页的内容与IV
byte[] firstPageBodyAndIv = Arrays.copyOfRange(firstPage, 0, firstPage.length - 32);
// 提取第一页的内容
byte[] firstPageBody = Arrays.copyOfRange(firstPage, 0, firstPage.length - 48);
// 提取第一页IV
byte[] firstPageIv = Arrays.copyOfRange(firstPage, firstPage.length - 48, firstPage.length - 32);
// 提取第一页的hashMac
byte[] firstPageHashMac = Arrays.copyOfRange(firstPage, firstPage.length - 32, firstPage.length - 12);
// 提取第一页的保留字段
byte[] firstPageReservedSegment = Arrays.copyOfRange(firstPage, firstPage.length - 48, firstPage.length);
// 生成key
byte[] key = Pbkdf2HmacUtil.pbkdf2Hmac(ALGORITHM, HexUtil.decodeHex(password), salt, ITERATIONS, HASH_KEY_LENGTH);
byte[] macSalt = new byte[salt.length];
for (int i = 0; i < salt.length; i++) {
macSalt[i] = (byte) (salt[i] ^ 58);
}
// 秘钥匹配成功
if (checkKey(key, macSalt, firstPageHashMac, firstPageBodyAndIv)) {
File outputFile = new File(decryptBO.getOutput());
File parentDir = outputFile.getParentFile();
// 检查父目录是否存在,如果不存在,则创建
if (!parentDir.exists()) {
parentDir.mkdirs();
}
// 解密并写入新文件
try (FileOutputStream deFile = new FileOutputStream(outputFile)) {
deFile.write(SQLITE_FILE_HEADER.getBytes());
deFile.write(doDecrypt(key, firstPageIv, firstPageBody));
deFile.write(firstPageReservedSegment);
// 解密后续数据块
for (byte[] page : splitDataPages(fileContent)) {
byte[] iv = Arrays.copyOfRange(page, page.length - 48, page.length - 32);
byte[] body = Arrays.copyOfRange(page, 0, page.length - 48);
byte[] reservedSegment = Arrays.copyOfRange(page, page.length - 48, page.length);
deFile.write(doDecrypt(key, iv, body));
deFile.write(reservedSegment);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 使用AES/CBC/NoPadding模式进行解密
*
* @param key 密钥
* @param iv 初始化向量
* @param input 待解密的数据
* @return 解密后的数据
* @throws GeneralSecurityException 抛出异常
*/
private byte[] doDecrypt(byte[] key, byte[] iv, byte[] input) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
return cipher.doFinal(input);
}
/**
* 将数据分割成多个页面
*
* @param fileContent 文件内容的字节数组
* @return 分割后的页面列表
*/
private List<byte[]> splitDataPages(byte[] fileContent) {
List<byte[]> pages = new ArrayList<>();
for (int i = DEFAULT_PAGESIZE; i < fileContent.length; i += DEFAULT_PAGESIZE) {
// 计算每个分割页面的结束位置
int end = Math.min(i + DEFAULT_PAGESIZE, fileContent.length);
byte[] slice = new byte[end - i];
// 将数据复制到新的页面中
System.arraycopy(fileContent, i, slice, 0, slice.length);
pages.add(slice);
}
return pages;
}
/**
* 检查密钥是否有效
*
* @param byteKey 密钥的字节数组
* @param macSalt MAC盐值
* @param hashMac 预期的MAC哈希值
* @param message 消息内容
* @return 如果密钥有效返回true否则返回false
* @throws Exception 抛出异常
*/
private boolean checkKey(byte[] byteKey, byte[] macSalt, byte[] hashMac, byte[] message) throws Exception {
// 使用PBKDF2算法生成MAC密钥
byte[] macKey = Pbkdf2HmacUtil.pbkdf2Hmac(ALGORITHM, byteKey, macSalt, 2, 32);
Mac mac = Mac.getInstance(ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(macKey, ALGORITHM);
mac.init(keySpec);
// 更新MAC计算的消息内容
mac.update(message);
// 添加额外的数据到消息中
mac.update(new byte[]{1, 0, 0, 0});
// 比较计算出的MAC值和预期的MAC值是否相同
return Arrays.equals(hashMac, mac.doFinal());
}
}
```
#### 源码地址
+ https://github.com/xuchengsheng/wx-dump-4j
+ `com.xcs.wx.service.impl.DecryptServiceImpl`