From 16ca02a28c873c0693a16393cc1d38c7e64e663b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Sat, 14 Sep 2024 01:33:32 +0100 Subject: [PATCH] feat: basic ncm support --- .idea/lib_um_crypto.iml | 1 + Cargo.lock | 139 +++++++++++++++++ um_crypto/ncm/Cargo.toml | 17 +++ um_crypto/ncm/Readme.MD | 23 +++ um_crypto/ncm/src/__fixture__/ncm_test1.bin | Bin 0 -> 1056 bytes um_crypto/ncm/src/content_key.rs | 56 +++++++ um_crypto/ncm/src/header.rs | 160 ++++++++++++++++++++ um_crypto/ncm/src/lib.rs | 37 +++++ um_crypto/ncm/src/metadata.rs | 105 +++++++++++++ 9 files changed, 538 insertions(+) create mode 100644 um_crypto/ncm/Cargo.toml create mode 100644 um_crypto/ncm/Readme.MD create mode 100644 um_crypto/ncm/src/__fixture__/ncm_test1.bin create mode 100644 um_crypto/ncm/src/content_key.rs create mode 100644 um_crypto/ncm/src/header.rs create mode 100644 um_crypto/ncm/src/lib.rs create mode 100644 um_crypto/ncm/src/metadata.rs 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 0000000000000000000000000000000000000000..554482b116913df4f1b09f84053e0df13789c654 GIT binary patch literal 1056 zcmWO5&CA<#0KoC&XPY!l(llwB*knqYC2i6qZJM-+dBTvfbx+hx^U@@kf+EkDKCq#J zpmH>JxhA?{g`_7^2K+~cc(i??tF8=e(=Mm zx6At=C@Yc^Ekt;1Opqt}6CDC=H-$%^oE4SoC5viH43j|hJ8%$f;wFX~g$zY=C+1O7 z5dwNt_jM%!s8m{RO?35)bY-tPdrApmq9`7txAaqOzp38p6Tr2!}3s61YwvU{#ksEcy zwFo>_ism>%_Gungr19LADY?Z{J%SJzCXK^9^aD<9uDcEy5y?iv2l9Z#H4|@!DYkAS zRMkooMdvEl>o=0kEG-#qHnvm4uaxD6=#XSSQ4v?xQ>B$|Asue+8Qq25YR?8v-3T&I z7_l9E5(lLK&sf(jIaPN%xFyhfs}GDACQzy$dma||a++oUMVH2AsN*RHLK-L>(`fs+ zT;re(Ja3{4EZY-JdOnFY+7Rt@N#`U+1(eEzjS!4BX*g|6hh8J3%l6a~#=|a}(>^*& zD?_Q^e3?-*E6Pd(;)MWWFo1&0l;R+c67rA%8I&IlcxO4+aME(RPU&i)Q7{cOQJ@yw z9ogohJ0w*w&1qxc&8KzdE+sa%8_m|D2i9`4hNT`GOjOa2s(d1EL5o~rPF3f&Vd8Lk i^swFY&mNsWa{Bo8>Zmh4bM5K9kHnXVg1!4yvicuQNZ$Ma literal 0 HcmV?d00001 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(()) +}