前言 網易雲的Vip音樂下載下來,格式不是mp3/flac這種通用的音樂格式,而是經過加密的ncm文件。只有用網易雲的音樂App才能夠打開。於是想到可不可以把.ncm文件轉換成mp3或者flac文件,上google查了一下,發現有不少人已經做了這件事,但沒有發現C語言版本的,就想著寫一個純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);
}
- AES採用的是AES-ECB模式,pack7padding填充方式。即16個位元組為一組,如果不夠16個位元組,那就缺幾個位元組就填充幾個位元組,每個位元組的值都是缺少的位元組數。所以獲取最後一個位元組的值就知道要填充了幾個位元組。
- RC4解密那裡,不是按RC4的來的,雖說叫RC4,但只有生成S盒那裡是一樣的,其它的不是按RC4演算法來的。
- 有些解析出來音樂的名字,系統是不支持的,比如帶'/'的,在創建新文件寫入時會失敗。
- 以"結束バンド - ギターと孤獨と蒼い惑星.ncm"為例看看它的json數據是怎麼樣的
{
"musicId": 1991012773,
"musicName": "ギターと孤獨と蒼い惑星",
"artist": [["結束バンド", 54103171]],
"albumId": 153542094,
"album": "ギターと孤獨と蒼い惑星",
"albumPicDocId": "109951167983448236",
"albumPic": "https://p4.music.126.net/rfstzrVK05hCPjU-4mzSFA==/109951167983448236.jpg",
"bitrate": 320000,
"mp3DocId": "f481d20151f01d5d681d2768d753ad64",
"duration": 229015,
"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,char**argv)
{
readFileData("結束バンド - ギターと孤獨と蒼い惑星.ncm");
return 0;
}