mirror of
https://git.um-react.app/um/lib_um_crypto_rust.git
synced 2026-03-08 04:29:54 +00:00
138 lines
3.4 KiB
Rust
138 lines
3.4 KiB
Rust
use crate::kwm_v1::CipherV1;
|
|
use anyhow::Result;
|
|
use byteorder::{ReadBytesExt, LE};
|
|
use std::io::{Cursor, Read};
|
|
use thiserror::Error;
|
|
use umc_qmc::QMCv2Cipher;
|
|
|
|
pub mod des;
|
|
|
|
pub mod kwm_v1;
|
|
pub use umc_qmc::QMCv2Cipher as CipherV2;
|
|
|
|
/// Commonly used secret key for Kuwo services.
|
|
pub const SECRET_KEY: [u8; 8] = *b"ylzsxkwm";
|
|
|
|
#[derive(Error, Debug)]
|
|
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),
|
|
}
|
|
|
|
impl Cipher {
|
|
pub fn decrypt<T>(&self, data: &mut T, offset: usize)
|
|
where
|
|
T: AsMut<[u8]> + ?Sized,
|
|
{
|
|
match self {
|
|
Cipher::V1(cipher) => cipher.decrypt(data, offset),
|
|
Cipher::V2(cipher) => cipher.decrypt(data, offset),
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|