diff --git a/Cargo.lock b/Cargo.lock index 050feb4..fb30661 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -606,6 +606,7 @@ dependencies = [ "byteorder", "cbc", "thiserror", + "umc_qmc", "umc_utils", ] diff --git a/Dockerfile b/Dockerfile index 9606bc3..adc3e9b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.81-bookworm +FROM rust:1.85-bookworm RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh diff --git a/um_crypto/kgm/Cargo.toml b/um_crypto/kgm/Cargo.toml index 562d697..1f01b70 100644 --- a/um_crypto/kgm/Cargo.toml +++ b/um_crypto/kgm/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" byteorder = "1.5.0" thiserror = "2.0.7" umc_utils = { path = "../utils" } +umc_qmc = { path = "../qmc" } aes = "0.8.4" cbc = "0.1.2" block-padding = "0.3.3" diff --git a/um_crypto/kgm/src/__fixtures__/kgm_invalid_magic.bin b/um_crypto/kgm/src/__fixtures__/kgm_invalid_magic.bin new file mode 100644 index 0000000..462e15b Binary files /dev/null and b/um_crypto/kgm/src/__fixtures__/kgm_invalid_magic.bin differ diff --git a/um_crypto/kgm/src/__fixtures__/kgm_v3_hdr.bin b/um_crypto/kgm/src/__fixtures__/kgm_v3_hdr.bin new file mode 100644 index 0000000..7a040a4 Binary files /dev/null and b/um_crypto/kgm/src/__fixtures__/kgm_v3_hdr.bin differ diff --git a/um_crypto/kgm/src/__fixtures__/kgm_v5_hdr.bin b/um_crypto/kgm/src/__fixtures__/kgm_v5_hdr.bin new file mode 100644 index 0000000..e66b7fd Binary files /dev/null and b/um_crypto/kgm/src/__fixtures__/kgm_v5_hdr.bin differ diff --git a/um_crypto/kgm/src/header.rs b/um_crypto/kgm/src/header.rs index 4b4b102..0c77796 100644 --- a/um_crypto/kgm/src/header.rs +++ b/um_crypto/kgm/src/header.rs @@ -1,39 +1,69 @@ use crate::KugouError; -use byteorder::{ByteOrder, LE}; +use byteorder::{ReadBytesExt, LE}; +use std::io::{BufReader, Read}; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Header { pub magic: [u8; 0x10], pub offset_to_data: usize, pub crypto_version: u32, - pub key_slot: u32, + pub key_slot: i32, pub decrypt_test_data: [u8; 0x10], pub file_key: [u8; 0x10], challenge_data: [u8; 0x10], + pub audio_hash: String, } -impl Header { - pub fn from_buffer(buffer: T) -> Result - where - T: AsRef<[u8]>, - { - let buffer = buffer.as_ref(); - if buffer.len() < 0x3c { - Err(KugouError::HeaderTooSmall(0x3c))?; - } +pub trait HeaderReaderHelper: Read { + fn read_u32_le(&mut self) -> Result { + self.read_u32::() + .map_err(KugouError::HeaderParseIOError) + } + fn read_i32_le(&mut self) -> Result { + self.read_i32::() + .map_err(KugouError::HeaderParseIOError) + } + + fn read_buff>(&mut self, buffer: &mut T) -> Result<(), KugouError> { + self.read_exact(buffer.as_mut()) + .map_err(KugouError::HeaderParseIOError) + } +} +impl HeaderReaderHelper for R {} + +impl Header { + pub fn from_reader(reader: &mut T) -> Result + where + T: Read, + { let mut magic = [0u8; 0x10]; - magic.copy_from_slice(&buffer[..0x10]); + reader.read_buff(&mut magic)?; let challenge_data = get_challenge_data(&magic)?; - let offset_to_data = LE::read_u32(&buffer[0x10..0x14]) as usize; - let crypto_version = LE::read_u32(&buffer[0x14..0x18]); - let key_slot = LE::read_u32(&buffer[0x18..0x1C]); let mut decrypt_test_data = [0u8; 0x10]; - decrypt_test_data.copy_from_slice(&buffer[0x1c..0x2c]); let mut file_key = [0u8; 0x10]; - file_key.copy_from_slice(&buffer[0x2c..0x3c]); + let mut audio_hash = "".to_string(); + + let offset_to_data = reader.read_u32_le()? as usize; + let crypto_version = reader.read_u32_le()?; + let key_slot = reader.read_i32_le()?; + reader.read_buff(&mut decrypt_test_data)?; + reader.read_buff(&mut file_key)?; + + if crypto_version == 5 { + let mut unused_padding = [0u8; 0x08]; + reader.read_buff(&mut unused_padding)?; // seek 8 bytes + let audio_hash_size = reader.read_u32_le()? as usize; + if audio_hash_size != 0x20 { + Err(KugouError::HeaderInvalidAudioHash(audio_hash_size))?; + } + + let mut audio_hash_bytes = vec![0u8; audio_hash_size]; + reader.read_buff(&mut audio_hash_bytes)?; + audio_hash = String::from_utf8_lossy(&audio_hash_bytes).to_string(); + } Ok(Self { magic, @@ -43,9 +73,23 @@ impl Header { decrypt_test_data, file_key, challenge_data, + audio_hash, }) } + pub fn from_buffer(buffer: T) -> Result + where + T: AsRef<[u8]>, + { + let buffer = buffer.as_ref(); + if buffer.len() < 0x40 { + Err(KugouError::HeaderTooSmall(0x40))?; + } + + let mut reader = BufReader::new(buffer); + Self::from_reader(&mut reader) + } + pub fn get_challenge_data(&self) -> [u8; 0x10] { self.challenge_data } @@ -74,3 +118,50 @@ pub const VPR_HEADER: [u8; 16] = [ pub const VPR_TEST_DATA: [u8; 16] = [ 0x1D, 0x5A, 0x05, 0x34, 0x0C, 0x41, 0x8D, 0x42, 0x9C, 0x83, 0x92, 0x6C, 0xAE, 0x16, 0xFE, 0x56, ]; + +#[cfg(test)] +mod tests { + use crate::header::Header; + use crate::KugouError; + + #[test] + fn parse_header_error_too_small() { + assert!(matches!( + Header::from_buffer(b"invalid file"), + Err(KugouError::HeaderTooSmall(_)) + )); + } + + #[test] + fn parse_header_error_file_magic() { + assert!(matches!( + Header::from_buffer(include_bytes!("__fixtures__/kgm_invalid_magic.bin")), + Err(KugouError::NotKGMFile) + )); + } + + #[test] + fn parse_header_v2() -> Result<(), KugouError> { + let hdr = Header::from_buffer(include_bytes!("__fixtures__/kgm_v2_hdr.bin"))?; + assert_eq!(hdr.key_slot, 1); + assert_eq!(hdr.crypto_version, 2); + Ok(()) + } + + #[test] + fn parse_header_v3() -> Result<(), KugouError> { + let hdr = Header::from_buffer(include_bytes!("__fixtures__/kgm_v3_hdr.bin"))?; + assert_eq!(hdr.key_slot, 1); + assert_eq!(hdr.crypto_version, 3); + Ok(()) + } + + #[test] + fn parse_header_v5() -> Result<(), KugouError> { + let hdr = Header::from_buffer(include_bytes!("__fixtures__/kgm_v5_hdr.bin"))?; + assert_eq!(hdr.key_slot, -1); + assert_eq!(hdr.crypto_version, 5); + assert_eq!(hdr.audio_hash, "81a26217da847692e7688e0a5ebe9da1"); + Ok(()) + } +} diff --git a/um_crypto/kgm/src/lib.rs b/um_crypto/kgm/src/lib.rs index 5b2d367..a8f88cc 100644 --- a/um_crypto/kgm/src/lib.rs +++ b/um_crypto/kgm/src/lib.rs @@ -2,6 +2,7 @@ pub mod header; mod pc_db_decrypt; pub mod v2; pub mod v3; +mod v5; pub use pc_db_decrypt::decrypt_db; @@ -10,6 +11,7 @@ use crate::v2::DecipherV2; use crate::v3::DecipherV3; use thiserror::Error; +use crate::v5::DecipherV5; use block_padding::UnpadError; #[derive(Debug, Error)] @@ -18,11 +20,14 @@ pub enum KugouError { HeaderTooSmall(usize), #[error("Unsupported key slot: {0}")] - UnsupportedKeySlot(u32), + UnsupportedKeySlot(i32), #[error("Unsupported cipher version: {0}")] UnsupportedCipherVersion(u32), + #[error("V5 requires ekey.")] + V5EKeyRequired, + #[error("Not KGM File (magic mismatch)")] NotKGMFile, @@ -40,23 +45,42 @@ pub enum KugouError { #[error("Database does not seem valid")] InvalidPage1Header, + + #[error("QMC2EKeyError: {0}")] + QMC2EKeyError(String), + + #[error("Parse KGM header with i/o error: {0}")] + HeaderParseIOError(std::io::Error), + + #[error("Invalid audio hash size: {0}")] + HeaderInvalidAudioHash(usize), } pub enum Decipher { V2(DecipherV2), V3(DecipherV3), + V5(DecipherV5), } impl Decipher { pub fn new(header: &Header) -> Result { + Self::new_v5(header, None) + } + + pub fn new_v5(header: &Header, ekey: Option) -> Result { let slot_key: &[u8] = match header.key_slot { 1 => b"l,/'", + -1 => b"", // unused, kgm v5 (kgg) slot => Err(KugouError::UnsupportedKeySlot(slot))?, }; let decipher = match header.crypto_version { 2 => Decipher::V2(DecipherV2::new(header, slot_key)?), 3 => Decipher::V3(DecipherV3::new(header, slot_key)?), + 5 => match ekey { + Some(ekey) => Decipher::V5(DecipherV5::new(&ekey)?), + _ => Err(KugouError::V5EKeyRequired)?, + }, version => Err(KugouError::UnsupportedCipherVersion(version))?, }; @@ -73,6 +97,7 @@ impl Decipher { match self { Decipher::V2(decipher) => decipher.decrypt(buffer, offset), Decipher::V3(decipher) => decipher.decrypt(buffer, offset), + Decipher::V5(decipher) => decipher.decrypt(buffer, offset), } } } diff --git a/um_crypto/kgm/src/pc_db_decrypt/key_derive.rs b/um_crypto/kgm/src/pc_db_decrypt/key_derive.rs index 0730eb4..51f32dd 100644 --- a/um_crypto/kgm/src/pc_db_decrypt/key_derive.rs +++ b/um_crypto/kgm/src/pc_db_decrypt/key_derive.rs @@ -29,7 +29,7 @@ fn next_page_iv(seed: u32) -> u32 { fn derive_page_aes_key(seed: u32) -> [u8; 0x10] { let mut master_key = DEFAULT_MASTER_KEY; LE::write_u32(&mut master_key[0x10..0x14], seed); - md5(&mut master_key) + md5(master_key) } fn derive_page_aes_iv(seed: u32) -> [u8; 0x10] { diff --git a/um_crypto/kgm/src/v2.rs b/um_crypto/kgm/src/v2.rs index 6bce07b..84fc587 100644 --- a/um_crypto/kgm/src/v2.rs +++ b/um_crypto/kgm/src/v2.rs @@ -21,8 +21,8 @@ impl DecipherV2 { #[test] fn test_v2_init() -> Result<(), KugouError> { - let hdr_v2 = Header::from_buffer(include_bytes!("__fixtures__/kgm_v2_hdr.bin"))?; - DecipherV2::new(&hdr_v2, b"1234")?; + let hdr = Header::from_buffer(include_bytes!("__fixtures__/kgm_v2_hdr.bin"))?; + DecipherV2::new(&hdr, b"1234")?; Ok(()) } diff --git a/um_crypto/kgm/src/v5.rs b/um_crypto/kgm/src/v5.rs new file mode 100644 index 0000000..5c3c43f --- /dev/null +++ b/um_crypto/kgm/src/v5.rs @@ -0,0 +1,17 @@ +use crate::KugouError; +use umc_qmc::QMCv2Cipher; + +pub struct DecipherV5(QMCv2Cipher); + +impl DecipherV5 { + pub fn new(ekey: &str) -> Result { + let cipher = QMCv2Cipher::new_from_ekey(ekey) + .map_err(|e| KugouError::QMC2EKeyError(e.to_string()))?; + + Ok(Self(cipher)) + } + + pub fn decrypt + ?Sized>(&self, buffer: &mut T, offset: usize) { + self.0.decrypt(buffer, offset) + } +} diff --git a/um_crypto/utils/src/md5.rs b/um_crypto/utils/src/md5.rs index d71d58d..838107a 100644 --- a/um_crypto/utils/src/md5.rs +++ b/um_crypto/utils/src/md5.rs @@ -1,7 +1,7 @@ use md5::{Digest, Md5}; /// Calculate the MD5 hash (non-modified) of a buffer. -pub fn md5>(buffer: T) -> [u8; 16] { +pub fn md5(buffer: impl AsRef<[u8]>) -> [u8; 0x10] { Md5::digest(buffer).into() } diff --git a/um_wasm/src/exports/kgm.rs b/um_wasm/src/exports/kgm.rs index 975e5ed..476b5df 100644 --- a/um_wasm/src/exports/kgm.rs +++ b/um_wasm/src/exports/kgm.rs @@ -2,18 +2,55 @@ use umc_kgm::{header::Header, Decipher}; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::JsError; +/// KuGou KGM file header. +#[wasm_bindgen(js_name=KuGouHeader)] +pub struct JsKuGouHdr(Header); + +#[wasm_bindgen(js_class=KuGouHeader)] +impl JsKuGouHdr { + /// Parse the KuGou header (0x400 bytes recommended). + #[wasm_bindgen(constructor)] + pub fn new(header: &[u8]) -> Result { + let header = Header::from_buffer(header).map_err(JsError::from)?; + Ok(Self(header)) + } + + /// Get the audio hash (kgm v5). + #[wasm_bindgen(getter, js_name = "audioHash")] + pub fn get_audio_hash(&self) -> String { + self.0.audio_hash.clone() + } + + /// Get version + #[wasm_bindgen(getter, js_name = "version")] + pub fn get_crypto_version(&self) -> u32 { + self.0.crypto_version + } + + /// Get offset to encrypted data + #[wasm_bindgen(getter, js_name = "offsetToData")] + pub fn get_offset_to_data(&self) -> u32 { + self.0.offset_to_data as u32 + } +} + /// KuGou KGM file decipher. #[wasm_bindgen(js_name=KuGou)] pub struct JsKuGou(Decipher); -#[wasm_bindgen(js_class = KuGou)] +#[wasm_bindgen(js_class=KuGou)] impl JsKuGou { - /// Parse the KuGou header (0x400 bytes) - pub fn from_header(header: &[u8]) -> Result { - let header = Header::from_buffer(header).map_err(JsError::from)?; - let decipher = Decipher::new(&header).map_err(JsError::from)?; + /// Parse the KuGou header (0x400 bytes recommended). + pub fn from_header(header: &[u8]) -> Result { + let header = JsKuGouHdr::new(header)?; + Self::from_header_v5(&header, None) + } - Ok(JsKuGou(decipher)) + /// Parse the KuGou header (0x400 bytes recommended). + #[wasm_bindgen(js_name=fromHeaderV5)] + pub fn from_header_v5(header: &JsKuGouHdr, ekey: Option) -> Result { + let decipher = Decipher::new_v5(&header.0, ekey).map_err(JsError::from)?; + Ok(Self(decipher)) } /// Decrypt a buffer. diff --git a/um_wasm_loader/build.js b/um_wasm_loader/build.js old mode 100644 new mode 100755