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 0000000..59fc486
Binary files /dev/null and b/um_crypto/kgm/src/__fixtures__/kgm_v2_hdr.bin differ
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..b89c2a7
--- /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, b"1234")?;
+
+ 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()
+}