昨天,我想将网易云上下载的歌曲拷到MP3里面,方便以后跑5公里的时候听,结果,突然发现不少歌都是ncm格式,不禁产生了好奇。
NCM格式分析
音频知识简介
特意读了一下《音视频开发进阶指南》,总结如下:
我们平常说的mp3格式、wav格式的音乐其实是说的压缩编码格式。
一首歌是怎么从歌手的喉咙里发出后变成一个文件的呢?
需要经过采样、量化和编码三个步骤。
采样
声音是连续的模拟信号,通过采样,将之转变为离散的数字信号,其中要遵循的是奈奎斯特定理:只要采样频率不低于声音信号最高频率的两倍,采样得到的数字信号就能保真地记录、还原声音。
人耳能够听到的范围是20Hz到20kHz,所以采样频率一般为44.1kHz,这样就可以保证采样声音达到20kHz也能被数字化,从而使得经过数字化处理之后,人耳听到的声音质量不会被降低。而所谓的44.1kHz就是代表1秒会采样44100次
量化
量化是指在幅度轴上对信号进行数字化,就是用多少位的数据来记录一个采样。比如用16比特的二进制信号来表示声音的一个采样,而16比特(一个short)所表示的范围是[-32768,32767],共有65536个可能取值,因此最终模拟的音频信号在幅度上也分为了65536层
编码
编码就是我们按一定的格式对采样和量化后的数字数据进行记录。直接存储的话,文件可能过大,像CD那样直接存储下来的没什么问题,但如果要在网络中在线传播,就必须进行压缩。
压缩的原理是压缩掉冗余信号,包括人耳感知不到的信号以及人耳掩蔽效应(指人耳只对最明显的声音反应敏感)掩蔽掉的信号。同时压缩算法包括有损压缩和无损压缩。无损压缩是指解压后的数据可以完全复原。有损压缩是指解压后的数据不能完全复原,会丢失一部分信息。
两种可能
第一种可能是网易独立进行了压缩编码算法的研究,创造出来的新的格式。
第二种是在现有格式的基础上,增加了一些冗余信息,相当于将一首MP3格式的歌放入密码箱中,付费者可开启。
不管是哪种,都必须了解格式的构成。
GitHub项目
我自知学艺不精,所以去万能的GitHub上寻求答案。
果然有先驱者,貌似是anonymous5l提供了最初的ncmdump版本,然后再由其他几位大佬进行重构和功能完善
格式分析
总体结构
首先,我从
由此可得知,NCM 实际上不是音频格式是容器格式,封装了对应格式的 Meta 以及封面等信息
密钥问题
另外,NCM使用了NCM使用了AES加密,但每个NCM加密的密钥是一样的,因此只要获取了AES的密钥KEY,就可以根据格式解开对应的资源。
AES我知道,一种对称加密算法嘛,这学期刚好学了网络密码。
AES是一种迭代型分组加密算法,分组长度为128bit,密钥长度为128、192或256bit,不同的密钥长度对应的迭代轮数不同,对应关系如下:
密钥长度
轮数
128
10
192
12
256
14
我最好奇的是AES的密钥是怎么搞到的。出于“不可能只有我一个人好奇”的信念,看了好几个项目的README.md以及issues
结果只有一个人在yoki123的项目中issues了这个问题,
大佬表示,他的密钥也是从annoymous51处获得的,但他推测是通过反编译播放器客户端得到的。
并给出了三条原因:
播放器也需要读取ncm格式,客户端就包含有解密逻辑
解密算法是AES,是对称加密
恰巧所有的文件都使用了相同的AES key,那么key在客户端播放器中就是一个常量
而作为第一个搞到密钥的大佬annoymous51,他的项目中竟然没有一个人问这个问题,我自己问了一下,看大佬会不会回复
代码分析
密钥的问题暂时不纠结了,接下来对照lianglixin的代码来钻研,
从提交说明来看,folder_dump.py实现的是批量的转换,虽说Python文件操作的部分不难,但是有人做了这个工作也省得我自己动手了。
在她的README.md中说明了需要安装依赖库pycrypto,使用pip install pycrypto安装,但如果使用了Anaconda,就不需要装了
代码地址为:
main函数
main函数中用来进行文件操作,根据输入的参数中的文件夹,在此文件夹中的全部文件中进行筛选,找到.ncm格式的文件,执行dump函数
这个程序按理来说,运行的方法是在命令行中cd到此文件所在路径,然后输入python folder_dump.py ncm保存文件夹路径
但这种方式挺麻烦的,而且程序中竟然还有变量都没有定义,比如rootdir,因此无法运行成功,
于是我对她这一部分再次进行了修改,我将main函数改成如下所示的内容:
if __name__ == '__main__':
file_path = input("请输入文件所在路径(例如:E:\\ncm_music)\n")
list = os.listdir(file_path) # Get all files in folder.
for i in range(0,len(list)):
# path = os.path.join("E:\\ncm_music",list[i])
path = os.path.join(file_path, list[i])
print(path)
if os.path.isfile(path):
if os.path.isfile(path):
if file_extension(path) == ".ncm":
try:
dump(path)
except:
pass
效果如下:
导入模块
然后看看导入的模块
import binascii
import struct
import base64
import json
import os
from Crypto.Cipher import AES
binascii的主要作用是实现进制和字符串之间的转换。
Python提供了struct模块,它是一个类似C或C++的struct结构,配合其模块提供的方法可以将二进制数据与Python的数据结构互相转换。
Base64 是网络上最常见的用于传输 8Bit 字节码的编码方式之一,Base64 就是一种基于 64 个可打印字符来表示二进制数据的方法。可查看 RFC2045 ~ RFC2049,上面有 MIME 的详细规范。Base64 编码是从二进制到字符的过程,可用于在 HTTP 环境下传递较长的标识信息。比如使二进制数据可以作为电子邮件的内容正确地发送,用作 URL 的一部分,或者作为 HTTP POST 请求的一部分。
json模块提供了对JSON的支持,它既包含了将JSON字符串恢复成Python对象的函数,也提供了将Python对象转换成JSON字符串的函数。
os模块提供了多数操作系统的功能接口函数。当os模块被导入后,它会自适应于不同的操作系统平台,根据不同的平台进行相应的操作,在python编程时,经常和文件、目录打交道,所以离不开os模块。
Crypto是一个加密算法模块,Cipher是该模块下的对称加密算法对象。
dump函数
最后看看dump函数,这个才是重点
1. def dump(file_path):
2. core_key = binascii.a2b_hex("687A4852416D736F356B496E62617857")
3. meta_key = binascii.a2b_hex("2331346C6A6B5F215C5D2630553C2728")
4. unpad = lambda s : s[0:-(s[-1] if type(s[-1]) == int else ord(s[-1]))]
5. f = open(file_path,'rb')
6. header = f.read(8)
7. assert binascii.b2a_hex(header) == b'4354454e4644414d'
8. f.seek(2, 1)
9. key_length = f.read(4)
10. key_length = struct.unpack('
11. key_data = f.read(key_length)
12. key_data_array = bytearray(key_data)
13. for i in range (0,len(key_data_array)): key_data_array[i] ^= 0x64
14. key_data = bytes(key_data_array)
15. cryptor = AES.new(core_key, AES.MODE_ECB)
16. key_data = unpad(cryptor.decrypt(key_data))[17:]
17. key_length = len(key