1.概述
Android QQ的聊天记录存储于/data/data/com.tencent.mobileqq/databases
目录下,其中QQ号.db
文件即为该QQ号的聊天记录数据库,获得该文件即有机会调取出相应的聊天记录。QQ数据库是不加密的,但是表里面的关键内容字段是加密的,需要破解。
破解教程:https://github.com/xxxyanchenxxx/QqDecrpt
https://github.com/Yiyiyimu/QQ-History-Backup
本机登录的QQ号查找方法:/shared_prefs/qb_info.xml,如下图所示:
项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用
Github地址:https://github.com/plasticene/plasticene-boot-starter-parent
Gitee地址:https://gitee.com/plasticene3/plasticene-boot-starter-parent
微信公众号:Shepherd进阶笔记
接下来我们进入今天主题:基于Android解析QQ数据库
2.数据库表
目前主要涉及的表有:conversation_info(会话信息表),Friends(好友表),TroopInfoV2(群聊表),TroopMemberInfo(群成员信息表),TroopMemberCardInfo(群成员信息表),DynamicAvatar(头像表)。
好友会话消息记录表:mr_friend_***_New
:其中的”*“是好友的QQ号uin的MD5加密32位字符串。
eg:mr_friend_3DAEEB4D801A88F08AF16930B5DB7C60_New
群会话消息记录表:mr_troop_***_New
: 其中的”*“是群组的群号uin的MD5加密32位字符串。
eg:mr_troop_62CE79A16BB9C8F8193BF1B043B05D82_New
以上数据库表分析不做截图数据展示,因为数据库全是乱码,需要解密
会话表:conversation_info
会话表有存入所有的会话uin,目前qq的无用会话非常多,我们只需要关注type=0(好友会话)和type=1(群会话),但是会话表没有会话时间,所以没办法根据时间来判断聊天会话是否改变,基础查询sql如下
select type, uin from conversation_info where type=0 or type = 1
在我们拿到uin之后,就可以遍历uin,按照上面所示uin的md5加密大写32字符串得到相应会话聊天记录表,这时候需要Android记录每张会话聊天记录表的上次上报聊天记录的最后时间
还有另外一种方式不需要借助会话表,那就是通过如下sql查询出所有的会话聊天记录表,再去和上面方式遍历
select * from sqlite_master where name like 'mr_friend_%New' or name like 'mr_troop_%New' ;
好友表:Friends
目前好友表存储中好友的qq号,昵称,备注,昵称全拼,时间字段等等有效信息,sql如下所示:
select uin, name, remark, mCompareSpell, datetime from Friends
同时未通过的添加好友信息也在该表中,目前来看和通过的好友区别是:datetime=0。还需要注意一个问题,在研究中发现,好友的信息改变了,但是datatime的时间不会立即变化,例如好友的头像换了或者备注改了,立刻拉取数据,会出现备注变成空的情况,估计需要经过一段时间qq后台数据库datetime字段更新了才能上报最新的好友信息
群聊表:TroopInfoV2
群聊表中包含了当前qq号,群qq号,群昵称,群备注,群主qq,时间等字段,sql如下所示:
select uin, troopuin, troopname, troopRemark, troopowneruin, timeSec from TroopInfoV2
查询陌生人信息:即同属于一个群但不是好友的关系的sql如下
select memberuin,troopuin, alias, autoremark, friendnick, datetime from TroopMemberInfo where memberuin not in(select uin from Friends)
消息记录表
qq的每个会话聊天记录都会新生成一张消息表,格式:mr_friend_md5(qq)_New(好友会话聊天记录), mr_troop_md5(qq)__New(群会话聊天记录,查询sql如下:
select selfuin, frienduin, senderuin, issend, msgUid, msgData, msgType from mr_friend_3DAEEB4D801A88F08AF16930B5DB7C60_New where msgtype=-1000 or msgType=-2000 or msgType=-2002
-1000:文本消息 -2000:图片 -2002:语音
这里的消息都是存在msgData这个字段里面,需要解密,msgData原生字段类型为blob,解密的方式:文本和图片,语音的解密方式是不一样的。
1) 文本解密:文本的解密方式是手机key和内容进行异或即可,代码示例如下:
public static String decryptString(String content) { if (StringUtils.isEmpty(content)) { return null; } if (!isPassInit) { genCodeKey(); } return decryptString(content, false); } private static String decryptString(String str, boolean z) { char[] cArr = null; if (str == null) { return null; } try { if (z) { cArr = StringUtil.reflactCharArray(str); } if (cArr == null) { cArr = new char[str.length()]; z = false; } for (int i = 0; i < str.length(); i++) { cArr[i] = (char) (str.charAt(i) ^ codeKey[i % codeKeyLen]); } if (cArr.length == 0) { return ""; } if (z) { return str; } return StringUtil.newStringWithData(cArr); } catch (Throwable th) { return ""; } }
2)图片,语音解密相对就比较复杂,qq使用了Google的Protobuf,一种平台无关,语言无关的结构化数据编解码协议,详细了解请自行百度,简单来说需要根据具体的使用场景编写特定的.proto文件,然后再通过客户端Protobuf.exe生成相应配置文件的代码工具类,在解析的工程中加入该工具类代码,进行解析。由于解析代码工具类过长,这里提供代码下载RichMsg
解析案例:
public static void testMsg(Connection connection) throws SQLException, InvalidProtocolBufferException {
Statement statement = connection.createStatement();
statement.setQueryTimeout(30); // set timeout to 30 sec.
// 执行查询语句
ResultSet rs = statement.executeQuery("select extStr,selfuin, frienduin, senderuin, issend, msgUid, msgData from mr_friend_3DAEEB4D801A88F08AF16930B5DB7C60_New where msgtype=-2000");
while (rs.next()) {
byte[] msgData = QqDecryptUtil.decryptBytes(rs.getBytes("msgData"));
RichMsg.PicRec picRec = RichMsg.PicRec.parseFrom(msgData);
String url = "chatimg:" + picRec.getMd5();
long value = CRC64FromString(url);
String filename = "";
if (value < 0) {
long abs = Math.abs(value);
filename = "-"+Long.toHexString(abs);
} else {
filename = Long.toHexString(value);
}
filename = "Cache_" + filename.replace("0x", "");
System.out.println(picRec);
}
}
private static final long[] table = new long[256];
static {
for(int n = 0; n < 256; ++n) {
long crc = (long)n;
for(int k = 0; k < 8; ++k) {
if ((crc & 1L) != 0L) {
crc = crc >> 1 ^ -7661587058870466123L;
} else {
crc >>= 1;
}
}
table[n] = crc;
}
}
public static long CRC64FromString(String val) {
byte[] s = val.getBytes();
long v = -1;
for (int i = 0; i < val.length(); i++) {
int value = s[i];
v = table[(value ^ ((int) v)) & 255] ^ v >> 8;
}
return v;
}