feat: basic ncm support
This commit is contained in:
parent
7b283a5a14
commit
16ca02a28c
@ -8,6 +8,7 @@
|
||||
<sourceFolder url="file://$MODULE_DIR$/um_crypto/qmc/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/um_cli/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/um_crypto/utils/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/um_crypto/ncm/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
|
139
Cargo.lock
generated
139
Cargo.lock
generated
@ -2,6 +2,17 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.15"
|
||||
@ -63,6 +74,15 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "block-padding"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.16.0"
|
||||
@ -90,6 +110,16 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.17"
|
||||
@ -146,12 +176,62 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc"
|
||||
version = "3.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
|
||||
dependencies = [
|
||||
"crc-catalog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc-catalog"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.15"
|
||||
@ -171,6 +251,16 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
|
||||
dependencies = [
|
||||
"block-padding",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
@ -229,6 +319,16 @@ version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "pretty_assertions"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
|
||||
dependencies = [
|
||||
"diff",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.86"
|
||||
@ -247,6 +347,12 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhexdump"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a92cbe1a3dd221e3777fef339115d3b85e17a538668e03a0f3ae9c6a98351c7"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
@ -315,6 +421,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||
|
||||
[[package]]
|
||||
name = "um_cli"
|
||||
version = "0.1.0"
|
||||
@ -351,6 +463,21 @@ dependencies = [
|
||||
"umc_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "umc_ncm"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"byteorder",
|
||||
"cipher",
|
||||
"crc",
|
||||
"itertools",
|
||||
"pretty_assertions",
|
||||
"rhexdump",
|
||||
"thiserror",
|
||||
"umc_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "umc_qmc"
|
||||
version = "0.1.0"
|
||||
@ -383,6 +510,12 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
@ -592,3 +725,9 @@ name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
||||
|
17
um_crypto/ncm/Cargo.toml
Normal file
17
um_crypto/ncm/Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "umc_ncm"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
aes = "0.8.4"
|
||||
byteorder = "1.5.0"
|
||||
cipher = { version = "0.4.4", features = ["block-padding"] }
|
||||
crc = "3.2.1"
|
||||
itertools = "0.13.0"
|
||||
thiserror = "1.0.63"
|
||||
umc_utils = { path = "../utils" }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1"
|
||||
rhexdump = "0.2.0"
|
23
um_crypto/ncm/Readme.MD
Normal file
23
um_crypto/ncm/Readme.MD
Normal file
@ -0,0 +1,23 @@
|
||||
# NCM Decoder
|
||||
|
||||
## Glossary
|
||||
|
||||
- LV: Length-Value Encoding, `length` is u32 in Little-Endian.
|
||||
|
||||
## File Format
|
||||
|
||||
- Magic: `CTENFDAM`
|
||||
- NCM Version: `01` (u8)
|
||||
- App version: `??` (u8)
|
||||
- ContentKey (LV)
|
||||
- Metadata (LV)
|
||||
- CRC32 (of all previous data)
|
||||
- Cover Block
|
||||
- Encrypted Audio
|
||||
|
||||
### Cover Block
|
||||
|
||||
- Length: `u32: frame_len = len(image1 + image2)`
|
||||
- Length: `u32: img1_len = len(image1)`
|
||||
- Data: `u8[img1_len]: image1`
|
||||
- Data: `u8[frame_len - img1_len]: image2` - unknown format
|
BIN
um_crypto/ncm/src/__fixture__/ncm_test1.bin
Normal file
BIN
um_crypto/ncm/src/__fixture__/ncm_test1.bin
Normal file
Binary file not shown.
56
um_crypto/ncm/src/content_key.rs
Normal file
56
um_crypto/ncm/src/content_key.rs
Normal file
@ -0,0 +1,56 @@
|
||||
use crate::NetEaseCryptoError;
|
||||
use aes::cipher::generic_array::GenericArray;
|
||||
use aes::cipher::KeyInit;
|
||||
use aes::Aes128Dec;
|
||||
use cipher::block_padding::Pkcs7;
|
||||
use cipher::BlockDecryptMut;
|
||||
use itertools::Itertools;
|
||||
|
||||
const CONTENT_KEY: [u8; 0x10] = *b"hzHRAmso5kInbaxW";
|
||||
|
||||
/// Decrypt content key
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key`: Encrypted content_key
|
||||
///
|
||||
/// returns: Result<Vec<u8, Global>, NetEaseCryptoError>
|
||||
///
|
||||
pub fn decrypt<T>(key: T) -> Result<Vec<u8>, NetEaseCryptoError>
|
||||
where
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
let mut data = key.as_ref().iter().map(|&b| b ^ 0x64).collect_vec();
|
||||
|
||||
let aes = Aes128Dec::new(&GenericArray::from(CONTENT_KEY));
|
||||
let content_key = aes
|
||||
.decrypt_padded_mut::<Pkcs7>(&mut data[..])
|
||||
.map_err(NetEaseCryptoError::ContentKeyDecryptError)?;
|
||||
|
||||
let key = match content_key.strip_prefix(b"neteasecloudmusic") {
|
||||
None => Err(NetEaseCryptoError::ContentKeyWrongPrefix(
|
||||
String::from_utf8_lossy(content_key).into(),
|
||||
))?,
|
||||
Some(key) => key,
|
||||
};
|
||||
|
||||
Ok(Vec::from(key))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decrypt_content_key() {
|
||||
let enciphered_key = [
|
||||
0x2C, 0xCE, 0xD5, 0xEB, 0x69, 0xEA, 0xFB, 0x14, 0x55, 0x0D, 0x45, 0xBF, 0x61, 0xDD, 0x17,
|
||||
0x1D, 0x93, 0x71, 0x47, 0x1E, 0xE1, 0xDD, 0xDA, 0xF4, 0xD5, 0xE8, 0x4F, 0x1C, 0xBA, 0x00,
|
||||
0x20, 0xC3, 0x02, 0xE9, 0xFE, 0x29, 0x92, 0xE1, 0x81, 0x45, 0x6F, 0x18, 0xC7, 0x2D, 0x11,
|
||||
0xF2, 0xBC, 0x5B, 0xBC, 0xDC, 0x22, 0x33, 0xF9, 0x68, 0xB4, 0xB0, 0x28, 0x38, 0x3F, 0x63,
|
||||
0x6C, 0x88, 0x66, 0x35, 0xF9, 0xE7, 0xB1, 0x70, 0x0E, 0xEE, 0x55, 0xAC, 0xB8, 0xED, 0x8B,
|
||||
0x48, 0x17, 0x25, 0x3A, 0xE6, 0x5E, 0xB5, 0x80, 0x78, 0x8A, 0xCD, 0xDC, 0xE1, 0xEF, 0x3D,
|
||||
0x30, 0xEC, 0x9C, 0x2A, 0xC6, 0xC7, 0x51, 0xAE, 0x3D, 0x11, 0xB5, 0x64, 0x88, 0x9E, 0xD6,
|
||||
0x77, 0x66, 0xF6, 0x2B, 0x52, 0x9E, 0xFA, 0xF9, 0x63, 0xF6, 0xDE, 0x27, 0x10, 0x45, 0x82,
|
||||
0xAC, 0x2D, 0x20, 0x84, 0x95, 0x4C, 0x0F, 0x7A, 0xAE, 0x8B, 0x91, 0x6D, 0x10, 0x2E, 0x63,
|
||||
0x1C, 0xEA, 0xCA, 0xF9, 0x14, 0x97, 0xD8, 0xB3, 0xE8,
|
||||
];
|
||||
let key = decrypt(enciphered_key).expect("Failed to decrypt content");
|
||||
assert_eq!(key, b"174279197715752960061821572626E7fT49x7dof9OKCgg9cdvhEuezy3iZCL1nFvBFd1T4uSktAJKmwZXsijPbijliionVUXXg9plTbXEclAE9Lb");
|
||||
}
|
160
um_crypto/ncm/src/header.rs
Normal file
160
um_crypto/ncm/src/header.rs
Normal file
@ -0,0 +1,160 @@
|
||||
use crate::{content_key, NetEaseCryptoError as Error};
|
||||
use byteorder::{ByteOrder, LE};
|
||||
|
||||
const CRC32: crc::Crc<u32> = crc::Crc::<u32>::new(&crc::CRC_32_ISO_HDLC);
|
||||
|
||||
struct NCMFile {
|
||||
pub ncm_version: u8,
|
||||
pub client_version: u8,
|
||||
/// Encrypted content key
|
||||
pub content_key: Vec<u8>,
|
||||
/// Encrypted metadata.
|
||||
pub metadata: Vec<u8>,
|
||||
/// Cover image 1
|
||||
pub image1: Option<Vec<u8>>,
|
||||
/// Cover image 2, format unknown
|
||||
pub image2: Option<Vec<u8>>,
|
||||
audio_rc4_key_stream: [u8; 256],
|
||||
pub audio_data_offset: usize,
|
||||
}
|
||||
|
||||
fn build_audio_rc4_key_stream(enciphered_content_key: &[u8]) -> Result<[u8; 256], Error> {
|
||||
let key = content_key::decrypt(enciphered_content_key)?;
|
||||
|
||||
let mut s = [0u8; 256];
|
||||
s.iter_mut().enumerate().for_each(|(i, b)| *b = i as u8);
|
||||
|
||||
{
|
||||
let mut j = 0u8;
|
||||
for (i, &k) in (0..256).zip(key.iter().cycle()) {
|
||||
j = j.wrapping_add(s[i]).wrapping_add(k);
|
||||
s.swap(i, j as usize);
|
||||
}
|
||||
}
|
||||
|
||||
let mut key = [0u8; 256];
|
||||
for (i, k) in key.iter_mut().enumerate() {
|
||||
let j = s[i].wrapping_add(i as u8);
|
||||
let idx = s[i].wrapping_add(s[j as usize]);
|
||||
*k = s[idx as usize];
|
||||
}
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
impl NCMFile {
|
||||
pub fn new<T>(header: T) -> Result<Self, Error>
|
||||
where
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
let header = header.as_ref();
|
||||
if header.len() < 14 {
|
||||
Err(Error::HeaderTooSmall(14 - header.len()))?;
|
||||
}
|
||||
if !header.starts_with(b"CTENFDAM") {
|
||||
Err(Error::NotNCMFile)?;
|
||||
}
|
||||
let ncm_version = header[8];
|
||||
let client_version = header[9];
|
||||
let content_key_len = LE::read_u32(&header[10..14]) as usize;
|
||||
|
||||
let offset = 14;
|
||||
if header.len() < offset + content_key_len + 4 {
|
||||
Err(Error::HeaderTooSmall(offset + content_key_len + 4))?;
|
||||
}
|
||||
let content_key = &header[offset..offset + content_key_len];
|
||||
let offset = offset + content_key_len;
|
||||
|
||||
let metadata_len = LE::read_u32(&header[offset..offset + 4]) as usize;
|
||||
let offset = offset + 4;
|
||||
|
||||
// 9: crc32 + cover_version + frame_size
|
||||
if header.len() < offset + metadata_len + 9 {
|
||||
Err(Error::HeaderTooSmall(offset + metadata_len + 9))?;
|
||||
}
|
||||
let metadata = &header[offset..offset + metadata_len];
|
||||
let offset = offset + metadata_len;
|
||||
|
||||
let expected_crc32 = LE::read_u32(&header[offset..offset + 4]);
|
||||
let actual_checksum = CRC32.checksum(&header[..offset]);
|
||||
if actual_checksum != expected_crc32 {
|
||||
Err(Error::ChecksumInvalid {
|
||||
expected: expected_crc32,
|
||||
actual: actual_checksum,
|
||||
})?;
|
||||
}
|
||||
let offset = offset + 4;
|
||||
|
||||
let cover_version = header[offset];
|
||||
match cover_version {
|
||||
1 => {}
|
||||
v => Err(Error::UnsupportedCoverImageVersion(v))?,
|
||||
};
|
||||
let offset = offset + 1;
|
||||
|
||||
let cover_frame_len = LE::read_u32(&header[offset..offset + 4]) as usize;
|
||||
if header.len() < offset + cover_frame_len + 4 {
|
||||
Err(Error::HeaderTooSmall(offset + cover_frame_len + 4))?;
|
||||
}
|
||||
let offset = offset + 4;
|
||||
|
||||
let image1_len = LE::read_u32(&header[offset..offset + 4]) as usize;
|
||||
if image1_len > cover_frame_len {
|
||||
Err(Error::InvalidCoverImage2Size {
|
||||
frame_size: cover_frame_len,
|
||||
image1_size: image1_len,
|
||||
})?;
|
||||
}
|
||||
let image2_len = cover_frame_len - image1_len;
|
||||
let image1 = match image1_len {
|
||||
0 => None,
|
||||
len => Some(Vec::from(&header[offset..offset + len])),
|
||||
};
|
||||
let offset = offset + image1_len;
|
||||
|
||||
let image2 = match image2_len {
|
||||
0 => None,
|
||||
len => Some(Vec::from(&header[offset..offset + len])),
|
||||
};
|
||||
let offset = offset + image2_len;
|
||||
|
||||
Ok(Self {
|
||||
ncm_version,
|
||||
client_version,
|
||||
content_key: Vec::from(content_key),
|
||||
metadata: Vec::from(metadata),
|
||||
image1,
|
||||
image2,
|
||||
audio_rc4_key_stream: build_audio_rc4_key_stream(content_key)?,
|
||||
audio_data_offset: offset,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_ncm() -> Result<(), Error> {
|
||||
let ncm_header = include_bytes!("__fixture__/ncm_test1.bin");
|
||||
let ncm = NCMFile::new(ncm_header)?;
|
||||
let sbox = [
|
||||
0x08, 0x67, 0x20, 0xF0, 0x5C, 0xAC, 0xAF, 0x1B, 0x74, 0x0D, 0x26, 0x40, 0xBE, 0x85, 0x61,
|
||||
0x45, 0x0D, 0xA8, 0x69, 0xAE, 0x78, 0xEC, 0xB8, 0x14, 0x79, 0x53, 0xC4, 0x74, 0xF2, 0x9F,
|
||||
0xE1, 0x18, 0xE2, 0xF9, 0xC0, 0x7D, 0x1E, 0x5A, 0xBA, 0x4A, 0xB3, 0x11, 0x36, 0xD9, 0xAA,
|
||||
0xD2, 0x13, 0xEE, 0x92, 0x45, 0x4B, 0x57, 0xE6, 0xF1, 0x13, 0x90, 0xE9, 0xE5, 0x2F, 0xBE,
|
||||
0x6E, 0x16, 0x01, 0xBF, 0x10, 0x81, 0x5C, 0x68, 0x1C, 0xE3, 0xEB, 0xDF, 0x5A, 0xC7, 0xC3,
|
||||
0x3F, 0x9C, 0xD7, 0x28, 0x3A, 0x81, 0x2B, 0x1F, 0xF3, 0x6F, 0x27, 0x15, 0x7E, 0x53, 0xD3,
|
||||
0xF0, 0xFA, 0x50, 0x9F, 0xC7, 0xF0, 0x7F, 0xA6, 0xA7, 0x24, 0x80, 0x87, 0x39, 0x55, 0xA5,
|
||||
0x0B, 0x2B, 0x8B, 0x00, 0x19, 0x82, 0xA8, 0x40, 0xA5, 0x77, 0x9C, 0x93, 0x3B, 0xEE, 0x27,
|
||||
0x70, 0x0F, 0xA2, 0xEB, 0xA5, 0x6F, 0x58, 0xFB, 0xEB, 0x57, 0x1B, 0xC3, 0x61, 0x10, 0x6D,
|
||||
0x95, 0x2C, 0xAA, 0xC6, 0x58, 0x88, 0xD5, 0x9E, 0x33, 0x9A, 0x5A, 0xD1, 0xB5, 0xD2, 0x17,
|
||||
0x44, 0xD6, 0xDB, 0xAB, 0xED, 0x7C, 0xFC, 0x5C, 0x37, 0xB9, 0x16, 0xB8, 0xA5, 0x77, 0xFB,
|
||||
0x58, 0x35, 0x36, 0xE7, 0x51, 0xD0, 0x03, 0xBB, 0x2A, 0x88, 0x24, 0x84, 0x1D, 0x28, 0xB1,
|
||||
0xE1, 0xA0, 0xA5, 0xA0, 0xDD, 0x15, 0x66, 0xBB, 0x4F, 0x7E, 0xBC, 0x99, 0x76, 0x1A, 0xD2,
|
||||
0xEF, 0xBE, 0xD9, 0x79, 0xA0, 0x33, 0xD5, 0x96, 0x6C, 0xD9, 0xEF, 0x42, 0x7E, 0x11, 0x45,
|
||||
0x1F, 0x99, 0x96, 0x1A, 0x90, 0x0C, 0x75, 0xBC, 0x01, 0xE2, 0xC8, 0xEF, 0x16, 0x98, 0x9A,
|
||||
0x09, 0xEB, 0xD8, 0xA1, 0xEA, 0x62, 0xE7, 0x92, 0x20, 0x8F, 0x6F, 0x72, 0xDE, 0x14, 0xFE,
|
||||
0xBF, 0xCD, 0xBA, 0x97, 0xDE, 0xA3, 0x3B, 0x67, 0xD4, 0x0B, 0xF4, 0xD3, 0x97, 0x25, 0xBA,
|
||||
0xCA,
|
||||
];
|
||||
assert_eq!(ncm.audio_rc4_key_stream, sbox);
|
||||
|
||||
Ok(())
|
||||
}
|
37
um_crypto/ncm/src/lib.rs
Normal file
37
um_crypto/ncm/src/lib.rs
Normal file
@ -0,0 +1,37 @@
|
||||
pub mod content_key;
|
||||
pub mod header;
|
||||
pub mod metadata;
|
||||
|
||||
use cipher::block_padding::UnpadError;
|
||||
use thiserror::Error;
|
||||
use umc_utils::base64;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum NetEaseCryptoError {
|
||||
#[error("Header need at least {0} more bytes")]
|
||||
HeaderTooSmall(usize),
|
||||
|
||||
#[error("Not a NCM file")]
|
||||
NotNCMFile,
|
||||
#[error("Invalid NCM checksum. Expected {expected:08x}, actual: {expected:08x}")]
|
||||
ChecksumInvalid { expected: u32, actual: u32 },
|
||||
#[error("Unsupported cover image version: {0}")]
|
||||
UnsupportedCoverImageVersion(u8),
|
||||
#[error("Cover image: Frame size is less than image 1. frame_size:{frame_size}, image1_size:{image1_size}")]
|
||||
InvalidCoverImage2Size {
|
||||
frame_size: usize,
|
||||
image1_size: usize,
|
||||
},
|
||||
|
||||
#[error("ContentKey: AES PKCS#7 Decode Error")]
|
||||
ContentKeyDecryptError(UnpadError),
|
||||
#[error("ContentKey: Invalid key prefix: {0}")]
|
||||
ContentKeyWrongPrefix(String),
|
||||
|
||||
#[error("Metadata: Invalid prefix while decoding: {0}")]
|
||||
MetadataWrongPrefix(String),
|
||||
#[error("Metadata: AES PKCS#7 Decode Error")]
|
||||
MetadataDecryptError(UnpadError),
|
||||
#[error("Metadata: Decode metadata failed: {0}")]
|
||||
MetadataDecodeError(base64::DecodeError),
|
||||
}
|
105
um_crypto/ncm/src/metadata.rs
Normal file
105
um_crypto/ncm/src/metadata.rs
Normal file
@ -0,0 +1,105 @@
|
||||
use crate::NetEaseCryptoError;
|
||||
use aes::Aes128Dec;
|
||||
use cipher::block_padding::Pkcs7;
|
||||
use cipher::generic_array::GenericArray;
|
||||
use cipher::{BlockDecrypt, KeyInit};
|
||||
use itertools::Itertools;
|
||||
use umc_utils::base64;
|
||||
|
||||
const METADATA_KEY: [u8; 0x10] = *b"#14ljk_!\\]&0U<'(";
|
||||
|
||||
/// Decrypt metadata
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key`: Encrypted metadata
|
||||
///
|
||||
/// returns: Result<Vec<u8, Global>, NetEaseCryptoError>
|
||||
pub fn decrypt<T>(enciphered_metadata: T) -> Result<Vec<u8>, NetEaseCryptoError>
|
||||
where
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
let data = enciphered_metadata
|
||||
.as_ref()
|
||||
.iter()
|
||||
.map(|&b| b ^ 0x63)
|
||||
.collect_vec();
|
||||
let data = match data.strip_prefix(b"163 key(Don't modify):") {
|
||||
None => Err(NetEaseCryptoError::MetadataWrongPrefix(
|
||||
String::from_utf8_lossy(&data[..]).to_string(),
|
||||
))?,
|
||||
Some(data) => data,
|
||||
};
|
||||
let mut data = base64::decode(data).map_err(NetEaseCryptoError::MetadataDecodeError)?;
|
||||
let aes = Aes128Dec::new(&GenericArray::from(METADATA_KEY));
|
||||
let metadata = aes
|
||||
.decrypt_padded::<Pkcs7>(&mut data[..])
|
||||
.map_err(NetEaseCryptoError::MetadataDecryptError)?;
|
||||
Ok(Vec::from(metadata))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decrypt_metadata() -> Result<(), NetEaseCryptoError> {
|
||||
let enciphered_metadata = [
|
||||
0x52, 0x55, 0x50, 0x43, 0x08, 0x06, 0x1A, 0x4B, 0x27, 0x0C, 0x0D, 0x44, 0x17, 0x43, 0x0E,
|
||||
0x0C, 0x07, 0x0A, 0x05, 0x1A, 0x4A, 0x59, 0x2F, 0x55, 0x57, 0x25, 0x36, 0x50, 0x34, 0x57,
|
||||
0x3A, 0x1B, 0x3B, 0x50, 0x39, 0x25, 0x37, 0x0E, 0x01, 0x39, 0x48, 0x5B, 0x4C, 0x05, 0x09,
|
||||
0x07, 0x2B, 0x55, 0x22, 0x00, 0x48, 0x31, 0x32, 0x4C, 0x17, 0x07, 0x0D, 0x24, 0x1A, 0x39,
|
||||
0x11, 0x26, 0x5A, 0x15, 0x29, 0x24, 0x50, 0x02, 0x07, 0x20, 0x56, 0x2D, 0x33, 0x5A, 0x0C,
|
||||
0x10, 0x29, 0x26, 0x22, 0x28, 0x4C, 0x1A, 0x0D, 0x0B, 0x06, 0x28, 0x0D, 0x2C, 0x1B, 0x09,
|
||||
0x2E, 0x0B, 0x2B, 0x28, 0x02, 0x13, 0x0F, 0x2F, 0x2C, 0x0E, 0x25, 0x1B, 0x4C, 0x2B, 0x52,
|
||||
0x33, 0x36, 0x09, 0x37, 0x2D, 0x0B, 0x25, 0x27, 0x2C, 0x0C, 0x0D, 0x02, 0x51, 0x35, 0x5A,
|
||||
0x1A, 0x35, 0x2D, 0x2F, 0x3B, 0x54, 0x2A, 0x0D, 0x4C, 0x2B, 0x16, 0x1B, 0x0C, 0x26, 0x21,
|
||||
0x0B, 0x52, 0x25, 0x52, 0x36, 0x5B, 0x51, 0x50, 0x56, 0x01, 0x0A, 0x19, 0x14, 0x5B, 0x57,
|
||||
0x32, 0x07, 0x06, 0x50, 0x25, 0x55, 0x54, 0x53, 0x30, 0x2C, 0x32, 0x52, 0x15, 0x00, 0x50,
|
||||
0x37, 0x39, 0x28, 0x0E, 0x15, 0x0F, 0x36, 0x48, 0x11, 0x24, 0x33, 0x56, 0x07, 0x22, 0x20,
|
||||
0x29, 0x14, 0x28, 0x06, 0x16, 0x01, 0x54, 0x48, 0x0B, 0x17, 0x20, 0x25, 0x36, 0x02, 0x01,
|
||||
0x16, 0x07, 0x27, 0x16, 0x0C, 0x37, 0x20, 0x2A, 0x24, 0x21, 0x22, 0x50, 0x56, 0x0E, 0x34,
|
||||
0x52, 0x37, 0x14, 0x29, 0x10, 0x32, 0x20, 0x36, 0x2D, 0x52, 0x21, 0x02, 0x09, 0x3B, 0x0B,
|
||||
0x01, 0x17, 0x00, 0x33, 0x36, 0x0D, 0x01, 0x21, 0x2B, 0x17, 0x4C, 0x39, 0x02, 0x33, 0x51,
|
||||
0x02, 0x20, 0x07, 0x37, 0x4C, 0x5A, 0x25, 0x39, 0x09, 0x51, 0x25, 0x50, 0x54, 0x4C, 0x19,
|
||||
0x32, 0x37, 0x08, 0x06, 0x32, 0x55, 0x1B, 0x53, 0x0B, 0x0B, 0x4C, 0x08, 0x16, 0x33, 0x54,
|
||||
0x27, 0x11, 0x02, 0x1A, 0x30, 0x28, 0x57, 0x25, 0x50, 0x3B, 0x2E, 0x28, 0x34, 0x51, 0x3B,
|
||||
0x36, 0x11, 0x53, 0x12, 0x56, 0x09, 0x21, 0x2B, 0x2C, 0x14, 0x2D, 0x24, 0x5A, 0x02, 0x53,
|
||||
0x16, 0x21, 0x29, 0x16, 0x02, 0x53, 0x02, 0x2A, 0x31, 0x2D, 0x31, 0x56, 0x3B, 0x17, 0x51,
|
||||
0x52, 0x0F, 0x56, 0x50, 0x56, 0x00, 0x17, 0x57, 0x2F, 0x08, 0x1A, 0x52, 0x25, 0x12, 0x2D,
|
||||
0x48, 0x05, 0x02, 0x37, 0x0F, 0x36, 0x30, 0x05, 0x14, 0x5A, 0x37, 0x5A, 0x50, 0x55, 0x16,
|
||||
0x09, 0x53, 0x3B, 0x1B, 0x24, 0x1A, 0x1A, 0x21, 0x50, 0x06, 0x37, 0x2E, 0x57, 0x09, 0x39,
|
||||
0x17, 0x13, 0x26, 0x31, 0x04, 0x52, 0x50, 0x55, 0x10, 0x53, 0x52, 0x57, 0x02, 0x04, 0x02,
|
||||
0x2F, 0x01, 0x5A, 0x02, 0x2A, 0x2F, 0x19, 0x4C, 0x0C, 0x55, 0x00, 0x50, 0x2D, 0x29, 0x06,
|
||||
0x16, 0x11, 0x20, 0x0D, 0x01, 0x53, 0x22, 0x32, 0x1B, 0x34, 0x00, 0x0A, 0x06, 0x02, 0x15,
|
||||
0x0F, 0x25, 0x5A, 0x1A, 0x22, 0x11, 0x0E, 0x21, 0x32, 0x31, 0x22, 0x39, 0x56, 0x5A, 0x09,
|
||||
0x16, 0x35, 0x16, 0x30, 0x29, 0x0F, 0x30, 0x27, 0x2C, 0x55, 0x52, 0x12, 0x48, 0x16, 0x0F,
|
||||
0x0C, 0x36, 0x0C, 0x25, 0x27, 0x28, 0x35, 0x0A, 0x1A, 0x06, 0x09, 0x16, 0x39, 0x1B, 0x08,
|
||||
0x10, 0x27, 0x12, 0x2C, 0x2C, 0x5B, 0x48, 0x01, 0x48, 0x33, 0x48, 0x0E, 0x1B, 0x54, 0x1B,
|
||||
0x02, 0x36, 0x25, 0x20, 0x48, 0x31, 0x0B, 0x06, 0x0D, 0x15, 0x11, 0x17, 0x01, 0x24, 0x20,
|
||||
0x27, 0x31, 0x30, 0x22, 0x0F, 0x22, 0x08, 0x17, 0x0F, 0x13, 0x0C, 0x0C, 0x1A, 0x12, 0x12,
|
||||
0x51, 0x4C, 0x3B, 0x53, 0x3B, 0x37, 0x39, 0x16, 0x25, 0x05, 0x54, 0x10, 0x32, 0x0D, 0x48,
|
||||
0x0C, 0x50, 0x5B, 0x20, 0x0B, 0x19, 0x22, 0x19, 0x0D, 0x0B, 0x28, 0x2C, 0x0B, 0x05, 0x3B,
|
||||
0x51, 0x09, 0x54, 0x0A, 0x06, 0x0F, 0x57, 0x08, 0x51, 0x50, 0x24, 0x29, 0x16, 0x57, 0x33,
|
||||
0x36, 0x15, 0x04, 0x50, 0x5B, 0x2F, 0x51, 0x56, 0x12, 0x09, 0x2F, 0x27, 0x30, 0x51, 0x2B,
|
||||
0x37, 0x57, 0x24, 0x35, 0x5B, 0x0C, 0x0E, 0x0D, 0x0D, 0x53, 0x2A, 0x25, 0x06, 0x33, 0x22,
|
||||
0x29, 0x51, 0x24, 0x08, 0x51, 0x3A, 0x06, 0x0E, 0x16, 0x31, 0x36, 0x2B, 0x10, 0x34, 0x30,
|
||||
0x08, 0x0A, 0x2F, 0x19, 0x32, 0x5A, 0x04, 0x39, 0x52, 0x36, 0x1A, 0x26, 0x14, 0x0E, 0x0B,
|
||||
0x2A, 0x4C, 0x39, 0x56, 0x1A, 0x01, 0x2F, 0x14, 0x33, 0x33, 0x56, 0x16, 0x25, 0x29, 0x33,
|
||||
0x0E, 0x0B, 0x2D, 0x1A, 0x11, 0x29, 0x08, 0x15, 0x02, 0x2B, 0x4C, 0x01, 0x0C, 0x07, 0x0F,
|
||||
0x15, 0x4C, 0x36, 0x36, 0x09, 0x4C, 0x13, 0x53, 0x11, 0x11, 0x12, 0x02, 0x0F, 0x11, 0x56,
|
||||
0x2B, 0x5B, 0x21, 0x29, 0x0B, 0x51, 0x09, 0x02, 0x3B, 0x11, 0x02, 0x24, 0x16, 0x12, 0x04,
|
||||
0x0A, 0x37, 0x0B, 0x22, 0x5A, 0x0B, 0x02, 0x0A, 0x02, 0x36, 0x36, 0x2A, 0x29, 0x1A, 0x14,
|
||||
0x14, 0x13, 0x1B, 0x2C, 0x11, 0x31, 0x2A, 0x4C, 0x28, 0x11, 0x2B, 0x1B, 0x30, 0x51, 0x35,
|
||||
0x11, 0x53, 0x0D, 0x09, 0x0F, 0x39, 0x0F, 0x25, 0x17, 0x02, 0x5B, 0x1A, 0x39, 0x26, 0x5B,
|
||||
0x51, 0x3B, 0x2E, 0x04, 0x2E, 0x21, 0x36, 0x04, 0x3B, 0x11, 0x56, 0x0A, 0x2E, 0x2D, 0x1A,
|
||||
0x27, 0x21, 0x10, 0x07, 0x53, 0x11, 0x37, 0x07, 0x2F, 0x51, 0x57, 0x2B, 0x24, 0x57, 0x01,
|
||||
0x53, 0x35, 0x52, 0x0F, 0x2F, 0x2D, 0x48, 0x52, 0x56, 0x2B, 0x00, 0x17, 0x1A, 0x39, 0x00,
|
||||
0x12, 0x09, 0x02, 0x07, 0x02, 0x52, 0x2C, 0x20, 0x4C, 0x39, 0x4C, 0x48, 0x50, 0x0D, 0x0F,
|
||||
0x15, 0x4C, 0x00, 0x2B, 0x0F, 0x37, 0x48, 0x19, 0x17, 0x32, 0x35, 0x31, 0x28, 0x0B, 0x0D,
|
||||
0x2D, 0x32, 0x10, 0x32, 0x56, 0x33, 0x28, 0x3B, 0x2B, 0x55, 0x12, 0x04, 0x07, 0x50, 0x24,
|
||||
0x00, 0x08, 0x01, 0x10, 0x26, 0x30, 0x2C, 0x34, 0x10, 0x21, 0x0D, 0x25, 0x02, 0x2E, 0x53,
|
||||
0x11, 0x2B, 0x19, 0x36, 0x31, 0x2E, 0x5A, 0x52, 0x33, 0x35, 0x20, 0x14, 0x53, 0x30, 0x04,
|
||||
0x05, 0x08, 0x34, 0x13, 0x02, 0x54, 0x22, 0x05, 0x5A, 0x01, 0x20, 0x13, 0x14, 0x39, 0x2A,
|
||||
0x25, 0x1B, 0x37, 0x48, 0x57, 0x53, 0x2A, 0x22, 0x3A, 0x00, 0x34, 0x53, 0x24, 0x12,
|
||||
];
|
||||
let key = decrypt(enciphered_metadata)?;
|
||||
assert_eq!(&key[..6], b"music:");
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in New Issue
Block a user