公司动态
QMCDecode 解码引擎实战拆解:从“打不开的 QMC 文件“到三套解密算法的完整落地
QMCDecode 解码引擎实战拆解从打不开的 QMC 文件到三套解密算法的完整落地【免费下载链接】QMCDecodeQQ音乐QMC格式转换为普通格式(qmcflac转flacqmc0,qmc3转mp3, mflac,mflac0等转flac)仅支持macOS可自动识别到QQ音乐下载目录默认转换结果存储到~/Music/QMCConvertOutput,可自定义需要转换的文件和输出路径项目地址: https://gitcode.com/gh_mirrors/qm/QMCDecodeQMCDecode 是一个专为 macOS 设计的 QQ 音乐加密格式转换工具核心能力是把 qmcflac、qmc0、qmc3、mflac、mgg 等 QMC 系列文件还原成标准 flac / mp3 / ogg 音频。本文不按项目简介的套路展开而是沿一条真实的问题链走一遍QMC 文件为什么普通播放器打不开解密到底要解决哪几个问题源码里每一层是怎么回答这些问题的读完你既能看懂原理也能照着把项目编译运行起来甚至自己扩展新格式。第一步先定义问题QMC 文件到底缺了什么一个 .qmcflac 文件表面看是一段音频拖进播放器却提示无法识别。原因在于 QQ 音乐客户端在把文件写入磁盘时做了两件事音频数据被异或加密原始字节流与一组由密钥生成的掩码逐字节异或数据面目全非解密密钥被藏进文件尾部密钥不是独立保存的而是以 Base64 字符串的形式追加在文件末尾并带上长度、分隔符等元信息。所以解码在工程上被拆成两个相对独立的子问题找到并还原密钥QMCKeyDecoder 负责以及用密钥对音频字节做逆运算QMCipher 协议下的三套实现负责。先解决哪个源码给出的答案是先找密钥——因为后面的解密算法是按密钥类型分发的密钥长什么样直接决定了用哪套算法。密钥藏在哪里文件尾巴里的寻宝逻辑打开QMDecoder.swift会发现整个解码流程的入口不是读文件头而是searchKey()——它先 seek 到文件末尾读取最后 4 个字节做判断。这里存在两个截然不同的藏法取决于文件是哪个端下载的来源端文件尾标记密钥长度读取方式真实音频长度计算移动端4 字节字符串QTag向前 4 字节按大端读 UInt32文件总长 - keySize - 8PC / macOS 端4 字节小端长度值按小端读 UInt32长度 0x300时文件总长 - keySize - 4否则视为无独立密钥对应的判断代码非常直白// 移动端下载的以 QTag 结尾 if String(bytes: lastFourBytes, encoding: .utf8) QTag { try fileHandle.seek(toOffset: UInt64(self.originFileLength - 8)) let keySize sizeBuffer.withUnsafeBytes { $0.load(as: UInt32.self).bigEndian } self.realAudioSize self.originFileLength - Int(keySize) - 8 // 从 realAudioSize 位置开始读 keykey 以逗号(ASCII 44)结尾 } else { let keySize lastFourBytes.withUnsafeBytes { $0.load(as: UInt32.self).littleEndian } if keySize 0x300 { self.realAudioSize self.originFileLength - Int(keySize) - 4 } else { // 用固定密钥解密整段音频 self.cipher try QMStaticCipher(originKey: privateKey256) } }这个分支里藏着两个值得留意的设计移动端密钥以逗号结尾。searchKey()用rawKey.firstIndex(of: 44)找到,的位置只取它之前的字节作为有效密钥避免把尾部填充数据也当成密钥。keySize 0x300时根本没有独立密钥。这类文件整个音频是用内置的 256 字节固定密钥privateKey256异或的直接走QMStaticCipher连找密钥这一步都省了。这解释了为什么有些 QMC 文件不需要任何 key 解析就能转换。拿到原始 key 字节后真正费劲的部分才开始它只是一串 Base64 文本必须先经过 TEA 解密还原成真正的密钥。密钥还原流水线TEA 算法与一次错位穿插QMCKeyDecoder.deriveKey(_:)把Base64 文本 → 可用密钥的过程实现得非常紧凑大致是四步Base64 解码并校验长度不小于 16 字节用种子106生成 8 字节的simpleKey对tan(seed index * 0.1)取绝对值再乘 100 取整把simpleKey与 Base64 解码结果的前 8 字节交错拼接成 16 字节的 TEA 密钥偶数位放 simpleKey奇数位放解码数据对剩余的base64DecodedKey[8...]调用decryptTencentTea做 CBC 模式 TEA 解密最终拼回前8字节 解密结果。let simpleKey simpleMakeKey(seed: 106, length: 8) var teaKey UInt8 for index in 0..8 { teaKey[index 1] simpleKey[index] // 偶数位simpleKey teaKey[(index 1) 1] base64DecodedKey[index] // 奇数位解码数据 } let inBuffer UInt8 let subBuffer try decryptTencentTea(inBuffer: inBuffer, key: teaKey) let newKey base64DecodedKey[0...7] subBuffer通俗理解 TEA 算法TEATiny Encryption Algorithm微型加密算法是 1994 年提出的小型分组密码特点是实现极简、性能极高非常适合嵌入在客户端做轻量加解密。它每次只处理 8 字节两个 32 位整数 v0、v1通过一个黄金分割常数delta 0x9e3779b9驱动多轮迭代每一轮让 v0、v1 相互搅动。打个比方TEA 就像两个人背靠背各拿半袋米每轮把对方的米量掺和自己的数字重新分配来回 32 次后原始配方就完全混进了新组合里。解密则是把同样的混合步骤倒着执行一遍。TeaCipher.swift里的decrypt正是这样做的var sum: UInt32 delta * (self.rounds / 2) for _ in 0..self.rounds / 2 { v1 v1 - (((v0 4) key2) ^ (v0 sum) ^ ((v0 5) key3)) v0 v0 - (((v1 4) key0) ^ (v1 sum) ^ ((v1 5) key1)) sum sum - delta }源码注释里特意强调这个算法整个在 32 位的框框内运行所以所有运算都用了-、这类溢出控制运算符确保 UInt32 环绕行为与 C 语言一致——这种细节如果不处理解密结果会整体错乱。另外decryptTencentTea还做了两处工程校验要求paddingLength saltLength 8并验证末尾zeroLength(7)个字节必须为零不满足就抛zeroCheckFailed。这些校验相当于解密后自检能提前发现 key 解析错误而不是把坏数据一路带到输出文件里。三套解密算法怎么选一次看密钥长度的分发密钥还原之后setCipher(_:)里有一行代码决定了后续用哪套算法let decodedKey try keyDecoder.deriveKey(keyBuffer) if decodedKey.count 300 { self.cipher try QMRC4Cipher(originKey: decodedKey) } else { self.cipher try QMMapCipher(originKey: decodedKey) }密钥超过 300 字节走 RC4 流加密否则走映射加密而固定密钥场景直接走静态加密。三套实现都遵循同一个QMCipher协议public protocol QMCipher { func qmDecrypt(data: Data, offset: Int) - Data init(originKey: [UInt8]) throws }offset参数是理解整套设计的钥匙它表示当前数据块在整段音频中的起始位置因为这三套算法的掩码都是位置相关的同样的字节在不同偏移处要用的掩码不同。这也是流式处理、分段解密的根基。静态加密QMStaticCipher一张查表卡掩码计算是(temp² 27) 0xFF作为索引去key里取值其中temp是偏移量对0x7FFF取模public func getMask(offset: Int) - UInt8 { let temp offset 0x7FFF ? (offset % 0x7FFF) : offset let index (temp * temp 27) 0xFF return key[index] }映射加密QMMapCipher查表后转个方向与静态加密几乎一样只差两个常数和一个旋转加数从27换成71214并且查出的字节要按index 0x7做循环移位let index (temp * temp 71_214) 0xFF return rotate(value: key[index], bits: index 0x7)旋转的实现也很取巧rotate (bits 4) % 8把左移与右移拼接成循环移位。可以理解为静态加密是原样对表映射加密是对完表再顺手转个角度两者对抗暴力的强度不同。RC4 流加密QMRC4Cipher最复杂也最现代的分支RC4 不是逐字节查表而是维护一个 256 字节的 S-box种子盒做密钥流生成。QMRC4Cipher的初始化会先用 KSA密钥调度算法打乱种子盒再基于密钥算出一个hashValue用于后续按段定位种子。真正的亮点在qmDecrypt的处理策略——它把数据切成三类片段分别处理片段类型长度处理方式首段0x80128 字节用getSegmentKey逐字节异或密钥常规段0x14005120 字节完整走一遍 S-box 置换 异或对齐碎片不满一段的零头用skipLength预跳步数后处理其中skipLength (offset % segmentSize) getSegmentKey(offset / segmentSize)是关键它让解密器能从任意偏移开始工作而不必从头重放整段密钥流——这正是流式处理大文件时随机访问能力的来源。func encodeAllSegment(data: inout [UInt8], offset: Int) { var newSeedBox UInt8 let skipLength (offset % segmentSize) self.getSegmentKey(index: offset / segmentSize) var left 0, right 0 for index in -skipLength..data.count { left (left 1) % self.originKeyLength right (Int(newSeedBox[left]) right) % self.originKeyLength (newSeedBox[right], newSeedBox[left]) (newSeedBox[left], newSeedBox[right]) if index 0 { let seedValue Int(newSeedBox[left]) Int(newSeedBox[right]) data[index] ^ newSeedBox[seedValue % self.originKeyLength] } } }把left/right两个指针在种子盒里来回交换、取和、取模就是 RC4 的 PRGA伪随机数生成阶段。负数索引的for循环只用来空转跳过skipLength步真正生效从index 0才开始设计相当精巧。一条完整链路qmcflac 文件是如何变成 flac 的把上面几层串起来一次转换的完整流水是读取文件尾部 → 判断 QTag / 长度分支 → 定位并截取 rawKey → Base64 解码 → TEA 交错拼 key → CBC 解密还原 → 拼出 decodedKey → 按 key 长度分发算法RC4 / Map / Static → 读取 realAudioSize 长度的音频 → qmDecrypt(data, offset: 0) → 按扩展名映射表换后缀 → 原子写入输出目录decryptAndWriteToFile()是这条链的收尾动作它从扩展名映射表encryptExtDictionary里查出目标后缀如 qmcflac → flac解密后直接用Data.write(options: .atomic)落盘。值得注意的细节是音频文件不需要整体读入内存因为 key 在文件尾realAudioSize一旦算出解密目标就锁定为前realAudioSize字节其余尾部元数据直接丢弃。界面与并行策略一个队列占一个 CPU 核心ViewController.swift的实现透露出作者明确的性能取向——用满 CPU。代码里按ProcessInfo().processorCount创建了与核心数等量的串行队列再用index % coreCount做轮询分发let coreCount ProcessInfo().processorCount for index in 0..dataSource.count { let queue queueArray[index % coreCount] queue.async { do { let decoder try QMDecoder(originFilePath: self.dataSource[index].path, outputDirectory: self.outputFolderURL.path) try decoder.decryptAndWriteToFile() self.progressAppend(index: index, success: true) } catch { self.progressAppend(index: index, success: false) } } }lazy 定义里那句尽量跑死 CPU的注释和qos: .utility的设定放在一起看很有意思utility 优先级意味着不抢占前台交互但又尽力并行。进度回调统一切回主线程累加计数完成后弹出成功 x 个失败 y 个的汇总对话框。整个界面默认逻辑也很贴心自动识别 QQ 音乐下载目录启动即扫描~/Library/Containers/com.tencent.QQMusicMac/Data/Library/Application Support/QQMusicMac/iQmc/下的加密文件并填入列表默认输出目录~/Music/QMCConvertOutput/不存在时自动创建支持自定义左侧选文件/文件夹右侧选输出路径。一张表看懂全部支持格式别忘了十六进制扩展名格式支持全部集中在Constants.swift的encryptExtDictionary中除了常见的扩展名还隐藏着一批十六进制字符串键——666c6163就是 ASCII 字符flac的十六进制编码6d7033对应mp36f6767对应ogg6d3461对应m4a776176对应wav。这类文件扩展名本身就是一串数字字符串解密时按字典键匹配即可。输入扩展名输出格式加密版本qmcflacflacv2qmcoggoggv2mflac / mflac0flacv2mgg / mgg1oggv2qmc0 / qmc3mp3v1qmc2oggv1bkcmp3mp3v1bkcflacflacv1tkmm4av1666c6163flacv16d7033mp3v16f6767oggv16d3461m4av1776176wavv1v2 版本通常携带更长密钥走 RC4v1 版本密钥较短走映射加密这与前面密钥长度 300 用 RC4的分发规则是对得上的。从源码到可运行macOS 下的编译清单项目仅支持 macOS依赖 Xcode 自带的 Cocoa / Foundation无第三方库编译门槛很低git clone https://gitcode.com/gh_mirrors/qm/QMCDecode cd QMCDecode open QMCDecode.xcodeproj进入 Xcode 后注意三点签名在 Signing Capabilities 里选择个人开发者证书或关闭签名用本地调试目标平台确认 Deployment Target 与你的 macOS 版本匹配运行Cmd B编译Cmd R启动应用首次运行时若提示沙盒权限问题检查QMCDecode.entitlements中的文件访问设置。编译产物是一个原生 App打开后会自动加载 QQ 音乐缓存目录直接点 Start 即可批量转换。常见问题排查清单现象可能原因处理建议列表为空QQ 音乐缓存目录变更或沙盒容器路径不同用Choose File手动选择包含 QMC 文件的目录提示扩展名不支持新格式尚未注册进encryptExtDictionary检查文件扩展名是否在支持表中转换失败且报zeroCheckFailedrawKey 截取位置不对或文件被截断确认文件完整重新下载后再试输出路径无法写入沙盒权限不足自定义输出目录到有写权限的位置转换成功但标签/封面缺失QMC 容器不携带完整元数据用 kid3 等工具批量补写 TITLE、ARTIST 等标签二次开发接入一个自定义加密格式的套路如果遇到不在表中的新格式扩展路径非常清晰三步即可1. 在Constants.swift注册映射把新扩展名指向目标后缀encryptExtDictionary[newfmt] ExtensionAndVersion(ext: flac, version: .v2)2. 若密钥机制不同新增一个遵循QMCipher的类实现init(originKey:)和qmDecrypt(data:offset:)两个方法即可协议之外的细节全部不用关心。3. 在QMDecoder的密钥分发处接入新算法让更长/更短的密钥路由到新实现// 伪代码在 setCipher 中按需增加分支 if isNewFormat { cipher try NewFormatCipher(originKey: decodedKey) } else if decodedKey.count 300 { cipher try QMRC4Cipher(originKey: decodedKey) } else { cipher try QMMapCipher(originKey: decodedKey) }整套架构把密钥提取QMDecoder与密钥还原QMCKeyDecoder和数据解密QMCipher 实现彻底解耦新格式最坏情况也只是多写一个 Cipher 类不需要动界面层。写在最后回头看这条问题链QMC 文件为什么打不开 → 密钥藏在文件尾 → 密钥需经 TEA 还原 → 按密钥长度分发三种解密算法 → 多队列并行落地每一步都是对上一步问题的直接回答没有多余的抽象也没有花哨的架构——这恰恰是它值得读源码的原因一个个人工具型项目把定位 key、还原 key、按 key 解密三件事做得干净利落还在TeaCipher的溢出处理、RC4 的分段跳步、按核心数开队列这些细节上体现了工程上的克制。作为二次开发的起点它同样合格核心解码逻辑与界面完全分离QMCipher协议、ExtensionAndVersion映射、QMDecoder的流程编排都可以被其他 macOS 应用直接复用。未来若想继续演进可以考虑把解码核心抽成 Swift Package 供命令行工具调用、补充完整的单元测试覆盖三套算法的边界偏移或者加入输出格式的元数据重写功能——这些方向在现有架构下都不需要伤筋动骨。使用提醒QMCDecode 仅供个人合法获取的音频文件转换与研究学习使用请遵守相关版权法规尊重创作者权益。【免费下载链接】QMCDecodeQQ音乐QMC格式转换为普通格式(qmcflac转flacqmc0,qmc3转mp3, mflac,mflac0等转flac)仅支持macOS可自动识别到QQ音乐下载目录默认转换结果存储到~/Music/QMCConvertOutput,可自定义需要转换的文件和输出路径项目地址: https://gitcode.com/gh_mirrors/qm/QMCDecode创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考