From c8af0b12113b13f6c07f1b9e576024dd6e13c28e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Sun, 15 Sep 2024 22:15:02 +0100 Subject: [PATCH] [kgm] feat #2: basic kgm support --- .idea/lib_um_crypto.iml | 1 + Cargo.lock | 42 +++++++++++++ um_cli/Cargo.toml | 3 +- um_cli/src/cmd/kgm.rs | 44 +++++++++++++ um_cli/src/cmd/mod.rs | 3 + um_cli/src/cmd/qmc2.rs | 4 +- um_cli/src/main.rs | 1 + um_crypto/kgm/Cargo.toml | 10 +++ um_crypto/kgm/src/__fixtures__/kgm_v2_hdr.bin | Bin 0 -> 80 bytes um_crypto/kgm/src/header.rs | 59 ++++++++++++++++++ um_crypto/kgm/src/lib.rs | 21 +++++++ um_crypto/kgm/src/v2.rs | 31 +++++++++ um_crypto/kgm/src/v3.rs | 48 ++++++++++++++ um_crypto/utils/Cargo.toml | 2 + um_crypto/utils/src/lib.rs | 2 + um_crypto/utils/src/md5.rs | 5 ++ 16 files changed, 273 insertions(+), 3 deletions(-) create mode 100644 um_cli/src/cmd/kgm.rs create mode 100644 um_crypto/kgm/Cargo.toml create mode 100644 um_crypto/kgm/src/__fixtures__/kgm_v2_hdr.bin create mode 100644 um_crypto/kgm/src/header.rs create mode 100644 um_crypto/kgm/src/lib.rs create mode 100644 um_crypto/kgm/src/v2.rs create mode 100644 um_crypto/kgm/src/v3.rs create mode 100644 um_crypto/utils/src/md5.rs diff --git a/.idea/lib_um_crypto.iml b/.idea/lib_um_crypto.iml index 89f51a2..3dbbad2 100644 --- a/.idea/lib_um_crypto.iml +++ b/.idea/lib_um_crypto.iml @@ -10,6 +10,7 @@ + diff --git a/Cargo.lock b/Cargo.lock index 5ad2119..468c733 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,6 +74,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block-padding" version = "0.3.3" @@ -216,6 +225,16 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "either" version = "1.13.0" @@ -303,6 +322,16 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "minicov" version = "0.3.5" @@ -441,6 +470,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", + "umc_kgm", "umc_kuwo", "umc_ncm", "umc_qmc", @@ -462,6 +492,16 @@ dependencies = [ "wasm-bindgen-test", ] +[[package]] +name = "umc_kgm" +version = "0.1.0" +dependencies = [ + "byteorder", + "itertools", + "thiserror", + "umc_utils", +] + [[package]] name = "umc_kuwo" version = "0.1.0" @@ -507,6 +547,8 @@ name = "umc_utils" version = "0.1.0" dependencies = [ "base64", + "itertools", + "md-5", ] [[package]] diff --git a/um_cli/Cargo.toml b/um_cli/Cargo.toml index 7e516a0..849154f 100644 --- a/um_cli/Cargo.toml +++ b/um_cli/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" anyhow = "1.0.86" clap = { version = "4.5.17", features = ["derive"] } umc_kuwo = { path = "../um_crypto/kuwo" } -umc_qmc = { path = "../um_crypto/qmc" } +umc_kgm = { path = "../um_crypto/kgm" } umc_ncm = { path = "../um_crypto/ncm" } +umc_qmc = { path = "../um_crypto/qmc" } umc_utils = { path = "../um_crypto/utils" } diff --git a/um_cli/src/cmd/kgm.rs b/um_cli/src/cmd/kgm.rs new file mode 100644 index 0000000..d969ae7 --- /dev/null +++ b/um_cli/src/cmd/kgm.rs @@ -0,0 +1,44 @@ +use crate::Cli; +use clap::Args; +use std::fs::File; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::path::PathBuf; +use umc_kgm::header::Header; + +/// Decrypt a KGM/VPR file (Kugou Music) +#[derive(Args)] +pub struct ArgsKGM { + /// Path to output file, e.g. /export/Music/song.flac + #[arg(short, long)] + output: PathBuf, + + /// Path to input file, e.g. /export/Music/song.kgm + #[arg(name = "input")] + input: PathBuf, +} + +impl ArgsKGM { + pub fn run(&self, cli: &Cli) -> anyhow::Result { + let mut file_input = File::open(&self.input)?; + let mut header = [0u8; 0x40]; + file_input.read_exact(&mut header)?; + let kgm_header = Header::from_buffer(&mut header)?; + let decipher = kgm_header.make_decipher()?; + file_input.seek(SeekFrom::Start(kgm_header.offset_to_data as u64))?; + + let mut offset = 0usize; + let mut buffer = vec![0u8; cli.buffer_size].into_boxed_slice(); + let mut file_output = File::create(&self.output)?; + while let Ok(n) = file_input.read(&mut buffer) { + if n == 0 { + break; + } + + decipher.decrypt(&mut buffer[..n], offset); + file_output.write_all(&buffer[..n])?; + offset += n; + } + + Ok(0) + } +} diff --git a/um_cli/src/cmd/mod.rs b/um_cli/src/cmd/mod.rs index 07c9f21..69e390c 100644 --- a/um_cli/src/cmd/mod.rs +++ b/um_cli/src/cmd/mod.rs @@ -1,5 +1,6 @@ use clap::Subcommand; +pub mod kgm; pub mod ncm; pub mod qmc1; pub mod qmc2; @@ -12,4 +13,6 @@ pub enum Commands { QMCv2(qmc2::ArgsQMCv2), #[command(name = "ncm")] NCM(ncm::ArgsNCM), + #[command(name = "kgm")] + KGM(kgm::ArgsKGM), } diff --git a/um_cli/src/cmd/qmc2.rs b/um_cli/src/cmd/qmc2.rs index e0d124e..e9cb80b 100644 --- a/um_cli/src/cmd/qmc2.rs +++ b/um_cli/src/cmd/qmc2.rs @@ -30,7 +30,7 @@ pub struct ArgsQMCv2 { info_only: bool, } -fn read_ekey(ekey: &str) -> Result> { +fn read_ekey(ekey: &str) -> Result> { let mut external_file = false; let mut decrypt_ekey = true; @@ -54,7 +54,7 @@ fn read_ekey(ekey: &str) -> Result> { let ekey = ekey.trim(); let ekey = match decrypt_ekey { true => umc_qmc::ekey::decrypt(ekey)?, - false => base64::decode(ekey)?.into_boxed_slice(), + false => base64::decode(ekey)?, }; Ok(ekey) } diff --git a/um_cli/src/main.rs b/um_cli/src/main.rs index 640c44d..d60d847 100644 --- a/um_cli/src/main.rs +++ b/um_cli/src/main.rs @@ -30,6 +30,7 @@ fn run_command(cli: &Cli) -> Result { Some(Commands::QMCv1(cmd)) => cmd.run(&cli), Some(Commands::QMCv2(cmd)) => cmd.run(&cli), Some(Commands::NCM(cmd)) => cmd.run(&cli), + Some(Commands::KGM(cmd)) => cmd.run(&cli), None => { // https://github.com/clap-rs/clap/issues/3857#issuecomment-1161796261 todo!("implement a sensible default command, similar to um/cli"); diff --git a/um_crypto/kgm/Cargo.toml b/um_crypto/kgm/Cargo.toml new file mode 100644 index 0000000..0e6488d --- /dev/null +++ b/um_crypto/kgm/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "umc_kgm" +version = "0.1.0" +edition = "2021" + +[dependencies] +byteorder = "1.5.0" +itertools = "0.13.0" +thiserror = "1.0.63" +umc_utils = { path = "../utils" } diff --git a/um_crypto/kgm/src/__fixtures__/kgm_v2_hdr.bin b/um_crypto/kgm/src/__fixtures__/kgm_v2_hdr.bin new file mode 100644 index 0000000000000000000000000000000000000000..59fc48681ce3740bae7a28d92029ec213bf09702 GIT binary patch literal 80 zcmb=qYV^8|sos0V`el9m|7VIYurM$%F)%PN0h5XP;@3DcgA6?-9p$#K(oOox TzBNFpKh|4oPS7Wy5)1$U9Lp5? literal 0 HcmV?d00001 diff --git a/um_crypto/kgm/src/header.rs b/um_crypto/kgm/src/header.rs new file mode 100644 index 0000000..5526049 --- /dev/null +++ b/um_crypto/kgm/src/header.rs @@ -0,0 +1,59 @@ +use crate::v2::DecipherV2; +use crate::v3::DecipherV3; +use crate::{Decipher, KugouError}; +use byteorder::{ByteOrder, LE}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Header { + pub magic: [u8; 0x10], + pub offset_to_data: usize, + pub crypto_version: u32, + pub key_slot: u32, + pub decrypt_test_data: [u8; 0x10], + pub file_key: [u8; 0x10], +} + +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))?; + } + + let mut magic = [0u8; 0x10]; + magic.copy_from_slice(&buffer[..0x10]); + 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]); + + Ok(Self { + magic, + offset_to_data, + crypto_version, + key_slot, + decrypt_test_data, + file_key, + }) + } + + pub fn make_decipher(&self) -> Result, KugouError> { + let slot_key: &[u8] = match self.key_slot { + 1 => b"l,/'", + slot => Err(KugouError::UnsupportedKeySlot(slot))?, + }; + + let decipher: Box = match self.crypto_version { + 2 => Box::from(DecipherV2::new(self, slot_key)?), + 3 => Box::from(DecipherV3::new(self, slot_key)?), + version => Err(KugouError::UnsupportedCipherVersion(version))?, + }; + Ok(decipher) + } +} diff --git a/um_crypto/kgm/src/lib.rs b/um_crypto/kgm/src/lib.rs new file mode 100644 index 0000000..80ccaaa --- /dev/null +++ b/um_crypto/kgm/src/lib.rs @@ -0,0 +1,21 @@ +pub mod header; +pub mod v2; +mod v3; + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum KugouError { + #[error("Header too small, need at least {0} bytes.")] + HeaderTooSmall(usize), + + #[error("Unsupported key slot: {0}")] + UnsupportedKeySlot(u32), + + #[error("Unsupported cipher version: {0}")] + UnsupportedCipherVersion(u32), +} + +pub trait Decipher { + fn decrypt(&self, buffer: &mut [u8], offset: usize); +} diff --git a/um_crypto/kgm/src/v2.rs b/um_crypto/kgm/src/v2.rs new file mode 100644 index 0000000..00d5732 --- /dev/null +++ b/um_crypto/kgm/src/v2.rs @@ -0,0 +1,31 @@ +use crate::header::Header; +use crate::{Decipher, KugouError}; + +pub struct DecipherV2 { + key: [u8; 4], +} + +impl DecipherV2 { + pub fn new(_header: &Header, slot_key: &[u8]) -> Result { + let mut key = [0u8; 4]; + key.copy_from_slice(slot_key); + Ok(Self { key }) + } +} + +impl Decipher for DecipherV2 { + fn decrypt(&self, buffer: &mut [u8], offset: usize) { + let key_stream = self.key.iter().cycle().skip(offset % self.key.len()); + for (datum, &k) in buffer.iter_mut().zip(key_stream) { + *datum ^= k; + } + } +} + +#[test] +fn test_v2_init() -> Result<(), KugouError> { + let hdr_v2 = Header::from_buffer(include_bytes!("__fixtures__/kgm_v2_hdr.bin"))?; + DecipherV2::new(&hdr_v2)?; + + Ok(()) +} diff --git a/um_crypto/kgm/src/v3.rs b/um_crypto/kgm/src/v3.rs new file mode 100644 index 0000000..a3cf6da --- /dev/null +++ b/um_crypto/kgm/src/v3.rs @@ -0,0 +1,48 @@ +use crate::header::Header; +use crate::{Decipher, KugouError}; + +pub struct DecipherV3 { + slot_key: [u8; 16], + file_key: [u8; 17], +} + +impl DecipherV3 { + fn hash_key>(data: T) -> [u8; 16] { + let digest = umc_utils::md5(data); + let mut result = [0u8; 16]; + for (result, digest) in result.rchunks_exact_mut(2).zip(digest.chunks_exact(2)) { + result[0] = digest[0]; + result[1] = digest[1]; + } + result + } + + pub fn new(header: &Header, slot_key: &[u8]) -> Result { + let slot_key = Self::hash_key(slot_key); + + let mut file_key = [0x6b; 17]; + file_key[..16].copy_from_slice(&Self::hash_key(header.file_key)); + + Ok(Self { slot_key, file_key }) + } +} + +impl Decipher for DecipherV3 { + fn decrypt(&self, buffer: &mut [u8], offset: usize) { + let slot_key_stream = self.slot_key.iter().cycle().skip(offset); + let file_key_stream = self.file_key.iter().cycle().skip(offset); + + let mut offset = offset as u32; + let key_stream = slot_key_stream.zip(file_key_stream); + for (datum, (&slot_key, &file_key)) in buffer.iter_mut().zip(key_stream) { + let mut temp = *datum; + temp ^= file_key; + temp ^= temp.wrapping_shl(4); + temp ^= slot_key; + temp ^= offset.to_ne_bytes().iter().fold(0, |acc, &x| acc ^ x); + *datum = temp; + + offset = offset.wrapping_add(1); + } + } +} diff --git a/um_crypto/utils/Cargo.toml b/um_crypto/utils/Cargo.toml index 71b9d4a..38efb79 100644 --- a/um_crypto/utils/Cargo.toml +++ b/um_crypto/utils/Cargo.toml @@ -5,3 +5,5 @@ edition = "2021" [dependencies] base64 = "0.22.1" +itertools = "0.13.0" +md-5 = "0.10.6" diff --git a/um_crypto/utils/src/lib.rs b/um_crypto/utils/src/lib.rs index 39abdce..173ad65 100644 --- a/um_crypto/utils/src/lib.rs +++ b/um_crypto/utils/src/lib.rs @@ -1 +1,3 @@ pub mod base64; +mod md5; +pub use md5::md5; diff --git a/um_crypto/utils/src/md5.rs b/um_crypto/utils/src/md5.rs new file mode 100644 index 0000000..78b39b8 --- /dev/null +++ b/um_crypto/utils/src/md5.rs @@ -0,0 +1,5 @@ +use md5::{Digest, Md5}; + +pub fn md5>(buffer: T) -> [u8; 16] { + Md5::digest(buffer).into() +}