feat: add kgm v5 (kgg) support.
This commit is contained in:
parent
02f0bb9a93
commit
54deabe74f
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -606,6 +606,7 @@ dependencies = [
|
|||||||
"byteorder",
|
"byteorder",
|
||||||
"cbc",
|
"cbc",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"umc_qmc",
|
||||||
"umc_utils",
|
"umc_utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
BIN
um_crypto/kgm/src/__fixtures__/kgm_invalid_magic.bin
Normal file
BIN
um_crypto/kgm/src/__fixtures__/kgm_invalid_magic.bin
Normal file
Binary file not shown.
BIN
um_crypto/kgm/src/__fixtures__/kgm_v3_hdr.bin
Normal file
BIN
um_crypto/kgm/src/__fixtures__/kgm_v3_hdr.bin
Normal file
Binary file not shown.
BIN
um_crypto/kgm/src/__fixtures__/kgm_v5_hdr.bin
Normal file
BIN
um_crypto/kgm/src/__fixtures__/kgm_v5_hdr.bin
Normal file
Binary file not shown.
@ -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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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] {
|
||||||
|
@ -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
17
um_crypto/kgm/src/v5.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
0
um_wasm_loader/build.js
Normal file → Executable file
Loading…
Reference in New Issue
Block a user