ncmc_lib/
lib.rs

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
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
//! Netease Cloud Music Crypt

mod error;

use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyInit};
use base64::{prelude::BASE64_STANDARD, Engine};
use ecb::Decryptor;
use error::{NcmError, Result};
use std::{
    fs::File,
    io::{Read, Seek, Write as _},
    path::{Path, PathBuf},
};

const CORE_KEY: &[u8; 16] = b"hzHRAmso5kInbaxW";
const META_KEY: &[u8; 16] = br#"#14ljk_!\]&0U<'("#;
const KEY_MASK: u8 = 0x64;
const META_MASK: u8 = 0x63;

/// Ncm file
#[derive(Debug)]
pub struct NcmFile {
    file: File,
    path: PathBuf,
    key: Vec<u8>,
    meta: Vec<u8>,
}

impl NcmFile {
    /// Open a ncm file
    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
        let path = path.as_ref().to_owned();
        let mut file = File::open(&path).unwrap();
        Self::verify_header(&mut file)?;
        let key = Self::get_key(&mut file)?;
        let meta = Self::get_meta(&mut file)?;
        file.seek_relative(9)?; // CRC(4) and Padding(5)
        Self::skip_image(&mut file)?;
        Ok(Self {
            file,
            path,
            key,
            meta,
        })
    }

    fn verify_header(file: &mut File) -> Result<()> {
        let mut buf = [0; 10];
        file.read_exact(&mut buf)?;
        if &buf[..8] != b"CTENFDAM" {
            return Err(NcmError::Invalid);
        }
        Ok(())
    }

    fn get_key(file: &mut File) -> Result<Vec<u8>> {
        let mut buf = [0; 4];
        file.read_exact(&mut buf)?;
        let length = u32::from_le_bytes(buf) as usize;
        let mut buf = vec![0; length];
        file.read_exact(&mut buf)?;
        buf.iter_mut().for_each(|byte| *byte ^= KEY_MASK);
        let aes = Decryptor::<aes::Aes128>::new_from_slice(CORE_KEY).unwrap();
        let buf = aes
            .decrypt_padded_mut::<Pkcs7>(&mut buf)
            .map_err(|_| NcmError::Invalid)?;
        if &buf[..17] != b"neteasecloudmusic" {
            return Err(NcmError::Invalid);
        }
        let key_data = &buf[17..];
        let mut key_box: [u8; 256] = core::array::from_fn(|i| i as u8);
        let mut last_byte = 0;
        let mut key_offset = 0;
        for i in 0..256 {
            let c = key_box[i]
                .wrapping_add(last_byte)
                .wrapping_add(key_data[key_offset]);
            key_offset += 1;
            if key_offset >= key_data.len() {
                key_offset = 0;
            }
            key_box.swap(i, c as usize);
            last_byte = c;
        }
        Ok(key_box.to_vec())
    }

    fn get_meta(file: &mut File) -> Result<Vec<u8>> {
        let mut buf = [0; 4];
        file.read_exact(&mut buf)?;
        let length = u32::from_le_bytes(buf) as usize;
        let mut buf = vec![0; length];
        file.read_exact(&mut buf)?;
        buf.iter_mut().for_each(|byte| *byte ^= META_MASK);
        if &buf[..22] != b"163 key(Don't modify):" {
            return Err(NcmError::Invalid);
        }
        let mut buf = BASE64_STANDARD
            .decode(&buf[22..])
            .map_err(|_| NcmError::Invalid)?;
        let aes = Decryptor::<aes::Aes128>::new_from_slice(META_KEY).unwrap();
        let buf = aes
            .decrypt_padded_mut::<Pkcs7>(&mut buf)
            .map_err(|_| NcmError::Invalid)?;
        if &buf[..6] != b"music:" {
            return Err(NcmError::Invalid);
        }
        Ok(buf[6..].to_vec())
    }

    fn skip_image(file: &mut File) -> Result<()> {
        let mut buf = [0; 4];
        file.read_exact(&mut buf)?;
        let length = u32::from_le_bytes(buf) as i64;
        file.seek_relative(length)?;
        Ok(())
    }

    /// save as general format next to the original ncm file
    pub fn save(&mut self) -> Result<()> {
        let meta = serde_json::from_slice::<serde_json::Value>(&self.meta[..])
            .map_err(|_| NcmError::Invalid)?;
        let format = meta["format"].as_str().ok_or(NcmError::Invalid)?;
        let mut file = std::fs::File::create(self.path.with_extension(format))?;
        let mut buf = vec![];
        let size = self.file.read_to_end(&mut buf)?;
        for i in 1..size + 1 {
            let j = i & 0xff;
            buf[i - 1] ^= self.key[self.key[j]
                .wrapping_add(self.key[self.key[j].wrapping_add(j as u8) as usize])
                as usize];
        }
        file.write_all(&buf)?;
        file.flush()?;
        Ok(())
    }
}