impl: kuwo cipher and bodian info
This commit is contained in:
parent
7429c0d167
commit
e7d8231474
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -374,8 +374,10 @@ name = "umc_kuwo"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"byteorder",
|
||||
"itertools",
|
||||
"thiserror",
|
||||
"umc_qmc",
|
||||
"umc_utils",
|
||||
]
|
||||
|
||||
|
@ -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
39
um_crypto/kuwo/Readme.MD
Normal 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)` 解密。
|
32
um_crypto/kuwo/src/kwm_v1.rs
Normal file
32
um_crypto/kuwo/src/kwm_v1.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ pub enum QmcCryptoError {
|
||||
QMCV2MapKeyEmpty,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum QMCv2Cipher {
|
||||
MapL(QMC2Map),
|
||||
RC4(QMC2RC4),
|
||||
|
@ -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],
|
||||
}
|
||||
|
@ -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]>,
|
||||
|
Loading…
Reference in New Issue
Block a user