网易云VIP音乐NCM文件转MP3,C语言版本

网易云VIP音乐NCM文件转MP3,C语言版本前言 网易云的 Vip 音乐下载下来 格式不是 mp3 flac 这种通用的音乐格式 而是经过加密的 ncm 文件 只有用网易云的音乐 App 才能够打开 于是想到可不可以把 ncm 文件转换成 mp3 或者 flac 文件 上 google 查了一下 发现有不少人已经做了这件事 但没有发现 C 语言版本的

大家好,我是讯享网,很高兴认识大家。

前言

网易云的Vip音乐下载下来,格式不是mp3/flac这种通用的音乐格式,而是经过加密的ncm文件。只有用网易云的音乐App才能够打开。于是想到可不可以把.ncm文件转换成mp3或者flac文件,上google查了一下,发现有不少人已经做了这件事,但没有发现C语言版本的,就想着写一个纯C语言版本的ncm转mp3/flac。

NCM文件结构

ncm文件的结构,网上有人解析出来了,分为下面几个部分

信息 大小 说明
Magic Header 10 bytes 文件头
Key Length 4 bytes AES128加密后的RC4密钥长度,字节是按小端排序。
Key Data Key Length 用AES128加密后的RC4密钥。
1. 先按字节对0x64进行异或。
2. AES解密,去除填充部分。
3. 去除最前面’neteasecloudmusic’17个字节,得到RC4密钥。
Music Info Length 4 bytes 音乐相关信息的长度,小端排序。
Music Info Data Music Info Length Json格式音乐信息数据。1. 按字节对0x63进行异或。
2. 去除最前面22个字节。
3. Base64进行解码。
4. AES解密。
6. 去除前面6个字节得到Json数据。
CRC 4 bytes 跳过
Gap 5 bytes 跳过
Image Size 4 bytes 图片的大小
Image Image Size 图片数据
Music Data - 1. RC4-KSA生成S盒。
2. 用S盒解密(自定义的解密方法),不是RC4-PRGA解密。

两个AES对应密钥
unsigned char meta_key[] = { 0x23,0x31,0x34,0x6C,0x6A,0x6B,0x5F,0x21,0x5C,0x5D,0x26,0x30,0x55,0x3C,0x27,0x28 };
unsigned char core_key[] = { 0x68,0x7A,0x48,0x52,0x41,0x6D,0x73,0x6F,0x35,0x6B,0x49,0x6E,0x62,0x61,0x78,0x57 };
不得不佩服当初激活成功教程这个东西的人,不仅把文件结构摸得请清楚楚,还把密钥也搞到手,应该是个激活成功教程大神。有了上面的东西,剩下的就很简单了,按部就班来就行了。

一些算法准备

开始前我们需要把AES算法,BASE64算法,RC4算法和Json解析算法先写好。
除此之外还有一个编码问题,解析出来的ncm文件是用utf-8编码存储的,所以它在中文windows系统下汉字会出现乱码,因为中文windows系统采用的编码是GBK,两者不兼容,所以我们要写一个编码转换算法,将utf8格式字符串转位GBK的。Linux下不用转换,Linux本身就是用UTF-8的。
C语言没有这些库,都要自己来。


讯享网

  • AES用GitHub上的
    tiny-AES-c
  • JSON用GitHub上的CJSON
    cJSON
  • Base64和RC4算法比较简单我们自己写
