soda-decrypt
v1.0.0
Published
汽水音乐音频解密:PlayAuth + 音频链接 → 可播放 M4A/FLAC(CENC AES-CTR),Node 与浏览器通用
Maintainers
Readme
汽水音乐音频解密工具
逆向自 SodaMusic v3.5.1(Win32, Electron)。给定 PlayAuth(spade_a) + 音频链接, 输出可直接播放的 M4A / FLAC 文件。
已发布到 npm(soda-decrypt),同一套包在 Node 与 浏览器 都可用——靠 exports
条件导出自动切换入口:Node 拿到 lib/(CJS + ESM),浏览器拿到 browser-libs/
(纯 ESM、零依赖、内置纯 JS AES,无需 WebCrypto polyfill)。
npm 安装
npm install soda-decrypt| 运行环境 | 用法 | 命中入口 |
| --- | --- | --- |
| Node(CommonJS) | const { decryptSodaAudio } = require('soda-decrypt') | lib/index.js |
| Node(ESM) | import { decryptSodaAudio } from 'soda-decrypt' | lib/index.mjs |
| 浏览器(bundler: vite / webpack / esbuild…) | import { decryptSodaAudio } from 'soda-decrypt' | browser-libs/decrypt.js |
| 浏览器(免构建,CDN) | import { decryptSodaAudio } from 'https://esm.sh/soda-decrypt' | 同上(esm.sh 自动选浏览器入口) |
命令行工具(可选全局安装):
npm install -g soda-decrypt
soda-decrypt --playauth '…' --url '…' --out song.m4a目录结构
decrypt-tool/
soda-decrypt.js CLI 入口(薄壳)
lib/ 解密库(可直接 require 调用)
index.js 高层 API:decryptSodaAudio()
decode-spade.js PlayAuth → 密钥(device.node)
cenc.js CENC AES-CTR 解密 MP4(核心)
download.js HTTP/HTTPS 下载
tools/ 数据提取工具
read-entries.js 读 LunaCacheV2/entries.db → data/entries-index.json
decode-spade.js 对索引里所有 spade 批量出密钥 → data/entries-with-keys.json
data/ 生成的索引/密钥映射表(json)
output/ 解密产物
test/ 测试脚本(与解密代码分离)
index.html 浏览器暗房 UI(可部署 GitHub Pages)
browser-libs/ 浏览器版解密库(无 Node 依赖,纯 ESM)
decrypt.js 浏览器入口(exports.browser 指向这里)
lib/index.mjs Node ESM 入口(包装 lib/index.js)
package.json exports 条件导出:Node→lib/,浏览器→browser-libs/CLI 用法
# 方式一:PlayAuth + 音频 URL
node soda-decrypt.js --playauth "你的spade_a" --url "音频链接" --out output/song.m4a
# 方式二:PlayAuth + 本地已下载的加密文件
node soda-decrypt.js --playauth "你的spade_a" --file "encrypted.bin" --out output/song.m4a
# 方式三:已经知道 16 字节密钥(32 hex)时,跳过 decodeSpade
node soda-decrypt.js --key 681abcf2dd0840a08eeda68c2876d6d4 --url "音频链接" --out output/song.m4a
# 解密后自动打印 ffprobe 信息验证
node soda-decrypt.js --playauth "..." --url "..." --out output/song.m4a --probeWindows PowerShell: URL 里的 & 会被当成命令分隔符,导致链接被截断,并出现 'ch' 不是内部或外部命令。请先赋给变量,或把 URL 写入文本文件后用 --url-file:
$u = 'https://...?a=8478&ch=0&cr=3&...'
node soda-decrypt.js --playauth '你的spade_a' --url $u --out output/song.m4a
# 或
node soda-decrypt.js --playauth '你的spade_a' --url-file url.txt --out output/song.m4a运行环境:无需安装 SodaMusic。decodeSpade 有纯 JS 实现(与原生 device.node
在全部 425 个缓存 PlayAuth 上产出一致),唯一依赖是 Node.js(≥14)。
仅当纯 JS 解析失败时才会尝试加载 device.node(此时需设 SODA_APP_DIR 指向含
device.node 的目录;设 SODA_FORCE_NATIVE=1 可强制只用原生)。
网页 / GitHub Pages
纯静态页面,浏览器直连音频 URL,没有后端代理。
node serve.js # 本地预览 http://127.0.0.1:3478/部署:把仓库推到 GitHub,在仓库 Settings → Pages 选择主分支根目录。汽水 CDN 若未放行 CORS,URL 下载会失败,改用「本地文件」即可(先另存加密音频再拖进页面)。
库调用
安装后顶层 API 在 Node / 浏览器完全一致:await decryptSodaAudio({...}) 返回
{ buffer, decryptedBytes, key, codec }(codec 为 'mp4a' 或 'fLaC')。
Node
// CommonJS
const { decryptSodaAudio } = require('soda-decrypt')
// ESM
// import { decryptSodaAudio } from 'soda-decrypt'
// PlayAuth + URL → 写文件 + 返回 Buffer
const r = await decryptSodaAudio({
playAuth: '你的spade_a', // 或 key: '681abcf2...'(32 hex)跳过第一层
url: '音频链接', // 或 file: 'encrypted.bin' / Buffer
out: 'song.m4a', // 可选:仅 Node,同时写文件(自动建目录)
onProgress: (m) => console.log(m),
})
// 底层函数也导出
const { decodeSpade, decryptM4A, download } = require('soda-decrypt/lib')浏览器
浏览器入口由 bundler 自动解析到 browser-libs/(纯 ESM、零依赖、内置纯 JS AES),
无需任何配置。在 vite / webpack / esbuild 项目里直接:
import { decryptSodaAudio } from 'soda-decrypt'
const r = await decryptSodaAudio({
playAuth: '你的spade_a',
file: encryptedUint8Array, // 或 url: '音频链接'(受 CDN CORS 限制)
onProgress: (m) => console.log(m),
})
// r = { buffer, decryptedBytes, key, codec } buffer 为 Uint8Array免构建场景直接用 CDN(esm.sh 自动选浏览器入口):
<script type="module">
import { decryptSodaAudio } from 'https://esm.sh/soda-decrypt'
</script>差异说明:浏览器版没有
download(下载用fetchEncrypted,绕过 CORS),也不支持out写文件(浏览器无文件系统,直接拿返回的Uint8Array)。decodeSpade/decryptM4A底层函数在两端同样导出。
解密算法(两层)
音频是 标准 CENC 加密的 MP4(enca sample entry + senc box),不是自定义流密码。
第一层:PlayAuth → 密钥(decodeSpade,纯 JS)
spade_a(即 item.PlayAuth)经 decodeSpade 得到 16 字节 AES-128 密钥(32 个 hex 字符)。
纯 JS 实现(lib/decode-spade.js),算法:
- Base64 解码
spade_a paddingLength = (bytes[0] ^ bytes[1] ^ bytes[2]) - 48;有效数据取bytes[1 .. len-padding]- 解密内核:前置字节流
[0xfa, 0x55],对第i字节做raw = bytes[i] ^ buff[i] - popcount(i) - 0x15,负值按 mod 255 归位 - 结果首字节经 base36 解码得到密钥长度,取后续字节 UTF-8 解码 → hex 密钥
不依赖 MachineGuid,不依赖 app。
第二层:音频流解密(CENC AES-CTR,ffmpeg.dll demuxer 内建)
ffmpeg.dll 通过 setDemuxerArg({key:'decryption_key', value: 密钥}) 接收密钥,播放时按 CENC 规范解密:
- 容器:非分片 MP4(已确认缓存文件均为
ftyp + moov + mdat;如有moof分片也支持) - sample entry:
enca(Encrypted Audio),解密后须改回真实 codec - IV:
sencbox 中每个 sample 一个 8 字节 IV(version 0;version 1 为 16 字节) - 加密方式:对每个 sample 做 AES-128-CTR
- counter 初始块 = 8 字节 IV 置于高 8 字节,低 8 字节为 0
- 每 16 字节块按 128 位大端整数 +1
- sample 在
mdat中的位置由stsz(大小)、stco(chunk 偏移)、stsc(sample↔chunk 映射)决定 - codec 补丁:
enca内层若是 FLAC 音频条目(含dfLabox)则补丁成fLaC,否则mp4a- AAC 曲目:
enca→mp4a - lossless 曲目(quality=lossless):24-bit FLAC 装在 MP4 容器里,
enca→fLaC; 输出是 MP4 容器内的 FLAC,可再ffmpeg -i out.mp4 -c copy out.flac转成纯 FLAC (已验证:44100Hz / 立体声 / 24-bit)
- AAC 曲目:
解密结果验证
ffprobe 识别输出为 AAC LC 或 FLAC,时长与 stsz sample 数一致。已对缓存中
425 个文件的采样批次(19 个不同曲目、含 highest/hi_res/lossless/spatial/medium
各音质)全部验证通过(每首全量解码 0 错误、非静音)。
测试
node test/pick-newest.js # 取最近访问的缓存条目 → data/_newest.json
node test/test-url.js # PlayAuth+URL 模式端到端测试(输出到 output/)
node test/batch-verify.js # 批量解密多样曲目 + ffprobe 验证
node test/demo.js # 模拟用户输入(PlayAuth+URL)完整流程从缓存批量提取音频(可选)
缓存里已有 425 个加密 .bin(LunaCacheV2\<chunkId>.bin),元数据(含 spade)在 entries.db。
用 tools/ 生成映射表,再逐条调用 soda-decrypt.js 全部还原:
node tools/read-entries.js # → data/entries-index.json
node tools/decode-spade.js # → data/entries-with-keys.json
# 然后对每条:
node soda-decrypt.js --key <key> --file "C:\Users\<user>\AppData\Roaming\SodaMusic\LunaCacheV2\<chunkId>.bin" --out output/<resourceId>.m4a注意:
tools/read-entries.js依赖restored-sources/addons/lmdb下的 JS 封装 + 复制到restored-sources/addons/lmdb/build/Release/lmdb.node的原生模块(bindings shim 在restored-sources/node_modules/bindings)。
关键文件位置
- 原生模块:
$Var31/app/resources/app/{device.node, lmdb.node, ffmpeg.dll} - 缓存目录:
C:\Users\<user>\AppData\Roaming\SodaMusic\LunaCacheV2\ - 播放器解密调用点:
restored-sources/src/services/player/v4/engine.ts:471-483
