diff --git a/.idea/lib_um_crypto.iml b/.idea/lib_um_crypto.iml
index 2a9d528..3a3b439 100644
--- a/.idea/lib_um_crypto.iml
+++ b/.idea/lib_um_crypto.iml
@@ -8,6 +8,7 @@
+
diff --git a/Cargo.lock b/Cargo.lock
index 5cae4bd..bf5072a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/um_crypto/ncm/Cargo.toml b/um_crypto/ncm/Cargo.toml
new file mode 100644
index 0000000..465853f
--- /dev/null
+++ b/um_crypto/ncm/Cargo.toml
@@ -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"
diff --git a/um_crypto/ncm/Readme.MD b/um_crypto/ncm/Readme.MD
new file mode 100644
index 0000000..43a7e7a
--- /dev/null
+++ b/um_crypto/ncm/Readme.MD
@@ -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
diff --git a/um_crypto/ncm/src/__fixture__/ncm_test1.bin b/um_crypto/ncm/src/__fixture__/ncm_test1.bin
new file mode 100644
index 0000000..554482b
Binary files /dev/null and b/um_crypto/ncm/src/__fixture__/ncm_test1.bin differ
diff --git a/um_crypto/ncm/src/content_key.rs b/um_crypto/ncm/src/content_key.rs
new file mode 100644
index 0000000..0544747
--- /dev/null
+++ b/um_crypto/ncm/src/content_key.rs
@@ -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, NetEaseCryptoError>
+///
+pub fn decrypt(key: T) -> Result, 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::(&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");
+}
diff --git a/um_crypto/ncm/src/header.rs b/um_crypto/ncm/src/header.rs
new file mode 100644
index 0000000..d3d3c1b
--- /dev/null
+++ b/um_crypto/ncm/src/header.rs
@@ -0,0 +1,160 @@
+use crate::{content_key, NetEaseCryptoError as Error};
+use byteorder::{ByteOrder, LE};
+
+const CRC32: crc::Crc = crc::Crc::::new(&crc::CRC_32_ISO_HDLC);
+
+struct NCMFile {
+ pub ncm_version: u8,
+ pub client_version: u8,
+ /// Encrypted content key
+ pub content_key: Vec,
+ /// Encrypted metadata.
+ pub metadata: Vec,
+ /// Cover image 1
+ pub image1: Option>,
+ /// Cover image 2, format unknown
+ pub image2: Option>,
+ 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(header: T) -> Result
+ 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(())
+}
diff --git a/um_crypto/ncm/src/lib.rs b/um_crypto/ncm/src/lib.rs
new file mode 100644
index 0000000..c0ff899
--- /dev/null
+++ b/um_crypto/ncm/src/lib.rs
@@ -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),
+}
diff --git a/um_crypto/ncm/src/metadata.rs b/um_crypto/ncm/src/metadata.rs
new file mode 100644
index 0000000..e3aad2f
--- /dev/null
+++ b/um_crypto/ncm/src/metadata.rs
@@ -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, NetEaseCryptoError>
+pub fn decrypt(enciphered_metadata: T) -> Result, 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::(&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(())
+}