Files
lib_um_crypto_rust/um_crypto/kuwo/src/lib.rs
2024-09-14 01:33:23 +01:00

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)
}
}