diff --git a/Cargo.lock b/Cargo.lock index f39033c..5dc9306 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -374,8 +374,10 @@ name = "umc_kuwo" version = "0.1.0" dependencies = [ "anyhow", + "byteorder", "itertools", "thiserror", + "umc_qmc", "umc_utils", ] diff --git a/um_crypto/kuwo/Cargo.toml b/um_crypto/kuwo/Cargo.toml index 680d3a0..9334307 100644 --- a/um_crypto/kuwo/Cargo.toml +++ b/um_crypto/kuwo/Cargo.toml @@ -5,6 +5,8 @@ edition = "2021" [dependencies] anyhow = "1.0.86" +byteorder = "1.5.0" itertools = "0.13.0" thiserror = "1.0.63" +umc_qmc = { path = "../qmc" } umc_utils = { path = "../utils" } diff --git a/um_crypto/kuwo/Readme.MD b/um_crypto/kuwo/Readme.MD new file mode 100644 index 0000000..5872c7a --- /dev/null +++ b/um_crypto/kuwo/Readme.MD @@ -0,0 +1,39 @@ +# umc_kuwo + +酷我解密相关。 + +## 酷我 + +### PC 平台 + +不需要额外配置密钥。 + +### 安卓平台 + +需要利用 `root` 权限提取 mmkv 数据库。 + +## 波点音乐 + +波点音乐(酷我 Lite,安卓/iOS)。 + +- 安卓包名 `cn.wenyu.bodian` + +### 安卓 + +数据库路径 `/data/data/cn.wenyu.bodian/databases/list_downloaded.db` + +密钥存储在 `download` 表中的 `json` 列。部分数据节选: + +```json5 +{ + "audioPath": "/sdcard/Android/data/cn.wenyu.bodian/files/BodianMusic/music/歌名-咯咯咯.mflac", + "downInfo": { + // ekey: string | null + "ekey": "des_encrypt(device_id || ekey)" + } +} +``` + +其中,当 `downInfo.ekey` 为 `null` 时表示该 `ekey` 不参与解密。 + +`ekey` 可以使用 `umc_kuwo::des::decode_ekey(ekey, umc_kuwo::SECRET_KEY)` 解密。 diff --git a/um_crypto/kuwo/src/kwm_v1.rs b/um_crypto/kuwo/src/kwm_v1.rs new file mode 100644 index 0000000..cd51313 --- /dev/null +++ b/um_crypto/kuwo/src/kwm_v1.rs @@ -0,0 +1,32 @@ +#[derive(Debug, PartialEq, Clone)] +pub struct CipherV1 { + key: [u8; 0x20], +} + +const KEY: [u8; 0x20] = [ + 0x4D, 0x6F, 0x4F, 0x74, 0x4F, 0x69, 0x54, 0x76, 0x49, 0x4E, 0x47, 0x77, 0x64, 0x32, 0x45, 0x36, + 0x6E, 0x30, 0x45, 0x31, 0x69, 0x37, 0x4C, 0x35, 0x74, 0x32, 0x49, 0x6F, 0x4F, 0x6F, 0x4E, 0x6B, +]; + +impl CipherV1 { + pub fn new(resource_id: u32) -> Self { + let mut key = KEY; + for (k, r) in key.iter_mut().zip(resource_id.to_string().as_bytes()) { + *k ^= r; + } + + Self { key } + } + + pub fn decrypt(&self, data: &mut T, offset: usize) + where + T: AsMut<[u8]> + ?Sized, + { + let data = data.as_mut(); + let key_stream = self.key.iter().cycle().skip(offset % self.key.len()); + + for (datum, key) in data.iter_mut().zip(key_stream) { + *datum ^= *key; + } + } +} diff --git a/um_crypto/kuwo/src/lib.rs b/um_crypto/kuwo/src/lib.rs index 61f99a4..7c5817d 100644 --- a/um_crypto/kuwo/src/lib.rs +++ b/um_crypto/kuwo/src/lib.rs @@ -1,5 +1,15 @@ +use anyhow::Result; +use byteorder::{ReadBytesExt, LE}; +use std::io::{Cursor, Read}; + pub mod des; + +pub mod kwm_v1; +pub use umc_qmc::QMCv2Cipher as CipherV2; + +use crate::kwm_v1::CipherV1; use thiserror::Error; +use umc_qmc::QMCv2Cipher; /// Commonly used secret key for Kuwo services. pub const SECRET_KEY: [u8; 8] = *b"ylzsxkwm"; @@ -8,4 +18,109 @@ pub const SECRET_KEY: [u8; 8] = *b"ylzsxkwm"; pub enum KuwoCryptoError { #[error("Invalid DES data size (expected: {0} mod 8 == 0)")] InvalidDesDataSize(usize), + + #[error("Invalid KWM header magic bytes: {0:?}")] + InvalidHeaderMagic([u8; 16]), + + #[error("KWMv2: EKey required")] + V2EKeyRequired, + + #[error("KWM: Unsupported version {0}")] + UnsupportedVersion(usize), +} + +pub const DATA_START_OFFSET: usize = 0x400; + +pub enum Cipher { + V1(CipherV1), + V2(CipherV2), +} + +pub struct Header { + pub magic: [u8; 0x10], + + /// 1: LegacyKWM + /// 2: TME/QMCv2 + pub version: u32, + pub unknown_1: u32, + pub resource_id: u32, + pub unknown_2: [u8; 0x14], + pub format_name: [u8; 0x0C], +} + +impl Header { + const MAGIC_1: [u8; 16] = *b"yeelion-kuwo-tme"; + const MAGIC_2: [u8; 16] = *b"yeelion-kuwo\0\0\0\0"; + + pub fn from_bytes(bytes: T) -> Result + where + T: AsRef<[u8]>, + { + let mut cursor = Cursor::new(bytes); + let mut magic = [0u8; 0x10]; + cursor.read_exact(&mut magic)?; + let version = cursor.read_u32::()?; + let unknown_1 = cursor.read_u32::()?; + let resource_id = cursor.read_u32::()?; + let mut unknown_2 = [0u8; 0x14]; + cursor.read_exact(&mut unknown_2)?; + let mut format_name = [0u8; 0x0C]; + cursor.read_exact(&mut format_name)?; + + if magic != Self::MAGIC_1 || magic != Self::MAGIC_2 { + Err(KuwoCryptoError::InvalidHeaderMagic(magic))?; + } + + Ok(Self { + magic, + version, + unknown_1, + resource_id, + unknown_2, + format_name, + }) + } + + pub fn get_cipher(&self, ekey: Option) -> Result + where + T: AsRef<[u8]>, + { + let cipher = match self.version { + 1 => Cipher::V1(CipherV1::new(self.resource_id)), + 2 => match ekey { + Some(ekey) => Cipher::V2(CipherV2::new(ekey)?), + None => Err(KuwoCryptoError::V2EKeyRequired)?, + }, + version => Err(KuwoCryptoError::UnsupportedVersion(version as usize))?, + }; + + Ok(cipher) + } + + /// Get the quality id + /// Used for matching Android MMKV id. + pub fn get_quality_id(&self) -> u32 { + self.format_name + .iter() + .take_while(|&&c| c != 0 && c.is_ascii_digit()) + .fold(0, |sum, &value| sum * 10 + u32::from(value - b'0')) + } +} + +pub struct CipherBoDian(QMCv2Cipher); + +impl CipherBoDian { + pub fn new(ekey: &str) -> Result { + let ekey = des::decode_ekey(&ekey, &SECRET_KEY)?; + let cipher = CipherV2::new(ekey)?; + Ok(Self(cipher)) + } + + #[inline] + pub fn decrypt(&self, data: &mut T, offset: usize) + where + T: AsMut<[u8]> + ?Sized, + { + self.0.decrypt(data.as_mut(), offset) + } } diff --git a/um_crypto/qmc/src/lib.rs b/um_crypto/qmc/src/lib.rs index 090b952..e5e0883 100644 --- a/um_crypto/qmc/src/lib.rs +++ b/um_crypto/qmc/src/lib.rs @@ -15,6 +15,7 @@ pub enum QmcCryptoError { QMCV2MapKeyEmpty, } +#[derive(Debug, PartialEq, Clone)] pub enum QMCv2Cipher { MapL(QMC2Map), RC4(QMC2RC4), diff --git a/um_crypto/qmc/src/v2_map/mod.rs b/um_crypto/qmc/src/v2_map/mod.rs index c39ae2f..c9b9e8b 100644 --- a/um_crypto/qmc/src/v2_map/mod.rs +++ b/um_crypto/qmc/src/v2_map/mod.rs @@ -4,6 +4,7 @@ use crate::v1::cipher::{qmc1_transform, V1_KEY_SIZE}; use crate::v2_map::key::key_compress; use anyhow::Result; +#[derive(Debug, PartialEq, Clone)] pub struct QMC2Map { key: [u8; V1_KEY_SIZE], } diff --git a/um_crypto/qmc/src/v2_rc4/cipher.rs b/um_crypto/qmc/src/v2_rc4/cipher.rs index 22ac55b..e23e02a 100644 --- a/um_crypto/qmc/src/v2_rc4/cipher.rs +++ b/um_crypto/qmc/src/v2_rc4/cipher.rs @@ -7,7 +7,7 @@ const FIRST_SEGMENT_SIZE: usize = 0x0080; const OTHER_SEGMENT_SIZE: usize = 0x1400; const RC4_STREAM_CACHE_SIZE: usize = OTHER_SEGMENT_SIZE + 512; -#[derive(Debug, Clone)] +#[derive(Debug, PartialEq, Clone)] pub struct QMC2RC4 { hash: f64, key: Box<[u8]>,