impl: kuwo cipher and bodian info

This commit is contained in:
鲁树人 2024-09-06 23:03:28 +01:00
parent 7429c0d167
commit e7d8231474
8 changed files with 193 additions and 1 deletions

2
Cargo.lock generated
View File

@ -374,8 +374,10 @@ name = "umc_kuwo"
version = "0.1.0"
dependencies = [
"anyhow",
"byteorder",
"itertools",
"thiserror",
"umc_qmc",
"umc_utils",
]

View File

@ -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" }

39
um_crypto/kuwo/Readme.MD Normal file
View File

@ -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)` 解密。

View File

@ -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<T>(&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;
}
}
}

View File

@ -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<T>(bytes: T) -> Result<Self>
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::<LE>()?;
let unknown_1 = cursor.read_u32::<LE>()?;
let resource_id = cursor.read_u32::<LE>()?;
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<T>(&self, ekey: Option<T>) -> Result<Cipher>
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<Self> {
let ekey = des::decode_ekey(&ekey, &SECRET_KEY)?;
let cipher = CipherV2::new(ekey)?;
Ok(Self(cipher))
}
#[inline]
pub fn decrypt<T>(&self, data: &mut T, offset: usize)
where
T: AsMut<[u8]> + ?Sized,
{
self.0.decrypt(data.as_mut(), offset)
}
}

View File

@ -15,6 +15,7 @@ pub enum QmcCryptoError {
QMCV2MapKeyEmpty,
}
#[derive(Debug, PartialEq, Clone)]
pub enum QMCv2Cipher {
MapL(QMC2Map),
RC4(QMC2RC4),

View File

@ -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],
}

View File

@ -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]>,