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"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"byteorder",
|
||||||
"itertools",
|
"itertools",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"umc_qmc",
|
||||||
"umc_utils",
|
"umc_utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.86"
|
anyhow = "1.0.86"
|
||||||
|
byteorder = "1.5.0"
|
||||||
itertools = "0.13.0"
|
itertools = "0.13.0"
|
||||||
thiserror = "1.0.63"
|
thiserror = "1.0.63"
|
||||||
|
umc_qmc = { path = "../qmc" }
|
||||||
umc_utils = { path = "../utils" }
|
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 des;
|
||||||
|
|
||||||
|
pub mod kwm_v1;
|
||||||
|
pub use umc_qmc::QMCv2Cipher as CipherV2;
|
||||||
|
|
||||||
|
use crate::kwm_v1::CipherV1;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use umc_qmc::QMCv2Cipher;
|
||||||
|
|
||||||
/// Commonly used secret key for Kuwo services.
|
/// Commonly used secret key for Kuwo services.
|
||||||
pub const SECRET_KEY: [u8; 8] = *b"ylzsxkwm";
|
pub const SECRET_KEY: [u8; 8] = *b"ylzsxkwm";
|
||||||
@ -8,4 +18,109 @@ pub const SECRET_KEY: [u8; 8] = *b"ylzsxkwm";
|
|||||||
pub enum KuwoCryptoError {
|
pub enum KuwoCryptoError {
|
||||||
#[error("Invalid DES data size (expected: {0} mod 8 == 0)")]
|
#[error("Invalid DES data size (expected: {0} mod 8 == 0)")]
|
||||||
InvalidDesDataSize(usize),
|
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,
|
QMCV2MapKeyEmpty,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub enum QMCv2Cipher {
|
pub enum QMCv2Cipher {
|
||||||
MapL(QMC2Map),
|
MapL(QMC2Map),
|
||||||
RC4(QMC2RC4),
|
RC4(QMC2RC4),
|
||||||
|
@ -4,6 +4,7 @@ use crate::v1::cipher::{qmc1_transform, V1_KEY_SIZE};
|
|||||||
use crate::v2_map::key::key_compress;
|
use crate::v2_map::key::key_compress;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub struct QMC2Map {
|
pub struct QMC2Map {
|
||||||
key: [u8; V1_KEY_SIZE],
|
key: [u8; V1_KEY_SIZE],
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ const FIRST_SEGMENT_SIZE: usize = 0x0080;
|
|||||||
const OTHER_SEGMENT_SIZE: usize = 0x1400;
|
const OTHER_SEGMENT_SIZE: usize = 0x1400;
|
||||||
const RC4_STREAM_CACHE_SIZE: usize = OTHER_SEGMENT_SIZE + 512;
|
const RC4_STREAM_CACHE_SIZE: usize = OTHER_SEGMENT_SIZE + 512;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub struct QMC2RC4 {
|
pub struct QMC2RC4 {
|
||||||
hash: f64,
|
hash: f64,
|
||||||
key: Box<[u8]>,
|
key: Box<[u8]>,
|
||||||
|
Loading…
Reference in New Issue
Block a user