Compare commits

...

2 Commits

Author SHA1 Message Date
c8af0b1211 [kgm] feat #2: basic kgm support 2024-09-15 22:15:02 +01:00
2222e7bc50 [kwm] refactor: rename cipher to decipher 2024-09-15 20:43:57 +01:00
18 changed files with 277 additions and 7 deletions

View File

@ -10,6 +10,7 @@
<sourceFolder url="file://$MODULE_DIR$/um_crypto/utils/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/um_crypto/ncm/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/um_audio/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/um_crypto/kgm/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
<excludeFolder url="file://$MODULE_DIR$/um_wasm_loader/dist" />
<excludeFolder url="file://$MODULE_DIR$/um_wasm_loader/pkg" />

42
Cargo.lock generated
View File

@ -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]]

View File

@ -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" }

44
um_cli/src/cmd/kgm.rs Normal file
View File

@ -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<i32> {
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)
}
}

View File

@ -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),
}

View File

@ -30,7 +30,7 @@ pub struct ArgsQMCv2 {
info_only: bool,
}
fn read_ekey(ekey: &str) -> Result<Box<[u8]>> {
fn read_ekey(ekey: &str) -> Result<Vec<u8>> {
let mut external_file = false;
let mut decrypt_ekey = true;
@ -54,7 +54,7 @@ fn read_ekey(ekey: &str) -> Result<Box<[u8]>> {
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)
}

View File

@ -30,6 +30,7 @@ fn run_command(cli: &Cli) -> Result<i32> {
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");

10
um_crypto/kgm/Cargo.toml Normal file
View File

@ -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" }

Binary file not shown.

View File

@ -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<T>(buffer: T) -> Result<Self, KugouError>
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<Box<dyn Decipher>, KugouError> {
let slot_key: &[u8] = match self.key_slot {
1 => b"l,/'",
slot => Err(KugouError::UnsupportedKeySlot(slot))?,
};
let decipher: Box<dyn Decipher> = 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)
}
}

21
um_crypto/kgm/src/lib.rs Normal file
View File

@ -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);
}

31
um_crypto/kgm/src/v2.rs Normal file
View File

@ -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<Self, KugouError> {
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(())
}

48
um_crypto/kgm/src/v3.rs Normal file
View File

@ -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<T: AsRef<[u8]>>(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<Self, KugouError> {
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);
}
}
}

View File

@ -105,7 +105,7 @@ impl Header {
})
}
pub fn get_cipher<T>(&self, ekey: Option<T>) -> Result<Cipher>
pub fn get_decipher<T>(&self, ekey: Option<T>) -> Result<Cipher>
where
T: AsRef<[u8]>,
{

View File

@ -5,3 +5,5 @@ edition = "2021"
[dependencies]
base64 = "0.22.1"
itertools = "0.13.0"
md-5 = "0.10.6"

View File

@ -1 +1,3 @@
pub mod base64;
mod md5;
pub use md5::md5;

View File

@ -0,0 +1,5 @@
use md5::{Digest, Md5};
pub fn md5<T: AsRef<[u8]>>(buffer: T) -> [u8; 16] {
Md5::digest(buffer).into()
}

View File

@ -25,9 +25,9 @@ impl JsKuwoHeader {
}
/// Create an instance of cipher (decipher) for decryption
#[wasm_bindgen(js_name=makeCipher)]
pub fn make_cipher(&self, ekey: Option<String>) -> Result<JsCipher, JsError> {
let cipher = self.0.get_cipher(ekey).map_err(map_js_error)?;
#[wasm_bindgen(js_name=makeDecipher)]
pub fn make_decipher(&self, ekey: Option<String>) -> Result<JsCipher, JsError> {
let cipher = self.0.get_decipher(ekey).map_err(map_js_error)?;
Ok(JsCipher(cipher))
}
}