[kgm] feat #2: basic kgm support
This commit is contained in:
parent
2222e7bc50
commit
c8af0b1211
@ -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
42
Cargo.lock
generated
@ -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]]
|
||||
|
@ -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
44
um_cli/src/cmd/kgm.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -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),
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
10
um_crypto/kgm/Cargo.toml
Normal 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" }
|
BIN
um_crypto/kgm/src/__fixtures__/kgm_v2_hdr.bin
Normal file
BIN
um_crypto/kgm/src/__fixtures__/kgm_v2_hdr.bin
Normal file
Binary file not shown.
59
um_crypto/kgm/src/header.rs
Normal file
59
um_crypto/kgm/src/header.rs
Normal 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
21
um_crypto/kgm/src/lib.rs
Normal 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
31
um_crypto/kgm/src/v2.rs
Normal 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
48
um_crypto/kgm/src/v3.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -5,3 +5,5 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.22.1"
|
||||
itertools = "0.13.0"
|
||||
md-5 = "0.10.6"
|
||||
|
@ -1 +1,3 @@
|
||||
pub mod base64;
|
||||
mod md5;
|
||||
pub use md5::md5;
|
||||
|
5
um_crypto/utils/src/md5.rs
Normal file
5
um_crypto/utils/src/md5.rs
Normal file
@ -0,0 +1,5 @@
|
||||
use md5::{Digest, Md5};
|
||||
|
||||
pub fn md5<T: AsRef<[u8]>>(buffer: T) -> [u8; 16] {
|
||||
Md5::digest(buffer).into()
|
||||
}
|
Loading…
Reference in New Issue
Block a user