unsigned char* base64_decode(unsigned char* code,int len,int * actLen) { //根据base64表,以字符找到对应的十进制数据 int table[] = { 0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,62,0,0,0, 63,52,53,54,55,56,57,58, 59,60,61,0,0,0,0,0,0,0,0, 1,2,3,4,5,6,7,8,9,10,11,12, 13,14,15,16,17,18,19,20,21, 22,23,24,25,0,0,0,0,0,0,26, 27,28,29,30,31,32,33,34,35, 36,37,38,39,40,41,42,43,44, 45,46,47,48,49,50,51 }; long str_len; unsigned char* res; int i, j; //计算解码后的字符串长度 //判断编码后的字符串后是否有= if (strstr(code, "==")) str_len = len / 4 * 3 - 2; else if (strstr(code, "=")) str_len = len / 4 * 3 - 1; else str_len = len / 4 * 3; *actLen = str_len; res = malloc(sizeof(unsigned char) * str_len + 1); res[str_len] = '\0'; //以4个字符为一位进行解码 for (i = 0, j = 0; i < len - 2; j += 3, i += 4) { res[j] = ((unsigned char)table[code[i]]) << 2 | (((unsigned char)table[code[i + 1]]) >> 4); res[j + 1] = (((unsigned char)table[code[i + 1]]) << 4) | (((unsigned char)table[code[i + 2]]) >> 2); res[j + 2] = (((unsigned char)table[code[i + 2]]) << 6) | ((unsigned char)table[code[i + 3]]); } return res; } 

讯享网
  • RC4生成S盒
讯享网//用key生成S盒 /* * s: s盒 * key: 密钥 * len: 密钥长度 */ void rc4Init(unsigned char* s, const unsigned char* key, int len) { int i = 0, j = 0; unsigned char T[256] = { 0 }; for (i = 0; i < 256; i++) { s[i] = i; T[i] = key[i % len]; } for (i = 0; i < 256; i++) { j = (j + s[i] + T[i]) % 256; unsigned tmp = s[i]; s[i]=s[j]; s[j]=tmp; } } //针对NCM文件的解密 //异或关系 /* * s: s盒 * data: 要加密或者解密的数据 * len: data的长度 */ void rc4PRGA(unsigned char* s, unsigned char* data, int len) { int i = 0; int j = 0; int k = 0; int idx = 0; for (idx = 0; idx < len; idx++) { i = (idx + 1) % 256; j = (i + s[i]) % 256; k= (s[i] + s[j]) % 256; data[idx]^=s[k]; //异或 } } 
  • Windows下utf8转GBK
#ifdef WIN32 #include<Windows.h> //返回转换好的字符串指针 unsigned char* utf8ToGbk(unsigned char*src,int len) { wchar_t* tmp = (wchar_t*)malloc(sizeof(wchar_t) * len+2); unsigned char* newSrc = (unsigned char*)malloc(sizeof(unsigned char) * len + 2); MultiByteToWideChar(CP_UTF8, 0, src, -1, tmp, len); WideCharToMultiByte(CP_ACP, 0, tmp, -1, newSrc, len+2, NULL,NULL); return newSrc; } #endif 

NCM文件解析

按照NCM文件结构一步一步读取数据来进行解析

讯享网//fileName:要转换的文件 void readFileData(const char* fileName) { FILE* f; f = fopen(fileName, "rb"); if (!f) { printf("No such file: %s\n", fileName); return; } unsigned char buf[16]; int len=0; int i = 0; unsigned char meta_key[] = { 0x23,0x31,0x34,0x6C,0x6A,0x6B,0x5F,0x21,0x5C,0x5D,0x26,0x30,0x55,0x3C,0x27,0x28 }; unsigned char core_key[] = { 0x68,0x7A,0x48,0x52,0x41,0x6D,0x73,0x6F,0x35,0x6B,0x49,0x6E,0x62,0x61,0x78,0x57 }; fseek(f, 10, SEEK_CUR); //f从当前位置移动10个字节 fread(buf, 1, 4, f); //读取rc4 key 的长度 len = (buf[3] << 8 | buf[2]) << 16 | (buf[1] << 8 | buf[0]); unsigned char* rc4Key= (unsigned char*)malloc(sizeof(unsigned char) * len); fread(rc4Key, 1, len, f); //读取rc4数据 //解密rc4密钥 for (i = 0; i < len; i++) { rc4Key[i] ^= 0x64; } struct AES_ctx ctx; AES_init_ctx(&ctx, core_key); //使用core_key密钥 int packSize = len / 16; //采用的是AES-ECB加密方式,和Pkcs7padding填充 for (i = 0; i < packSize; i++) { AES_ECB_decrypt(&ctx, &rc4Key[i * 16]); } int pad = rc4Key[len - 1]; //获取填充的长度 rc4Key[len - pad] = '\0'; //去除填充的部分,得到RC4密钥 fread(buf, 1, 4, f); //读取Music Info 长度数据 len = ((buf[3] << 8 | buf[2]) << 16) | (buf[1] << 8 | buf[0]); unsigned char* meta = (unsigned char*)malloc(sizeof(unsigned char) * len); fread(meta, 1, len, f); //读取Music Info数据 //解析Music info信息 for (i = 0; i < len; i++) { meta[i] ^= 0x63; } int act = 0; unsigned char* data = base64_decode(&meta[22], len - 22, &act); //base64解码 AES_init_ctx(&ctx, meta_key); //AES解密 packSize = act / 16; for (i = 0; i < packSize; i++) { AES_ECB_decrypt(&ctx, &data[i * 16]); } pad = data[act - 1]; data[act - pad] = '\0'; //去除填充部分 unsigned char* newData = data; #ifdef WIN32 newData = utf8ToGbk(data, strlen(data)); #endif cJSON* cjson = cJSON_Parse(&newData[6]); //json解析,获取格式和名字等 if (cjson == NULL) { printf("cjson parse failed\n"); return; } //printf("%s\n", cJSON_Print(cjson)); //输出json fseek(f, 9, SEEK_CUR); //从当前位置跳过9个字节 fread(buf, 1, 4, f); //读取图片大小 len = (buf[3] << 8 | buf[2]) << 16 | (buf[1] << 8 | buf[0]); unsigned char* img = (unsigned char*)malloc(sizeof(unsigned char) * len); fread(img, 1, len, f); //读取图片数据 int offset= 1024 * 1024 * 10; //10MB 音乐数据一般比较大一次读入10MB int total = 0; int reSize = offset; unsigned char* musicData = (unsigned char*)malloc(offset); //10m while (!feof(f)) { len = fread(musicData+total, 1, offset, f); //每次读取10M total += len; reSize += offset; musicData=realloc(musicData,reSize); //扩容 } unsigned char sBox[256] = { 0 }; //s盒 rc4Init(sBox, &rc4Key[17], strlen(&rc4Key[17])); //用rC4密钥进行初始化s盒 rc4PRGA(sBox, musicData, total); //解密 //拼接文件名(artist + music name+format) char* musicName = cJSON_GetObjectItem(cjson, "musicName")->valuestring; cJSON* sub = cJSON_GetObjectItem(cjson, "artist"); char*artist=cJSON_GetArrayItem(cJSON_GetArrayItem(sub, 0),0)->valuestring; char* format = cJSON_GetObjectItem(cjson, "format")->valuestring; char* saveFileName =(char*)malloc(strlen(musicName) + strlen(artist) + strlen(format)+5); sprintf(saveFileName, "%s - %s.%s", artist, musicName, format); FILE* fo=fopen(saveFileName, "wb"); if (fo == NULL) { printf("The fileName - '%s' is invalid in this system\n", saveFileName); } else { fwrite(musicData, 1, total, fo); fclose(fo); } #ifdef WIN32 free(newData); #endif free(data); free(meta); free(img); free(musicData); fclose(f); } 
  1. AES采用的是AES-ECB模式,pack7padding填充方式。即16个字节为一组,如果不够16个字节,那就缺几个字节就填充几个字节,每个字节的值都是缺少的字节数。所以获取最后一个字节的值就知道要填充了几个字节。
  2. RC4解密那里,不是按RC4的来的,虽说叫RC4,但只有生成S盒那里是一样的,其它的不是按RC4算法来的。
  3. 有些解析出来音乐的名字,系统是不支持的,比如带’/'的,在创建新文件写入时会失败。
  4. 以"結束バンド - ギターと孤独と蒼い惑星.ncm"为例看看它的json数据是怎么样的

{
“musicId”: ,
“musicName”: “ギターと孤独と蒼い惑星”,
“artist”: [[“結束バンド”, ]],
“albumId”: ,
“album”: “ギターと孤独と蒼い惑星”,
“albumPicDocId”: “”,
“albumPic”: “https://p4.music.126.net/rfstzrVK05hCPjU-4mzSFA==/.jpg”,
“bitrate”: ,
“mp3DocId”: “f481d20151f01d5d681d2768d753ad64”,
“duration”: ,
“mvId”: 0,
“alias”: [“TV动画《孤独摇滚!》插曲”],
“transNames”: [],
“format”: “mp3”,
“flag”: 4
}

可以根据需要自由提取需要的信息

完整代码

点击查看代码
/* * date:2022-12-12 * author: FL * purpose: ncm file to mp3 */ #include <stdio.h> #include <string.h> #include <stdlib.h> #include "aes.h" #include "cJSON.h" #ifdef WIN32 #include<Windows.h> //返回转换好的字符串指针 unsigned char* utf8ToGbk(unsigned char*src,int len) { wchar_t* tmp = (wchar_t*)malloc(sizeof(wchar_t) * len+2); unsigned char* newSrc = (unsigned char*)malloc(sizeof(unsigned char) * len + 2); MultiByteToWideChar(CP_UTF8, 0, src, -1, tmp, len); //转为unicode WideCharToMultiByte(CP_ACP, 0, tmp, -1, newSrc, len+2, NULL,NULL); //转gbk return newSrc; } #endif void swap(unsigned char* a, unsigned char* b) { unsigned char t = *a; *a = *b; *b = t; } //用key生成S盒 /* * s: s盒 * key: 密钥 * len: 密钥长度 */ void rc4Init(unsigned char* s, const unsigned char* key, int len) { int i = 0, j = 0; unsigned char T[256] = { 0 }; for (i = 0; i < 256; i++) { s[i] = i; T[i] = key[i % len]; } for (i = 0; i < 256; i++) { j = (j + s[i] + T[i]) % 256; swap(s + i, s + j); } } //针对NCM文件的解密 //异或关系 /* * s: s盒 * data: 要加密或者解密的数据 * len: data的长度 */ void rc4PRGA(unsigned char* s, unsigned char* data, int len) { int i = 0; int j = 0; int k = 0; int idx = 0; for (idx = 0; idx < len; idx++) { i = (idx + 1) % 256; j = (i + s[i]) % 256; k = (s[i] + s[j]) % 256; data[idx] ^= s[k]; //异或 } } //base64 解码 /* * code: 要解码的数据 */ unsigned char* base64_decode(unsigned char* code, int len, int* actLen) { //根据base64表,以字符找到对应的十进制数据 int table[] = { 0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,62,0,0,0, 63,52,53,54,55,56,57,58, 59,60,61,0,0,0,0,0,0,0,0, 1,2,3,4,5,6,7,8,9,10,11,12, 13,14,15,16,17,18,19,20,21, 22,23,24,25,0,0,0,0,0,0,26, 27,28,29,30,31,32,33,34,35, 36,37,38,39,40,41,42,43,44, 45,46,47,48,49,50,51 }; long str_len; unsigned char* res; int i, j; //计算解码后的字符串长度 //判断编码后的字符串后是否有= if (strstr(code, "==")) str_len = len / 4 * 3 - 2; else if (strstr(code, "=")) str_len = len / 4 * 3 - 1; else str_len = len / 4 * 3; *actLen = str_len; res = malloc(sizeof(unsigned char) * str_len + 1); res[str_len] = '\0'; //以4个字符为一位进行解码 for (i = 0, j = 0; i < len - 2; j += 3, i += 4) { res[j] = ((unsigned char)table[code[i]]) << 2 | (((unsigned char)table[code[i + 1]]) >> 4); res[j + 1] = (((unsigned char)table[code[i + 1]]) << 4) | (((unsigned char)table[code[i + 2]]) >> 2); res[j + 2] = (((unsigned char)table[code[i + 2]]) << 6) | ((unsigned char)table[code[i + 3]]); } return res; } void readFileData(const char* fileName) { FILE* f; f = fopen(fileName, "rb"); if (!f) { printf("No such file: %s\n", fileName); return; } unsigned char buf[16]; int len=0; int i = 0; unsigned char meta_key[] = { 0x23,0x31,0x34,0x6C,0x6A,0x6B,0x5F,0x21,0x5C,0x5D,0x26,0x30,0x55,0x3C,0x27,0x28 }; unsigned char core_key[] = { 0x68,0x7A,0x48,0x52,0x41,0x6D,0x73,0x6F,0x35,0x6B,0x49,0x6E,0x62,0x61,0x78,0x57 }; fseek(f, 10, SEEK_CUR); //f从当前位置移动10个字节 fread(buf, 1, 4, f); //读取rc4 key 的长度 len = (buf[3] << 8 | buf[2]) << 16 | (buf[1] << 8 | buf[0]); unsigned char* rc4Key= (unsigned char*)malloc(sizeof(unsigned char) * len); fread(rc4Key, 1, len, f); //读取rc4数据 //解密rc4密钥 for (i = 0; i < len; i++) { rc4Key[i] ^= 0x64; } struct AES_ctx ctx; AES_init_ctx(&ctx, core_key); //使用core_key密钥 int packSize = len / 16; //采用的是AES-ECB加密方式,和Pkcs7padding填充 for (i = 0; i < packSize; i++) { AES_ECB_decrypt(&ctx, &rc4Key[i * 16]); } int pad = rc4Key[len - 1]; //获取填充的长度 rc4Key[len - pad] = '\0'; //去除填充的部分,得到RC4密钥 fread(buf, 1, 4, f); //读取Music Info 长度数据 len = ((buf[3] << 8 | buf[2]) << 16) | (buf[1] << 8 | buf[0]); unsigned char* meta = (unsigned char*)malloc(sizeof(unsigned char) * len); fread(meta, 1, len, f); //读取Music Info数据 //解析Music info信息 for (i = 0; i < len; i++) { meta[i] ^= 0x63; } int act = 0; unsigned char* data = base64_decode(&meta[22], len - 22, &act); //base64解码 AES_init_ctx(&ctx, meta_key); //AES解密 packSize = act / 16; for (i = 0; i < packSize; i++) { AES_ECB_decrypt(&ctx, &data[i * 16]); } pad = data[act - 1]; data[act - pad] = '\0'; //去除填充部分 unsigned char* newData = data; #ifdef WIN32 newData = utf8ToGbk(data, strlen(data)); #endif cJSON* cjson = cJSON_Parse(&newData[6]); //json解析,获取格式和名字等 if (cjson == NULL) { printf("cjson parse failed\n"); return; } //printf("%s\n", cJSON_Print(cjson)); //输出json fseek(f, 9, SEEK_CUR); //从当前位置跳过9个字节 fread(buf, 1, 4, f); //读取图片大小 len = (buf[3] << 8 | buf[2]) << 16 | (buf[1] << 8 | buf[0]); unsigned char* img = (unsigned char*)malloc(sizeof(unsigned char) * len); fread(img, 1, len, f); //读取图片数据 int offset= 1024 * 1024 * 10; //10MB 音乐数据一般比较大一次读入10MB int total = 0; int reSize = offset; unsigned char* musicData = (unsigned char*)malloc(offset); //10m while (!feof(f)) { len = fread(musicData+total, 1, offset, f); //每次读取10M total += len; reSize += offset; musicData=realloc(musicData,reSize); //扩容 } unsigned char sBox[256] = { 0 }; //s盒 rc4Init(sBox, &rc4Key[17], strlen(&rc4Key[17])); //用rC4密钥进行初始化s盒 rc4PRGA(sBox, musicData, total); //解密 //拼接文件名(artist + music name+format) char* musicName = cJSON_GetObjectItem(cjson, "musicName")->valuestring; cJSON* sub = cJSON_GetObjectItem(cjson, "artist"); char*artist=cJSON_GetArrayItem(cJSON_GetArrayItem(sub, 0),0)->valuestring; char* format = cJSON_GetObjectItem(cjson, "format")->valuestring; char* saveFileName =(char*)malloc(strlen(musicName) + strlen(artist) + strlen(format)+5); sprintf(saveFileName, "%s - %s.%s", artist, musicName, format); FILE* fo=fopen(saveFileName, "wb"); if (fo == NULL) { printf("The fileName - '%s' is invalid in this system\n", saveFileName); } else { fwrite(musicData, 1, total, fo); fclose(fo); } #ifdef WIN32 free(newData); #endif free(data); free(meta); free(img); free(musicData); fclose(f); } int main(int argc,charargv) { readFileData("結束バンド - ギターと孤独と蒼い惑星.ncm"); return 0; } 

GitHub项目

ncmToMp3
小讯
上一篇 2025-02-19 18:29
下一篇 2025-03-06 21:01

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/74967.html