feat: add kgm v5 (kgg) support.

This commit is contained in:
鲁树人 2025-02-24 20:41:17 +09:00
parent 02f0bb9a93
commit 54deabe74f
14 changed files with 202 additions and 30 deletions

1
Cargo.lock generated
View File

@ -606,6 +606,7 @@ dependencies = [
"byteorder", "byteorder",
"cbc", "cbc",
"thiserror", "thiserror",
"umc_qmc",
"umc_utils", "umc_utils",
] ]

View File

@ -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 RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

View File

@ -7,6 +7,7 @@ edition = "2021"
byteorder = "1.5.0" byteorder = "1.5.0"
thiserror = "2.0.7" thiserror = "2.0.7"
umc_utils = { path = "../utils" } umc_utils = { path = "../utils" }
umc_qmc = { path = "../qmc" }
aes = "0.8.4" aes = "0.8.4"
cbc = "0.1.2" cbc = "0.1.2"
block-padding = "0.3.3" block-padding = "0.3.3"

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,39 +1,69 @@
use crate::KugouError; 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 struct Header {
pub magic: [u8; 0x10], pub magic: [u8; 0x10],
pub offset_to_data: usize, pub offset_to_data: usize,
pub crypto_version: u32, pub crypto_version: u32,
pub key_slot: u32, pub key_slot: i32,
pub decrypt_test_data: [u8; 0x10], pub decrypt_test_data: [u8; 0x10],
pub file_key: [u8; 0x10], pub file_key: [u8; 0x10],
challenge_data: [u8; 0x10], challenge_data: [u8; 0x10],
pub audio_hash: String,
} }
impl Header { pub trait HeaderReaderHelper: Read {
pub fn from_buffer<T>(buffer: T) -> Result<Self, KugouError> fn read_u32_le(&mut self) -> Result<u32, KugouError> {
where self.read_u32::<LE>()
T: AsRef<[u8]>, .map_err(KugouError::HeaderParseIOError)
{ }
let buffer = buffer.as_ref();
if buffer.len() < 0x3c {
Err(KugouError::HeaderTooSmall(0x3c))?;
}
fn read_i32_le(&mut self) -> Result<i32, KugouError> {
self.read_i32::<LE>()
.map_err(KugouError::HeaderParseIOError)
}
fn read_buff<T: AsMut<[u8]>>(&mut self, buffer: &mut T) -> Result<(), KugouError> {
self.read_exact(buffer.as_mut())
.map_err(KugouError::HeaderParseIOError)
}
}
impl<R: Read + ?Sized> HeaderReaderHelper for R {}
impl Header {
pub fn from_reader<T>(reader: &mut T) -> Result<Self, KugouError>
where
T: Read,
{
let mut magic = [0u8; 0x10]; let mut magic = [0u8; 0x10];
magic.copy_from_slice(&buffer[..0x10]); reader.read_buff(&mut magic)?;
let challenge_data = get_challenge_data(&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]; let mut decrypt_test_data = [0u8; 0x10];
decrypt_test_data.copy_from_slice(&buffer[0x1c..0x2c]);
let mut file_key = [0u8; 0x10]; 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 { Ok(Self {
magic, magic,
@ -43,9 +73,23 @@ impl Header {
decrypt_test_data, decrypt_test_data,
file_key, file_key,
challenge_data, challenge_data,
audio_hash,
}) })
} }
pub fn from_buffer<T>(buffer: T) -> Result<Self, KugouError>
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] { pub fn get_challenge_data(&self) -> [u8; 0x10] {
self.challenge_data self.challenge_data
} }
@ -74,3 +118,50 @@ pub const VPR_HEADER: [u8; 16] = [
pub const VPR_TEST_DATA: [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, 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(())
}
}

View File

@ -2,6 +2,7 @@ pub mod header;
mod pc_db_decrypt; mod pc_db_decrypt;
pub mod v2; pub mod v2;
pub mod v3; pub mod v3;
mod v5;
pub use pc_db_decrypt::decrypt_db; pub use pc_db_decrypt::decrypt_db;
@ -10,6 +11,7 @@ use crate::v2::DecipherV2;
use crate::v3::DecipherV3; use crate::v3::DecipherV3;
use thiserror::Error; use thiserror::Error;
use crate::v5::DecipherV5;
use block_padding::UnpadError; use block_padding::UnpadError;
#[derive(Debug, Error)] #[derive(Debug, Error)]
@ -18,11 +20,14 @@ pub enum KugouError {
HeaderTooSmall(usize), HeaderTooSmall(usize),
#[error("Unsupported key slot: {0}")] #[error("Unsupported key slot: {0}")]
UnsupportedKeySlot(u32), UnsupportedKeySlot(i32),
#[error("Unsupported cipher version: {0}")] #[error("Unsupported cipher version: {0}")]
UnsupportedCipherVersion(u32), UnsupportedCipherVersion(u32),
#[error("V5 requires ekey.")]
V5EKeyRequired,
#[error("Not KGM File (magic mismatch)")] #[error("Not KGM File (magic mismatch)")]
NotKGMFile, NotKGMFile,
@ -40,23 +45,42 @@ pub enum KugouError {
#[error("Database does not seem valid")] #[error("Database does not seem valid")]
InvalidPage1Header, 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 { pub enum Decipher {
V2(DecipherV2), V2(DecipherV2),
V3(DecipherV3), V3(DecipherV3),
V5(DecipherV5),
} }
impl Decipher { impl Decipher {
pub fn new(header: &Header) -> Result<Self, KugouError> { pub fn new(header: &Header) -> Result<Self, KugouError> {
Self::new_v5(header, None)
}
pub fn new_v5(header: &Header, ekey: Option<String>) -> Result<Self, KugouError> {
let slot_key: &[u8] = match header.key_slot { let slot_key: &[u8] = match header.key_slot {
1 => b"l,/'", 1 => b"l,/'",
-1 => b"", // unused, kgm v5 (kgg)
slot => Err(KugouError::UnsupportedKeySlot(slot))?, slot => Err(KugouError::UnsupportedKeySlot(slot))?,
}; };
let decipher = match header.crypto_version { let decipher = match header.crypto_version {
2 => Decipher::V2(DecipherV2::new(header, slot_key)?), 2 => Decipher::V2(DecipherV2::new(header, slot_key)?),
3 => Decipher::V3(DecipherV3::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))?, version => Err(KugouError::UnsupportedCipherVersion(version))?,
}; };
@ -73,6 +97,7 @@ impl Decipher {
match self { match self {
Decipher::V2(decipher) => decipher.decrypt(buffer, offset), Decipher::V2(decipher) => decipher.decrypt(buffer, offset),
Decipher::V3(decipher) => decipher.decrypt(buffer, offset), Decipher::V3(decipher) => decipher.decrypt(buffer, offset),
Decipher::V5(decipher) => decipher.decrypt(buffer, offset),
} }
} }
} }

View File

@ -29,7 +29,7 @@ fn next_page_iv(seed: u32) -> u32 {
fn derive_page_aes_key(seed: u32) -> [u8; 0x10] { fn derive_page_aes_key(seed: u32) -> [u8; 0x10] {
let mut master_key = DEFAULT_MASTER_KEY; let mut master_key = DEFAULT_MASTER_KEY;
LE::write_u32(&mut master_key[0x10..0x14], seed); 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] { fn derive_page_aes_iv(seed: u32) -> [u8; 0x10] {

View File

@ -21,8 +21,8 @@ impl DecipherV2 {
#[test] #[test]
fn test_v2_init() -> Result<(), KugouError> { fn test_v2_init() -> Result<(), KugouError> {
let hdr_v2 = Header::from_buffer(include_bytes!("__fixtures__/kgm_v2_hdr.bin"))?; let hdr = Header::from_buffer(include_bytes!("__fixtures__/kgm_v2_hdr.bin"))?;
DecipherV2::new(&hdr_v2, b"1234")?; DecipherV2::new(&hdr, b"1234")?;
Ok(()) Ok(())
} }

17
um_crypto/kgm/src/v5.rs Normal file
View File

@ -0,0 +1,17 @@
use crate::KugouError;
use umc_qmc::QMCv2Cipher;
pub struct DecipherV5(QMCv2Cipher);
impl DecipherV5 {
pub fn new(ekey: &str) -> Result<Self, KugouError> {
let cipher = QMCv2Cipher::new_from_ekey(ekey)
.map_err(|e| KugouError::QMC2EKeyError(e.to_string()))?;
Ok(Self(cipher))
}
pub fn decrypt<T: AsMut<[u8]> + ?Sized>(&self, buffer: &mut T, offset: usize) {
self.0.decrypt(buffer, offset)
}
}

View File

@ -1,7 +1,7 @@
use md5::{Digest, Md5}; use md5::{Digest, Md5};
/// Calculate the MD5 hash (non-modified) of a buffer. /// Calculate the MD5 hash (non-modified) of a buffer.
pub fn md5<T: AsRef<[u8]>>(buffer: T) -> [u8; 16] { pub fn md5(buffer: impl AsRef<[u8]>) -> [u8; 0x10] {
Md5::digest(buffer).into() Md5::digest(buffer).into()
} }

View File

@ -2,18 +2,55 @@ use umc_kgm::{header::Header, Decipher};
use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsError; 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<Self, JsError> {
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. /// KuGou KGM file decipher.
#[wasm_bindgen(js_name=KuGou)] #[wasm_bindgen(js_name=KuGou)]
pub struct JsKuGou(Decipher); pub struct JsKuGou(Decipher);
#[wasm_bindgen(js_class = KuGou)] #[wasm_bindgen(js_class=KuGou)]
impl JsKuGou { impl JsKuGou {
/// Parse the KuGou header (0x400 bytes) /// Parse the KuGou header (0x400 bytes recommended).
pub fn from_header(header: &[u8]) -> Result<JsKuGou, JsError> { pub fn from_header(header: &[u8]) -> Result<Self, JsError> {
let header = Header::from_buffer(header).map_err(JsError::from)?; let header = JsKuGouHdr::new(header)?;
let decipher = Decipher::new(&header).map_err(JsError::from)?; 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<String>) -> Result<Self, JsError> {
let decipher = Decipher::new_v5(&header.0, ekey).map_err(JsError::from)?;
Ok(Self(decipher))
} }
/// Decrypt a buffer. /// Decrypt a buffer.

0
um_wasm_loader/build.js Normal file → Executable file
View File