代码拉取完成,页面将自动刷新
import binascii
import struct
import base64
import json
import os
import tkinter
from tkinter import filedialog
from Crypto.Cipher import AES
def dump(file_path):
"""
原始项目:ncmdump
解密文件,生成对应的通用格式音频文件
:param file_path: 文件路径
:return: 解密后的文件名
"""
# 十六进制转字符串
core_key = binascii.a2b_hex("687A4852416D736F356B496E62617857")
meta_key = binascii.a2b_hex("2331346C6A6B5F215C5D2630553C2728")
# 分组加密函数,如果数据的长度不是分组的整数倍,需要填充数据到分组的倍数,填充的每个字节值为填充的长度
# 这里用来去除填充的数据
unpad = lambda s: s[0:-(s[-1] if type(s[-1]) == int else ord(s[-1]))] # lambda函数,传入s。ord将字符转ASCII码对应数值
f = open(file_path, 'rb')
header = f.read(8) # 读取8个字节的数据(Magic Header)
# 字符串转十六进制
assert binascii.b2a_hex(header) == b'4354454e4644414d' # 验证magic头
f.seek(2, 1) # 将指针从当前位置处(形参1),向后移动两个字节(共10字节magic头)
key_length = f.read(4) # AES秘钥长度,占4字节
key_length = struct.unpack('<I', bytes(key_length))[0] # 十六进制数据按照小端字节序(<),无符号整型(I)数据解析。返回元组,提取数值(128)
key_data = f.read(key_length) # 向后读取key长度(其实就是128)的字节
key_data_array = bytearray(key_data) # 转换成字节数组,可以赋值0-255
for i in range(0, len(key_data_array)):
key_data_array[i] ^= 0x64 # 每个字节中的值与0x64进行异或
key_data = bytes(key_data_array) # 转换回字节序列
cryptor = AES.new(core_key, AES.MODE_ECB) # AES,电码本模式(ECB)
key_data = unpad(cryptor.decrypt(key_data))[17:] # 去除17字节的前缀
key_length = len(key_data) # key长度
key_data = bytearray(key_data) # 转字节数组
# RC4-KSA算法生成S盒
key_box = bytearray(range(256)) # 生成一个字节取值为0-255的字节数组,作为s盒的初值
c = 0 # 随机搅乱
last_byte = 0 # 上一轮的c
key_offset = 0 # 偏移值
for i in range(256):
swap = key_box[i]
c = (swap + last_byte + key_data[key_offset]) & 0xff # & 0xff,防止数值超过255
key_offset += 1
if key_offset >= key_length:
key_offset = 0
key_box[i] = key_box[c]
key_box[c] = swap # 此处[i]和[c]发生交换
last_byte = c
meta_length = f.read(4)
meta_length = struct.unpack('<I', bytes(meta_length))[0] # Meta的长度
meta_data = f.read(meta_length)
meta_data_array = bytearray(meta_data)
for i in range(0, len(meta_data_array)):
meta_data_array[i] ^= 0x63
meta_data = bytes(meta_data_array)
meta_data = base64.b64decode(meta_data[22:]) # 去除前缀,base64解码
cryptor = AES.new(meta_key, AES.MODE_ECB)
meta_data = unpad(cryptor.decrypt(meta_data)).decode('utf-8')[6:] # 去除music:前缀,获得元数据json字符串
meta_data = json.loads(meta_data)
crc32 = f.read(4) # CRC校验码
crc32 = struct.unpack('<I', bytes(crc32))[0]
f.seek(5, 1) # 跳过5字节
image_size = f.read(4)
image_size = struct.unpack('<I', bytes(image_size))[0] # 图片大小
image_data = f.read(image_size) # 图片数据
file_name = f.name.split("/")[-1].split(".ncm")[0] + '.' + meta_data['format'] # 文件名
m = open(os.path.join(os.path.split(file_path)[0], file_name), 'wb')
# chunk = bytearray()
while True:
chunk = bytearray(f.read(0x8000))
chunk_length = len(chunk)
if not chunk:
break
for i in range(1, chunk_length + 1): # RC4-PRGA
j = i & 0xff
chunk[i - 1] ^= key_box[(key_box[j] + key_box[(key_box[j] + j) & 0xff]) & 0xff]
m.write(chunk)
m.close()
f.close()
return file_name
def get_file_path(dir_path):
"""
:param dir_path: 文件所在目录
:return: 文件路径list
"""
file_path_list = []
for root, dirs, items in os.walk(dir_path):
for item in items:
file_path = os.path.join(root, item)
file_path = file_path.replace('\\', '/')
suffix = os.path.splitext(file_path)[-1]
suffix_limit = ['.ncm'] # 格式限定
if suffix not in suffix_limit:
continue
file_path_list.append(file_path)
return file_path_list
def main():
tkinter.Tk().withdraw() # 创建一个Tkinter.Tk()实例,隐藏
dir_path = filedialog.askdirectory(title='选择ncm歌曲所在目录') # 选择文件夹
if dir_path == '':
print("未指定路径!")
return
file_path_list = get_file_path(dir_path)
total = len(file_path_list)
print(f'----------开始转换,总计:{total}----------')
for num, path in enumerate(file_path_list):
try:
file_name = dump(path)
print(f'{num + 1}/{total}(Succeed) - {file_name}')
except AssertionError:
print(f'{num + 1}/{total}(Failed) - {path}')
continue
print('----------全部转换完成----------')
if __name__ == '__main__':
main()
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。