Files
ncmmiao/src/ncmdump.rs
lkhsss e7ecb37878 - 修改默认线程数为cpu核心数
- 修改多线程通信为crossbeam-channel库,增加通讯性能
 - ⬆️ 升级依赖
 - 🔨 优化解密算法,提高解密效率
2025-08-12 22:26:46 +08:00

523 lines
17 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use crate::apperror::AppError;
use crate::messager;
use aes::cipher::generic_array::typenum::U16;
use aes::cipher::{generic_array::GenericArray, BlockDecrypt, KeyInit};
use aes::Aes128;
use audiotags::{MimeType, Picture, Tag};
use base64::{self, Engine};
use colored::*;
use log::{debug, info, trace};
use messager::Signals;
use serde_derive::{Deserialize, Serialize};
use serde_json::{self, Value};
use std::fmt::Debug;
use std::fs::{self, File};
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::str::from_utf8;
use std::vec;
// lazy_static! {
// // 解密需要的密钥
// static ref KEY_CORE: Vec<u8> = decode("687A4852416D736F356B496E62617857").unwrap();//绝对正确
// static ref KEY_META: Vec<u8> = decode("2331346C6A6B5F215C5D2630553C2728").unwrap();
// }
// 原KEY_CORE数据687A4852416D736F356B496E62617857
const NEW_KEY_CORE: [u8; 16] = [
0x68, 0x7A, 0x48, 0x52, 0x41, 0x6D, 0x73, 0x6F, 0x35, 0x6B, 0x49, 0x6E, 0x62, 0x61, 0x78, 0x57,
];
// 原KEY_META数据2331346C6A6B5F215C5D2630553C2728
const NEW_KEY_META: [u8; 16] = [
0x23, 0x31, 0x34, 0x6C, 0x6A, 0x6B, 0x5F, 0x21, 0x5C, 0x5D, 0x26, 0x30, 0x55, 0x3C, 0x27, 0x28,
];
#[derive(Debug)]
#[allow(unused_variables)]
pub struct Ncmfile {
/// 文件对象
pub file: File,
/// 文件名称,不带文件后缀
pub filename: String,
/// 文件名称,带后缀名
pub fullfilename: String,
/// 文件大小
pub size: u64,
/// 游标
pub position: u64,
}
impl Ncmfile {
/// 各种工具方法
pub fn new(filepath: &str) -> Result<Ncmfile, AppError> {
let file = match File::open(filepath) {
Ok(f) => f,
Err(_) => return Err(AppError::FileReadError),
};
let path = Path::new(filepath);
let fullfilename = path
.file_name()
.ok_or(AppError::FileReadError)?
.to_str()
.ok_or(AppError::FileReadError)?
.to_string();
let size = file
.metadata()
.map_err(|_| AppError::CannotReadMetaInfo)?
.len();
let filename = match Path::new(&filepath).file_stem() {
Some(f) => f.to_str().ok_or(AppError::FileReadError)?.to_string(),
None => return Err(AppError::CannotReadFileName),
};
Ok(Ncmfile {
file,
filename,
fullfilename,
size,
position: 0,
})
}
/// 根据传入的长度来读取文件
///
/// 该函数可以记录上次读取的位置,下次读取时从上次读取的位置开始
/// - length 想要读取的长度
pub fn seekread(&mut self, length: u64) -> Result<Vec<u8>, AppError> {
if self.position + length > self.size {
Err(AppError::FileReadError)
} else {
let mut reader = BufReader::new(&self.file);
let _ = reader.seek(SeekFrom::Start(self.position));
let mut buf = vec![0; length as usize];
let _ = reader.read_exact(&mut buf);
self.position += length;
Ok(buf[..].to_vec())
}
}
/// 从指定位置开始读取。
///
/// !!!该函数仍然会更新游标
///
/// - offset 开始位置
/// - length 想要读取的长度
#[allow(dead_code)]
pub fn seekread_from(&mut self, offset: u64, length: u64) -> Result<Vec<u8>, AppError> {
if self.position + length > self.size {
Err(AppError::FileReadError)
} else {
let mut reader = BufReader::new(&self.file);
let _ = reader.seek(SeekFrom::Start(offset));
let mut buf = vec![0; length as usize];
let _ = reader.read_exact(&mut buf);
self.position = offset + length;
Ok(buf[..].to_vec())
}
}
#[allow(dead_code)]
pub fn seekread_to_end(&mut self) -> Result<Vec<u8>, std::io::Error> {
let mut reader = BufReader::new(&self.file);
reader.seek(SeekFrom::Start(self.position))?;
let mut buf = vec![0; self.size as usize - self.position as usize];
reader.read_exact(&mut buf)?;
self.position = self.size;
Ok(buf[..].to_vec())
}
pub fn seekread_no_error(&mut self, length: u64) -> Vec<u8> {
if self.position + length > self.size {
if self.position >= self.size {
vec![]
} else {
let mut reader = BufReader::new(&self.file);
let _ = reader.seek(SeekFrom::Start(self.position));
let mut buf: Vec<u8> = vec![0; (self.size - self.position) as usize];
let _ = reader.read_exact(&mut buf);
self.position += length;
buf[..].to_vec()
}
} else {
let mut reader = BufReader::new(&self.file);
let _ = reader.seek(SeekFrom::Start(self.position));
let mut buf = vec![0; length as usize];
let _ = reader.read_exact(&mut buf);
self.position += length;
buf[..].to_vec()
}
}
/// 跳过某些数据
pub fn skip(&mut self, length: u64) -> Result<(), AppError> {
if self.position + length > self.size {
Err(AppError::FileReadError)
} else {
self.position += length;
Ok(())
}
}
///按字节进行0x64异或。
fn parse_key(key: &mut [u8]) -> &[u8] {
for item in &mut *key {
*item ^= 0x64;
}
key
}
fn save(&mut self, path: &PathBuf, data: Vec<u8>) -> Result<(), AppError> {
let music_file = match File::create(path) {
Ok(o) => o,
Err(_) => return Err(AppError::FileWriteError),
};
let mut writer = BufWriter::new(music_file);
let _ = writer.write_all(&data);
// 关闭文件
match writer.flush() {
Ok(o) => o,
Err(_) => return Err(AppError::FileWriteError),
};
Ok(())
}
fn is_ncm(data: Vec<u8>) -> Result<(), AppError> {
let header = from_utf8(&data).map_err(|_| AppError::NotNcmFile)?;
if header != "CTENFDAM" {
Err(AppError::NotNcmFile)
} else {
Ok(())
}
}
/// 使用PKCS5Padding标准去掉填充信息
fn unpad(data: &[u8]) -> Vec<u8> {
data[..data.len() - data[data.len() - 1] as usize].to_vec()
}
}
impl Ncmfile {
/// 解密函数
#[allow(unused_assignments)]
pub fn dump(
&mut self,
outputdir: &Path,
tx: crossbeam_channel::Sender<messager::Message>,
force_save: bool,
) -> Result<(), AppError> {
let messager = messager::Messager::new(self.fullfilename.clone(), tx);
let _ = messager.send(Signals::Start);
// 获取magic header 应为CTENFDAM
trace!("读取magic header");
let magic_header = self.seekread(8)?;
// 判断是否为ncm格式的文件
trace!("判断是否为ncm格式的文件");
Self::is_ncm(magic_header)?;
// 跳过2字节
trace!("跳过2字节");
self.skip(2)?;
trace!("获取RC4密钥长度");
//小端模式读取RC4密钥长度 正常情况下应为128
let key_length = u32::from_le_bytes(
self.seekread(4)?
.try_into()
.map_err(|_| AppError::FileReadError)?,
) as u64; //数据长度不够只能使用u32 然后转化为u64
// debug!("RC4密钥长度为{}", key_length);
//读取密钥 开头应为 neteasecloudmusic
trace!("读取RC4密钥");
let mut key_data = self.seekread(key_length)?;
//aes128解密
let key_data =
&aes128_to_slice(&NEW_KEY_CORE, Self::parse_key(&mut key_data[..]).to_vec())?; //先把密钥按照字节进行0x64异或
// RC4密钥
let key_data = Self::unpad(&key_data[..])[17..].to_vec(); //去掉neteasecloudmusic
//读取meta信息的数据大小
trace!("获取meta信息数据大小");
let meta_length = u32::from_le_bytes(
self.seekread(4)?
.try_into()
.map_err(|_| AppError::FileDataError)?,
) as u64;
let _ = messager.send(Signals::GetMetaInfo);
// 读取meta信息
trace!("读取meta信息");
let meta_data = {
let mut meta_data = self.seekread(meta_length)?; //读取源数据
//字节对0x63进行异或。
for item in &mut meta_data {
*item ^= 0x63;
}
// base64解密
let mut decode_data = Vec::<u8>::new();
if base64::engine::general_purpose::STANDARD
.decode_vec(&mut meta_data[22..], &mut decode_data)
.is_err()
{
return Err(AppError::CannotReadMetaInfo);
};
// aes128解密
let aes_data = aes128_to_slice(&NEW_KEY_META, decode_data)?;
// unpadding
let json_data = match String::from_utf8(Self::unpad(&aes_data)[6..].to_vec()) {
Ok(o) => o,
Err(_) => return Err(AppError::CannotReadMetaInfo),
};
debug!("json_data: {}", json_data);
let data: Value = match serde_json::from_str(&json_data[..]) {
Ok(o) => o,
Err(_) => return Err(AppError::CannotReadMetaInfo),
}; //解析json数据
data
};
//处理文件路径
trace!("拼接文件路径");
let path = {
let filename = format!(
"{}.{}",
self.filename,
meta_data
.get("format")
.ok_or(AppError::CannotReadMetaInfo)?
.as_str()
.ok_or(AppError::CannotReadMetaInfo)?
);
// let filename = standardize_filename(filename);
debug!("文件名:{}", filename.yellow());
//链级创建输出目录
if fs::create_dir_all(outputdir).is_err() {
return Err(AppError::FileWriteError);
}
outputdir.join(filename)
};
debug!("文件路径: {:?}", path);
// 先检查是否存在
if !force_save && Path::new(&path).exists() {
return Err(AppError::ProtectFile);
}
// 跳过4个字节的校验码
// trace!("读取校验码");
// let _crc32 = u32::from_le_bytes(self.seekread(4)?.try_into().map_err(AppError::FileDataError)?) as u64;
self.skip(4)?;
// 跳过5个字节
trace!("跳过5个字节");
self.skip(5)?;
let _ = messager.send(Signals::GetCover);
// 获取图片数据的大小
trace!("获取图片数据的大小");
let image_data_length = u32::from_le_bytes(
self.seekread(4)?
.try_into()
.map_err(|_| AppError::FileDataError)?,
) as u64;
// 读取图片,并写入文件当中
let image_data = self.seekread(image_data_length)?; //读取图片数据
trace!("组成密码盒");
let key_box = {
let key_length = key_data.len();
// let key_data = Vec::from(key_data);
let mut key_box = (0..=255).collect::<Vec<u8>>();
let mut temp = 0;
let mut last_byte = 0;
let mut key_offset = 0;
for i in 0..=255 {
let swap = key_box[i as usize] as u64;
temp = (swap + last_byte + key_data[key_offset] as u64) & 0xFF;
key_offset += 1;
if key_offset >= key_length {
key_offset = 0;
}
key_box[i as usize] = key_box[temp as usize];
key_box[temp as usize] = swap as u8;
last_byte = temp;
}
// let key_box = key_box.clone();
key_box
};
//解密音乐数据
trace!("解密音乐数据");
let _ = messager.send(Signals::Decrypt);
let mut music_data: Vec<u8> = Vec::new();
loop {
let mut chunk = self.seekread_no_error(0x8000);
let chunk_length = chunk.len();
if chunk_length != 0 {
for i in 1..chunk_length + 1 {
let j = i & 0xFF;
chunk[i - 1] ^= key_box[(key_box[j] as usize
+ key_box[(key_box[j] as usize + j) & 0xff] as usize)
& 0xff]
// chunk[i - 1] ^= key_box[(key_box[j] + key_box[(key_box[j as usize] as usize + j as usize) & 0xFF]) & 0xFF];
}
//向music_data中最追加chunk
music_data.append(&mut chunk);
} else {
break;
}
}
//退出循环,写入文件
let _ = messager.send(Signals::Save);
self.save(&path, music_data)?;
{
// 保存封面
let mut tag = match Tag::new().read_from_path(&path) {
Ok(o) => o,
Err(_) => return Err(AppError::CoverCannotSave),
};
let cover = Picture {
mime_type: MimeType::Jpeg,
data: &image_data,
};
tag.set_album_cover(cover); //添加封面
let _ = tag
.write_to_path(path.to_str().ok_or(AppError::SaveError)?)
.map_err(|_| AppError::SaveError); //保存
}
info!(
"[{}] 文件已保存到: {}",
self.filename.yellow(),
path.to_str().ok_or(AppError::SaveError)?.bright_cyan()
);
info!(
"[{}]{}",
self.fullfilename.yellow(),
"解密成功".bright_green()
);
let _ = messager.send(Signals::End);
Ok(())
}
}
/// 存储元数据的结构体
#[derive(Serialize, Deserialize, Debug)]
#[allow(unused_variables, dead_code)]
struct Metadata {
//编号
#[serde(rename = "musicId", skip)] //没用过,跳过
music_id: String,
// 音乐名称
#[serde(rename = "musicName")]
music_name: String,
// 艺术家
#[serde(rename = "artist")]
music_artist: Vec<(String, String)>,
// 专辑id
#[serde(rename = "albumId")]
album_id: String,
// 专辑
#[serde(rename = "album")]
album: String,
//
#[serde(rename = "albumPicDocId", skip)]
album_pic_doc_id: String,
//
#[serde(rename = "albumPic", skip)]
album_pic: String,
// 比特率
#[serde(rename = "bitrate")]
bitrate: u128,
//
#[serde(rename = "mp3DocId", skip)]
mp3_doc_id: String,
// 时间长短
#[serde(rename = "duration")]
duration: u128,
//
#[serde(rename = "mvId")]
mv_id: String,
// 别名
#[serde(rename = "alias")]
alias: Vec<String>,
// 译名
#[serde(rename = "transNames")]
trans_names: Vec<String>,
// 音乐格式
#[serde(rename = "format")]
format: String,
}
// 存储各种密钥的结构体
#[derive(Clone)]
#[allow(dead_code)]
pub struct Key {
pub core: Vec<u8>,
pub meta: Vec<u8>,
}
// fn read_meta(file: &mut File, meta_length: u32) -> Result<Vec<u8>, Error> {}
fn convert_to_generic_arrays(input: &[u8]) -> Result<Vec<GenericArray<u8, U16>>, AppError> {
// 确保输入的长度是16的倍数
if input.len() % 16 != 0 {
return Err(AppError::FileDataError);
}
Ok(input
.chunks(16)
.map(|chunk| {
// 将每个块转换为GenericArray
GenericArray::clone_from_slice(chunk)
})
.collect())
}
/// ## AES128解密
/// 解密NCM文件的rc4密钥前记得按字节对0x64进行异或
fn aes128_to_slice<T: AsRef<[u8]>>(key: &T, blocks: Vec<u8>) -> Result<Vec<u8>, AppError> {
trace!("进行AES128解密");
let key = GenericArray::from_slice(key.as_ref());
let mut blocks = convert_to_generic_arrays(&blocks)?;
// 初始化密钥
let cipher = Aes128::new(key);
// 开始解密
cipher.decrypt_blocks(&mut blocks);
//取出解密后的值
let mut x: Vec<u8> = Vec::new();
for block in blocks.iter() {
for i in block {
x.push(i.to_owned());
}
}
Ok(x)
}
// ## 规范文件名称
// 防止创建文件失败
// 符号一一对应:
// - \ / * ? " : < > |
// - _ _ ⟨ ⟩ _
// #[allow(dead_code)]
// fn standardize_filename(old_fullfilename: String) -> String {
// trace!("格式化文件名");
// let mut new_fullfilename = String::from(old_fullfilename);
// // debug!("规范文件名:{}", new_fullfilename);
// let standard = ["\\", "/", "*", "?", "\"", ":", "<", ">", "|"];
// let resolution = ["_", "_", "", "", "", "", "⟨", "⟩", "_"];
// for i in 0..standard.len() {
// new_fullfilename =
// new_fullfilename.replace(&standard[i].to_string(), &resolution[i].to_string());
// }
// new_fullfilename
// }