ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

macOS平台QMC格式解密工具:原理、Swift实现与批量转换

macOS平台QMC格式解密工具:原理、Swift实现与批量转换 简介该资源是一套专为macOS平台设计的QQ音乐QMC格式音频转换工具源码主要面向计算机、软件、电子信息等专业学生适合课程设计、毕业设计或作为Python/Swift项目实践的原型参考。项目可将qmcflac、mflac等格式转为FLAC将qmc0、qmc3转为MP3实现QQ音乐专属音频格式向通用格式的批量转换。资源包共43个文件约1.78MB以Swift源代码、Xcode工程配置pbxproj、plist、storyboard与项目文档为主并包含界面截图和演示动图便于理解解码流程与界面结构。完整工程包含QMCKeyDecoder、TeaCipher等核心解码模块以及应用界面、测试用例和备份文件可直接打开编译运行也可按需修改扩展。目前已有63人学习下载适合希望了解QMC解码原理或需要macOS音频处理工具源码的学习者参考使用。1. macOS平台QMC格式转换工具先搞懂解密思路再动手macOS平台上的QQ音乐QMC格式转换工具常被当成“玄学”源码丢进收藏夹。qmcflac转flac、qmc0/qmc3转mp3听起来是转码实际是解密QQ音乐把原生音频流用替换表和TEA加密后缀只是伪装。这套Xcode工程源码是一个适合毕设案例的完整参考QMCipher、QMCKeyDecoder、TeaCipher三个Swift文件把解密链路拆得很清楚。很多人第一反应是“这种工具该用Python写”但macOS桌面端用Swift做文件拖拽、沙盒权限和原生UI集成更省事也更容易看懂工程结构。它解决的是音频文件脱离播放器锁定、换到任意播放器都能播的问题适合做课程设计、学期末综合实践也适合想读原生代码的初学者——动态密钥那块确实需要耐心。2. QMC格式的加密结构静态密钥与动态TEA两条解密路径2.1 qmc0/qmc3与qmcflac后缀不同加密代际也不同QMC格式并不只有一套算法。工程里能看到qmc0、qmc3、qmcflac、mflac、mflac0五种后缀根源在于QQ音乐在不同时期换过加密方案而后缀名又与原始音频格式强绑定导致很多初学者一上来就被名字带偏。qmc0、qmc3属于早期的QMC1静态加密。所谓静态是指解密所需的替换表以常量形式写死在程序里。加密时把原始音频数据按128字节的替换表逐字节置换解密时拿同一张表再做一次反向置换位操作完全可逆所以网上很多老脚本几十行就能解。qmc3和qmc0的区别主要在原始编码qmc3多见于MP3文件qmc0也基本是MP3流派还有极少数伪装成其他格式的情况。qmcflac则是QMC2动态加密。解密表不固定而是先读取文件里加密的密钥用TEA解出后动态生成一张65536字节的置换表实际解密时按字节取值完成置换。因为表是每个文件单独生成的所以必须先从文件头拿到密钥才能继续解正文。mflac、mflac0是另一套命名分支代码处理时走的还是同一套动态解密入口只是后缀映射不同。区分代际最直接的办法是读文件头前两个字节。动态版本会把密钥长度写在这里静态版本没有这个概念长度位通常为0。工程里Constants.swift就集中处理这件事把后缀、原始格式、代际判断放在一处改一处就能全局生效。排查问题时最好先把这张表抄进笔记后缀解密后原始格式加密代际密钥来源qmc0MP3QMC1 静态内置常量表qmc3MP3QMC1 静态内置常量表qmcflacFLACQMC2 动态文件头TEA解出mflacFLACQMC2 动态文件头TEA解出mflac0FLACQMC1/混合按头部探测决定注意qmcflac里的flac代表解密后的格式不代表加密强度。看到qmcflac就以为只能解FLAC是很常见的误读。实际上只要文件头前两个字节非零qmcflac、qmcogg都走同一套动态逻辑只是产物扩展名不同。这也是为什么源码中会保留QMCDecodeTests.swift测试样例锁定的就是这类容易混淆的边界。2.2 工程里三个核心Swift文件怎么分工拿到压缩包后别急着点开xcodeproj按依赖顺序读文件更有效率。我建议的顺序是QMCipher.swift → QMCKeyDecoder.swift → TeaCipher.swift最后再看ViewController.swift里的拖拽事件。QMCipher.swift是门面也是命令行工具要保留的入口。它对外暴露convert方法内部先探测文件头版本再决定走静态分支还是动态分支。源码里这个方法还包含一个progress回调GUI界面的进度条就是靠它刷新的。你不需要在UI层做任何解密逻辑所有格式判断、替换表生成、分块读写都集中在这一个文件里这让抽取命令行版本变得非常省事。QMCKeyDecoder.swift是动态密钥的解析器。它的输入是文件头那一小块数据输出是解出来的密钥。具体分三步先读两字节密钥长度再截取对应长度的密文密钥段最后调用TeaCipher解密。很多网上流传的“QMC解密失败”案例问题就出在这个长度位读错了比如没有做字节序转换直接把高字节读成低字节导致解出来的替换表完全错位。TeaCipher.swift是TEA实现。TEA以64位分组为单位加密密钥是128位通常表现为4个UInt32。工程里用Swift写循环迭代把加解密封装成静态方法同时提供deriveTable方法根据解出的密钥生成置换表。这段代码属于基础算法实现考试里常见的TEA加解密填空在这里能看到实际工程长什么样边界处理、字节序统一、大端小端转换都在这一层做完。除了这三个核心文件其余大多是UI外壳。WindowController.swift负责窗口生命周期ViewController.swift接收拖拽进来的文件路径Assets.xcassets里Success.imageset是转换完成时的提示图。Info.plist声明了支持的文档类型让Finder能把qmcflac这类文件直接拖到App图标上。AppDelegate.swift.zbak是备份我一般留着不删AppDelegate反复修改是macOS工程的常态备份里往往记录着上一次能跑通的启动配置。2.3 一个最小解密模型看懂解壳过程就能改参数下面这段是浓缩后的最小模型去掉了窗口、进度回调、错误弹窗只保留版本判断和解密分派可以直接作为写命令行版qmc-convert的骨架import Foundation func detectQMCVersion(_ header: Data) - Int { guard header.count 16 else { return 1 } let length header.prefix(2).withUnsafeBytes { $0.load(as: UInt16.self) } .byteSwapped return length 0 ? 1 : 2 } func convertFile(_ url: URL, to output: URL) throws { let header try Data(contentsOf: url, options: .mappedIfSafe) switch detectQMCVersion(header) { case 1: // QMC1内置静态替换表对每一块做反向置换 try applyStaticTable(input: url, output: output) case 2: // QMC2先解文件头密钥再生成动态替换表 let key QMCKeyDecoder.extractKey(from: header) let table TeaCipher.deriveTable(from: key) try applyDynamicTable(input: url, output: output, table: table) default: throw QMCDecodeError.unsupportedVersion } }这段代码有两个关键转换。第一个是.byteSwapped文件头里的密钥长度按大端存储x86_64和Apple Silicon上如果直接load会读反必须交换字节序。第二个是header.count 16只读16字节足够判断版本不要为了判断版本把整个文件都载入内存。Data(contentsOf:options:.mappedIfSafe)在这里只映射头部区域后续正文要分块读取。参数说明detectQMCVersion的返回值1和2对应源码里QMCipher的两个内部分支extractKey(from:)只接受head数据不接受整个文件这样避免重复读文件deriveTable(from:)的产物是65536字节的Data工程里用数组缓存避免解密每个块时重新生成表。如果想把这段模型移植到Python或其他语言保住这三个函数边界就行内部实现可以整体替换。这里也解释了为什么工具可以不联网、不登录、离线转换动态密钥的TEA主密钥是写死在TeaCipher.swift里的固定常量。解密者不需要拿到原始音频密钥只需要从文件头中解出加密后的密钥表。QMC2的安全短板不在TEA本身而在那把固定的下发密钥理解了这一点后面排查版本分支就不会被网上互相矛盾的说法带偏。3. 把这套源码跑起来Xcode工程构建与命令行调用3.1 工程结构速览哪些文件是核心、哪些是模板压缩包打开后是一套标准的Xcode AppKit工程核心解密逻辑集中在顶层几个Swift文件里其余大多是界面和配置。先把文件归类能省去大量翻找时间。路径作用是否核心QMCipher.swift统一解密入口核心QMCKeyDecoder.swift文件头密钥解析核心TeaCipher.swiftTEA算法与置换表生成核心Constants.swift后缀映射、格式常量核心QMCDecodeTests.swift单元测试锁定格式判断参考ViewController.swift拖拽交互、进度展示GUI外壳WindowController.swift窗口配置GUI外壳QMCDecode.entitlements沙盒读写权限配置*.zbak / 备份文件.zip备份历史版本备用这里最需要留意的是entitlements。工程里同时存在QMCDecode.entitlements和QMCDecode.entitlements.zbak说明作者在沙盒权限上反复折腾过。如果不给App增加“用户选择的文件可读写”权限拖拽文件进窗口时会直接失败。先记住不要一上来就删任何.zbak文件它可能比当前配置更贴近一次成功运行的状态。3.2 用xcodebuild命令行编译绕过图形界面也能出包打开Xcode图形界面编译是最直观的方式可一旦遇到签名问题整个窗口会卡在错误弹窗里。我习惯直接用xcodebuild出错能直接看日志也方便写批量构建脚本。先确认工程名和scheme名一致然后执行cd QMCDecode xcodebuild \ -project QMCDecode.xcodeproj \ -scheme QMCDecode \ -configuration Release \ build \ CODE_SIGN_IDENTITY- \ CODE_SIGNING_REQUIREDNO这几个参数依次说明-project指定工程文件-scheme指定共享scheme在xcshareddata/xcschemes目录里能看到名字默认就是QMCDecode-configuration Release生成优化过的发布包调试阶段可改成Debug最后两行让Xcode用ad-hoc签名或不强制签名避免没有开发者证书时build直接失败。如果一切正常控制台最后会出现BUILD SUCCEEDED产物在build/Release/QMCDecode.app。这个包可以双击运行也可以继续做命令行调用。但注意用CODE_SIGNING_REQUIREDNO构建的App在部分macOS版本上可能被Gatekeeper拦截本地开发时右键打开即可要分发给别人还得走正式签名。如果你更习惯图形界面在Xcode的Signing Capabilities里把Team选为None签名证书选Sign to Run Locally同样不需要开发者账号。这两种方式对毕设演示和本地验证都够用真正发布的时候才需要申请付费证书。3.3 把转换核心提炼成命令行工具swiftc一条命令GUI版本适合手动拖文件但要批量处理几百首还是命令行更顺手。核心解密类并不依赖AppKit只有AppDelegate和ViewController用了窗口框架所以可以把四个核心文件抠出来配一个main.swift用swiftc编译成纯命令行工具。先写main.swiftimport Foundation guard CommandLine.arguments.count 2 else { print(用法: qmc-convert 输入文件 [输出文件或目录]) exit(1) } let input URL(fileURLWithPath: CommandLine.arguments[1]) let output: URL if CommandLine.arguments.count 3 { output URL(fileURLWithPath: CommandLine.arguments[2]) } else { output input.deletingPathExtension() } do { try QMCipher.shared.convert(input: input, output: output) print(完成: \(output.path)) } catch { print(转换失败: \(error.localizedDescription)) exit(2) }保存为main.swift后与QMCipher.swift、QMCKeyDecoder.swift、TeaCipher.swift、Constants.swift放到同一目录执行编译swiftc -O -o qmc-convert \ main.swift \ QMCipher.swift QMCKeyDecoder.swift TeaCipher.swift Constants.swift编译时如果报cannot find type NSColor之类的错误多半是QMCipher.swift文件头写了import Cocoa或import AppKit。命令行模式下改成import Foundation就能解决因为颜色、窗口这些UI类型不会被核心解密方法用到。这个细节说明工程的核心与UI边界做得不错换一个耦合重的项目抠命令行版要改的就不是一行import的事了。编译成功后调用./qmc-convert /Users/你的用户名/Music/test.qmcflac ~/Desktop/converted第一个参数是输入文件第二个参数是输出文件或目录输出目录不存在时QMCipher内部会自动创建。到这里工具已经从GUI应用退化成一个干净的命令行程序批量脚本可以完全绕开图形界面。建议把swiftc编译命令写成一个build.sh保存起来以后拿到源码的干净副本能快速重建。4. 批量转换qmcflac与qmc0/qmc3脚本、参数与输出验证4.1 批量转换脚本循环处理整个目录命令行工具有了手动逐个敲路径依然不现实。写一个批量脚本把输入目录里所有qmcflac、qmc0、qmc3、mflac、mflac0统一丢给qmc-convert并在输出目录保留原始相对路径这样整理素材时目录结构不会乱#!/bin/bash INPUT_DIR${1:?用法: $0 输入目录 [输出目录]} OUTPUT_DIR${2:-$HOME/Music/QMC_Converted} mkdir -p $OUTPUT_DIR find $INPUT_DIR -type f \( \ -name *.qmcflac -o -name *.qmc0 -o -name *.qmc3 -o \ -name *.mflac -o -name *.mflac0 \ \) -print0 | while IFS read -r -d file; do rel${file#$INPUT_DIR/} dest_dir$OUTPUT_DIR/$(dirname $rel) mkdir -p $dest_dir echo [转换] $file ./qmc-convert $file $dest_dir/$(basename $rel) echo [完成] $dest_dir/$(basename $rel) done解释几个关键点find用-o并列五种后缀-print0配合read -d 是为了安全处理带空格的中文文件名rel${file#$INPUT_DIR/}把绝对路径前缀去掉留下相对目录basename $rel去掉扩展名后传给qmc-convert由工具内部按实际解密结果补扩展名而不是在原名上硬改。这里要强调for file in $(find ...)的老写法几乎必然在空格文件名上翻车。中文歌名出现空格、括号的概率极高用while read -d 才是正解。执行前记得加执行权限chmod x batch_convert.sh ./batch_convert.sh /Users/你的用户名/Music/已下载 /Users/你的用户名/Music/unlocked脚本跑完先别急着删原文件随便挑几个输出文件看一眼大小如果发现输出的flac和源文件一样大甚至更大多半是走了错误的解密分支下一章会专门讲这类问题。4.2 输出参数说明FLAC与MP3的压缩级别、采样率如何设置这一步最容易产生误区。qmc-convert干的是“解密”不是“转码”。qmcflac文件解密后直接得到原始FLAC流采样率、位深、压缩级别都是打包之前的样子工具不会也没有必要重新编码。同理qmc0、qmc3解密后是原始MP3码率是原文件带出来的解密不会让音质“变得更清晰”也不会让文件变得更小。所以这个工具不存在“设置FLAC压缩级别8”或“把MP3码率压到192k”这种参数。如果你想要一个统一下发到手机的文件格式正确做法是先解密得到原始文件再交给FFmpeg做二次编码。常见做法是解密后保留FLAC用于归档MP3用于便携播放器如果原始是FLAC转成256k AAC或V0 MP3都很常见。目标场景建议命令说明手机音乐库ffmpeg -i input.flac -c:a aac -b:a 256k output.m4a体积与音质折中老式播放器ffmpeg -i input.flac -c:a libmp3lame -q:a 0 output.mp3V0可变码率无损归档不转码保留解密后的flac不损失任何信息真正需要关注的是输出文件的封装格式。qmc-convert默认按照Constants.swift里的映射写扩展名qmcflac→flacqmc0/qmc3→mp3mflac/mflac0→flac。但扩展名只是“最可能的结果”不是保证极少数文件实际音频流可能是AAC这时应该用ffprobe判断真实编码见下一节。4.3 验证转换结果用ffprobe和md5确认不是“假转码”批量转换完要抽样验证确认输出没有“看似成功、实则乱码”。macOS上安装FFmpeg工具链后执行ffprobe -v error \ -show_entries formatformat_name,duration \ -show_entries streamcodec_name,codec_type \ -of json output.flac正常FLAC输出的JSON里format_name是flaccodec_name也是flacduration与原歌曲时长对应MP3则应该看到codec_namemp3。如果看到aac但扩展名是.flac说明原始流不是FLAC需要按ffprobe的结果修改扩展名或者重新确认版本分支。还可以用file命令快速看个大概file output.flac识别为“FLAC audio bitstream data”基本没问题如果识别成“RIFF”或“MPEG ADTS”说明封装名写错了。对比加密文件和输出文件的大小也有参考价值动态密钥版本的文件头比原始流多出一小段密钥数据所以解密后文件体积通常会减少几十到几百字节而不是增大。更直接的验证是播放输出听开头一秒有没有爆音或明显停顿这比任何工具都直观。提示解密不是转码qmc-convert不会改变音频的采样率、位深和码率。如果你想统一格式先确认解密结果再交给FFmpeg二次编码不要试图在解密阶段直接改音频参数。5. 避坑与常见问题五类高频翻车现场5.1 拖进去没反应日志里出现“No such file or directory”现象把qmcflac文件拖进QMCDecode.app窗口界面没有任何反应控制台输出一行No such file or directory但文件明明存在。原因macOS沙盒权限没放行。工程里的QMCDecode.entitlements如果只开启了App Sandbox没有声明用户选择的文件读写权限App内部能看到的目录会被限定在容器里拖进来的外部路径被系统拦截表现为“文件不存在”。解决在entitlements文件里增加com.apple.security.files.user-selected.read-write保存后重新编译。如果是命令行版干脆不启用沙盒qmc-convert没有沙盒容器直接走普通文件读写最省事。我一般在调试阶段用命令行版发布GUI时再开沙盒这样两份产物互不干扰。5.2 转出来的FLAC播放器不认VLC能放但原生“音乐”不认现象解密生成.flac文件文件大小看起来合理Windows上能播macOS自带音乐App却不认双击提示格式不支持。原因扩展名与实际音频流不一致。qmcflac只是“最可能对应FLAC”但如果原始下载源是AAC编码伪装成flac后缀解密产物就是AAC流强行叫.flac自然被原生播放器拒收。另一种可能是解密不完整文件头残留了密钥块播放器解析metadata时失败。解决用ffprobe读取实际codec_name是aac就改扩展名为.m4a是mp3就改成.mp3。每次批量转换后跑一遍自动改名脚本比手动逐一播放节省大量时间。如果确认是解密不完整重点检查5.5里的版本判断分支。5.3 大文件转换到一半闪退或内存占用飙到2GB现象转换一首两三百MB的演唱会录音时进度条走到一半程序闪退Activity Monitor显示qmc-convert内存占用持续上涨。原因解密逻辑把整个文件读进一个Data对象然后一次性异或写回。FLAC高码率长音频体积很大缓冲区叠加就把内存顶爆。解决改成FileHandle分块读取和写入每次处理64KB或256KB不要一次读全文件。命令行版抽取核心时尤其要注意如果QMCipher.swift里保留的是整文件读取建议改成分块。下面这段是macOS上常见的分块读写写法let inputHandle try FileHandle(forReadingFrom: inputURL) defer { try? inputHandle.close() } let outputHandle try FileHandle(forWritingTo: outputURL) defer { try? outputHandle.close() } while true { guard let chunk try inputHandle.read(upToCount: 64 * 1024), !chunk.isEmpty else { break } let result try decryptor.processChunk(chunk) try outputHandle.write(contentsOf: result) }参数说明read(upToCount:)在EOF时返回nil用guard let退出循环64KB是通用折中值既减少磁盘IO次数又不会让内存峰值太夸张decryptor.processChunk需要保持内部状态因为QMC2的解密表虽然全局固定但某些版本还带块内偏移跨块状态必须保留否则中间会出现连续性破音。5.4 Xcode编译报签名错误codesign直接失败现象打开xcodeproj点Run弹窗提示签名失败错误信息类似Failed to code sign QMCDecode。没有开发者账号的情况下非常常见。原因Xcode默认要求Team证书而本机只装了Apple Development证书或根本没有证书。解决命令行编译时用CODE_SIGN_IDENTITY- CODE_SIGNING_REQUIREDNO让系统做ad-hoc签名图形界面则在Signing Capabilities里把Team选为None签名证书选Sign to Run Locally。这个操作不需要开发者账号本地运行完全够用。要提醒的是用这种方式构建的App换到别的机器上可能被Gatekeeper拦但作为毕设演示和本地验证没有问题。5.5 同一首歌有人转出来是FLAC你转出来却是MP3伪装成FLAC现象同一个qmcflac文件网上别人贴出的结果扩展名是flac你转换后却被ffprobe识别为MP3两个结果都能播但编码不同。原因QMC2动态版本里扩展名qmcflac只代表“下载时原始音频可能是FLAC”但有些下载渠道会把原始AAC或MP3也统一包装成qmcflac后缀。QMC解密只负责还原加密前的字节流不负责纠正原始格式。网上那些转出FLAC的人可能是用另一套旧工具走了静态分支或者文件本来就源于真FLAC。解决解密完不要迷信扩展名先跑ffprobe再根据实际格式重命名。工程里QMCDecodeTests.swift锁定的就是这类边界场景改代码后先跑测试能有效减少把MP3判断成FLAC的问题。如果发现特定目录下的文件频繁出现这种错位建议在批量脚本里加一个ffprobe探测步骤识别到非预期格式时自动改后缀并打警告日志。6. 进阶用法把转换核心挂到文件监听上做全自动解码6.1 用fswatch监听下载目录新文件自动转换批量脚本每次都要手动跑一遍时间久了还是会犯懒。macOS上用fswatch监听目录新文件落地后自动交给qmc-convert能省掉重复劳动。先安装fswatch然后写监听脚本fswatch -0 $INPUT_DIR | while read -d file; do case $file in *.qmcflac|*.qmc0|*.qmc3|*.mflac|*.mflac0) echo 检测到新文件: $file ./qmc-convert $file $HOME/Music/unlocked ;; esac donefswatch的-0表示用空字符分隔事件配合read -d 处理中文和空格路径。监听脚本启动后下载目录一出现新加密文件几秒内就会被转成普通格式。这个方案适合长期挂机的场景也适合验证不同下载源、不同后缀的文件是否都能正确转换。6.2 解码后自动喂给FFmpeg做二次转码解密只是第一步把结果自动转成适合手机或播放器的格式才算一条完整流水线。在监听脚本里追加一段ffmpeg调用遇到解码后的FLAC就顺手转成256k AAC./qmc-convert $file $TMP_DIR base${file##*/} flac${base%.*}.flac if ffprobe -v error -of defaultnoprint_wrappers1:nokey1 \ -select_streams a:0 -show_entries streamcodec_name $TMP_DIR/$flac | grep -q flac; then ffmpeg -y -i $TMP_DIR/$flac -c:a aac -b:a 256k $OUT_DIR/${base%.*}.m4a fi这里ffmpeg参数-c:a aac选择AAC编码器-b:a 256k是目标码率-y表示覆盖同名输出。这段逻辑把“解密、格式探测、转码”串成一条链以后拿到任何qmcflac都不用先手动确认格式脚本会自动判断是否值得二次转码。加密文件进入目录普通音频出来整个过程不需要开窗口。从那以后我每次拿到音频类转换源码都会先把它编译成一个命令行入口再做监听和转码最后才回到GUI工程里看交互。GUI版本适合演示命令行入口适合跑批和排错两条路各自保留能省下大量重复劳动。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进