ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

QMCDecode解码引擎深度拆解:从一枚加密文件到标准音频的完整还原之旅

QMCDecode解码引擎深度拆解:从一枚加密文件到标准音频的完整还原之旅 QMCDecode解码引擎深度拆解从一枚加密文件到标准音频的完整还原之旅【免费下载链接】QMCDecodeQQ音乐QMC格式转换为普通格式(qmcflac转flacqmc0,qmc3转mp3, mflac,mflac0等转flac)仅支持macOS可自动识别到QQ音乐下载目录默认转换结果存储到~/Music/QMCConvertOutput,可自定义需要转换的文件和输出路径项目地址: https://gitcode.com/gh_mirrors/qm/QMCDecodeQMCDecode 是一款专注于 macOS 平台的 QQ 音乐 QMC 格式转换工具核心能力是把.qmcflac、.qmc0、.mflac等加密音频还原为.flac、.mp3、.ogg等标准格式。与同类工具最大的不同在于它的解密路径里藏着两代加密协议的兼容处理、一套 TEA 密钥派生流程和三种风格迥异的流式解密算法——本文不打算停留在怎么用而是跟着一枚文件从落盘到还原成可播放音频的真实旅程逐层拆开 QMCDecode 的解码引擎架构。一张图看懂解码全流程整个转换可以拆成五个依次衔接的环节每一环都会决定下一环的输入文件落盘 │ ├─ 1. 按扩展名查词典判定加密版本与目标格式 │ ├─ 2. 读取文件尾部解析密钥封装区取出原始密钥 │ ├─ 3. 密钥派生Base64 → TEA-CBC 解密 → 最终密钥 │ ├─ 4. 依据最终密钥长度分派到静态/映射/RC4 三类解密器 │ └─ 5. 按解密器规则逐字节还原音频数据写盘有意思的地方在于密钥不是单独存放的而是内嵌在文件末尾。这让 QMCDecode 不需要联网、不需要额外配置文件拿到文件就能解也让它必须先解决怎么从文件里安全地挖出密钥这个问题。下面按这条主线逐步深入。第一站扩展名词典——一个字典同时承担两件职责打开 Constants.swift最先看到的是encryptExtDictionary。它同时记录了输入扩展名 → 输出扩展名和加密版本let encryptExtDictionary: [String: ExtensionAndVersion] [ mgg: ExtensionAndVersion(ext: ogg, version: .v2), mflac: ExtensionAndVersion(ext: flac, version: .v2), qmcflac: ExtensionAndVersion(ext: flac, version: .v2), qmc0: ExtensionAndVersion(ext: mp3, version: .v1), qmc3: ExtensionAndVersion(ext: mp3, version: .v1), 666c6163: ExtensionAndVersion(ext: flac, version: .v1), // ... ]注意最后几个奇怪的键666c6163是十六进制字符串解码后正是 ASCII 的 flac。这是一批老式文件扩展名本身就是十六进制编码的音频格式名。QMCDecode 的处理方式很直接把它当作普通扩展名注册进同一个字典解密完成后替换成对应的标准后缀。为什么版本号这么重要因为 v1 与 v2 对应着两种不同的密钥派生路线和不同的尾部封装协议先判定版本后面每一步才能走对分支。这个设计让格式扩展变得极轻量——新增一种格式往往只是往字典里加一行。第二站挖密钥——文件尾部的双协议封装区密钥封装区的解析逻辑集中在 QMDecoder.swift 的searchKey()。这里体现了项目最有价值的一个兼容性设计同一文件格式移动端与 PC/macOS 端的封装方式完全不同。// 移动端以 QTag 结尾长度字段用大端序 if String(bytes: lastFourBytes, encoding: .utf8) QTag { let keySize ... // bigEndian 读取 self.realAudioSize self.originFileLength - Int(keySize) - 8 // 从 realAudioSize 处读 rawKey以逗号作为 key 的结束标记 } else { // PC/macOS 端末尾 4 字节即 key 长度小端序 let keySize ... // littleEndian 读取 if keySize 0x300 { // key 位于文件末尾往前 keySize4 字节处 } else { // keySize 异常大说明是老版本静态加密用内置固定密钥 self.cipher try QMStaticCipher(originKey: privateKey256) } }两个分支揭示了三种真实存在的情况来源尾部标记长度序密钥位置移动端下载4 字节 QTag大端紧邻 QTag 前以逗号,结尾PC/macOS 端直接是长度值小端文件末尾前keySize字节老版本文件长度值 ≥ 0x300小端无密钥内置privateKey256直接解密这里有个很值得琢磨的细节长度值异常大就当作没有密钥。0x300768字节远大于正常密钥尺寸一旦读取到这种异常值说明这份文件根本没有内嵌密钥——它是早期静态加密时代的产物直接用内置的 256 字节固定密钥即可解密。这一判断避免了对文件结构做大量假设用一字节大小阈值就完成了新旧协议的自动切换堪称整个解码引擎里性价比最高的一个分支。顺带一提realAudioSize也在这一步被算出来音频数据 文件总长 − 密钥区长度 − 尾部标记长度。解密时只处理这段真实音频密钥区会被整体丢弃。第三站密钥派生——一场 TEA-CBC 手工解密拿到原始密钥后还不能直接用。它还要经过 QMCKeyDecoder.swift 的deriveKey()做一次脱壳。这一步是全项目数学含量最高的地方。3.1 Base64 解码与长度守卫原始 key 本质上是 Base64 编码的密文先解码成二进制并做长度校验——少于 16 字节直接抛错因为后续流程至少需要 8 字节 IV 加 8 字节密文块。3.2 用 tan 函数造出 TEA 密钥TEA 的 16 字节密钥是这样拼出来的先取一个简单密钥simpleMakeKey(seed: 106, length: 8)它的生成方式出人意料result[index] UInt8(fabs(tan(Double(seed) Double(index) * 0.1)) * 100.0)对用三角函数 tan 生成密钥字节。这种伪随机但可复现的生成方式不需要存储任何秘密算法本身即密钥——逆向方只要知道 seed106 就能重建同一串字节。随后把 simpleKey 与解密后密文的前 8 字节交错拼成 16 字节的 TEA 密钥teaKey[index 1] simpleKey[index] teaKey[(index 1) 1] base64DecodedKey[index]3.3 TEA 解密标准的 32 轮 手动 CBC 模式TeaCipher.swift 实现了教科书级的 TEATiny Encryption Algorithm64 轮加密对应 32 轮解密固定 delta0x9e3779b9大端读取 UInt32。源码里有一句注释格外醒目这个煞笔算法整个在 32 位的框框内运行——因为 Swift 的UInt32运算默认会溢出作者用、-、*这些溢出运算符强行模拟了 TEA 依赖的 32 位回绕行为。这是移植 C 系密码学时最容易踩的坑也是这版实现最需要留意的可读性细节。解密密钥区时decryptTencentTea并非一次性解完而是手动实现了 CBC 链式模式每一轮用上一块的密文ivPrevious与当前块的明文做异或后再继续解密中间还要跳过 2 字节 salt 和 7 字节 zero 校验区let paddingLength Int(tempBuffer[0] 0x7) let outputLength inBuffer.count - 1 - paddingLength - saltLength - zeroLength最后一步是校验解密结果的末尾 7 字节必须与ivPrevious对应位置完全相等否则说明密钥或数据有误抛出zeroCheckFailed。这一校验把解密成功从概率事件变成了可验证的确定事件——如果一个密钥解析错了会在这一刻被立刻拦截而不是产出损坏的音频文件。第四站分派——密钥长度决定解密算法最终密钥拿到后QMDecoder.swift 的setCipher()用一个非常朴素但有效的规则做算法分派if decodedKey.count 300 { self.cipher try QMRC4Cipher(originKey: decodedKey) } else { self.cipher try QMMapCipher(originKey: decodedKey) }密钥超过 300 字节走 RC4否则走映射解密。这个阈值不是拍脑袋定的——密钥长度直接反映了加密强度新版本文件的密钥普遍更长采用了更强的 RC4 流加密老版本密钥较短用映射表就够。一行判断两代算法各归其位。而QMStaticCipher不进这个分支它只在上文提到的老版本固定密钥场景被直接创建。三种解密器共同实现同一个协议public protocol QMCipher { func qmDecrypt(data: Data, offset: Int) - Data init(originKey: [UInt8]) throws }协议里有个关键的offset参数它代表当前数据在整个音频流中的绝对偏移。这让解密器天然支持分段解密——任意位置的数据块都能独立解密而不必从头开始。这正是 RC4 分段解密能够成立的前提。第五站静态与映射——一字节一字节的异或还原QMStaticCipher 和 QMMapCipher 的骨架完全一样对每个字节用一个随偏移变化的掩码做异或。public func getMask(offset: Int) - UInt8 { let temp offset 0x7FFF ? (offset % 0x7FFF) : offset let index (temp * temp 27) 0xFF return key[index] }掩码的生成依赖一个二次函数(offset² 常数) 0xFF得到查表下标。两个算法的差别只在两处算法二次函数常数查表后处理QMStaticCipher27直接取key[index]QMMapCipher71214对key[index]做循环移位rotate这里的0x7FFF取模很关键它把偏移量限定在一个 32768 字节的窗口内防止大文件偏移导致整数溢出同时让掩码序列在文件内部周期性重复——这既简化了计算也意味着解密任何局部区块都不需要知道完整文件长度。如果不这样设计会怎样如果直接对原始 offset 做平方运算一个几百 MB 的音频文件偏移值会轻松溢出 32 位整数掩码序列立刻错乱。周期化处理是这个实现能稳定处理大文件的前提。第六站RC4 分段解密——整个引擎最精巧的部分QMRC4Cipher 是全项目实现最复杂、也最值得反复研读的一环。它没有简单地对整个文件跑一遍标准 RC4而是把数据流切成三种不同规则的分段private let firstSegmentSize: Int 0x80 // 128 字节首段 private let segmentSize: Int 0x1400 // 5120 字节主段解密入口qmDecrypt(data:offset:)依次处理四段首段若 offset 0x80使用encodeFirstSegment掩码来自getSegmentKey——一种基于 hashValue 与种子派生出的偏移敏感密钥流对齐段若 offset 不是 5120 的整数倍先解到下一个对齐边界批量主段以 5120 字节为单位循环解密尾段剩余不足一个分段的字节。主段解密encodeAllSegment是标准 RC4 的 KSAPRGA 流程每段用同一份 seedBox 做密钥调度KSA再按段内偏移跳过指定数量的密钥流字节相当于丢弃 PRGA 的前skipLength个输出随后才开始真正的异或解密。let skipLength (offset % segmentSize) self.getSegmentKey(index: offset / segmentSize)这个skipLength是理解整个设计的关键它保证每一段都从密钥流的正确位置开始生成从而让按段独立解密与一次性整体解密产生完全一致的结果。而getSegmentKey内部用到了hashValue——一个在 init 时对密钥字节连乘得到的值它对每个文件都不同相当于给 RC4 加了一层文件级个性化。为什么要费这么大力气做分段原因很实际支持随机访问解密。对于动辄几十 MB 的 FLAC 文件分段意味着内存占用恒定不必把整个文件加载进来也意味着理论上可以只解密文件的一部分比如播放器拖动进度条时只解当前位置。这是对流式处理的一次认真尝试。第七站并发调度——按 CPU 核心数铺队列解密算法就绪后真正提升体验的是 ViewController.swift 里的并发设计。作者没有用一个串行队列依次处理而是做了一套按核心数轮询分发的调度let coreCount ProcessInfo().processorCount for index in 0..dataSource.count { let queue queueArray[index % coreCount] queue.async { ... } }queueArray里有多少队列ProcessInfo().processorCount个注释写得相当直白根据CPU物理核心数组装队列尽量跑死CPU。每个文件的解密任务按index % coreCount轮询分发给对应队列天然实现了负载均衡——文件数超过核心数时任务会均匀铺到每个核心上。进度汇报则统一回到主线程通过succeedCount errorCount累计计算百分比全部完成后弹出结果弹窗。这一并行干活、串行汇报的模式避免了 UI 线程被大量并发回调挤爆是批量转换场景的标准解法。解码器的性能特征与选型对比把四种解密路径放一起看它们的成本结构差异很大解密器单字节成本内存占用适用格式特点QMStaticCipher最低一次查表O(1)老版本固定密钥无密钥文件专用QMMapCipher低查表移位O(1)qmc0/qmc3 等 v1 格式掩码周期 0x7FFFQMRC4Cipher中KSAPRGAO(keyLength)mflac/mgg 等 v2 格式分段流式支持随机访问TEA 密钥派生一次性开销O(1)所有内嵌密钥文件每文件仅执行一次关键结论TEA 密钥派生只在文件开头执行一次均摊到几十 MB 的音频数据上几乎可以忽略真正的大头是逐字节异或与 RC4 的 PRGA 循环因此总吞吐量主要由 RC4 段的线性扫描速度决定。对于整批转换场景瓶颈往往在磁盘 IO 而非 CPU——这也解释了为什么作者放心地尽量跑死CPU。下一步动手跑通一个 Demo现在你对整条还原链路已经有了完整认知。想亲手验证这套解码引擎可以这样开始克隆并编译仓库地址https://gitcode.com/gh_mirrors/qm/QMCDecode用 Xcode 打开QMCDecode.xcodeprojCmd B即可构建macOS 10.13Xcode 11。喂数据运行后应用会自动扫描 QQ 音乐 macOS 客户端的下载目录~/Library/Containers/com.tencent.QQMusicMac/.../iQmc/也可以用Choose File手动选择任意目录。观察链路挑选一枚.qmcflac文件单步调试searchKey()→deriveKey()→qmDecrypt()你会亲眼看到密钥从文件尾部被挖出、经 TEA 脱壳、最终在掩码表里逐字节还原出 FLAC 头fLaC的过程——当输出文件头部出现可识别的格式魔数时整条链路就通了。如果你是冲着二次开发来的值得动手改造的三个方向往encryptExtDictionary里注册新的格式映射在QMCipher协议下新增自定义解密器并接入setCipher()的分派逻辑把QMDecoder从 App 里抽出来封装成命令行工具或库——核心代码与 UI 的耦合度很低这几处改动都不会伤筋动骨。解码引擎的每一个环节都留有余地这正是它作为学习样本和改造基座的真正价值。【免费下载链接】QMCDecodeQQ音乐QMC格式转换为普通格式(qmcflac转flacqmc0,qmc3转mp3, mflac,mflac0等转flac)仅支持macOS可自动识别到QQ音乐下载目录默认转换结果存储到~/Music/QMCConvertOutput,可自定义需要转换的文件和输出路径项目地址: https://gitcode.com/gh_mirrors/qm/QMCDecode创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表