feat: add kugou db decryption logic
This commit is contained in:
parent
acf3a814bd
commit
02f0bb9a93
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -1,6 +1,6 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
@ -601,8 +601,10 @@ dependencies = [
|
||||
name = "umc_kgm"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"block-padding",
|
||||
"byteorder",
|
||||
"itertools",
|
||||
"cbc",
|
||||
"thiserror",
|
||||
"umc_utils",
|
||||
]
|
||||
@ -674,7 +676,6 @@ version = "0.1.2"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"byteorder",
|
||||
"cbc",
|
||||
"ctr",
|
||||
"thiserror",
|
||||
"umc_utils",
|
||||
|
28
um_cli/src/buffered_decrypt.rs
Normal file
28
um_cli/src/buffered_decrypt.rs
Normal file
@ -0,0 +1,28 @@
|
||||
use std::io::{Read, Write};
|
||||
|
||||
pub fn buffered_decrypt<T, R, W>(
|
||||
f_in: &mut R,
|
||||
f_out: &mut W,
|
||||
buffer_size: usize,
|
||||
decipher: T,
|
||||
) -> anyhow::Result<usize>
|
||||
where
|
||||
R: Read,
|
||||
W: Write,
|
||||
T: Fn(&mut [u8], usize),
|
||||
{
|
||||
let mut offset = 0usize;
|
||||
let mut buffer = vec![0u8; buffer_size].into_boxed_slice();
|
||||
while let Ok(n) = f_in.read(&mut buffer) {
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let chunk = &mut buffer[..n];
|
||||
decipher(chunk, offset);
|
||||
f_out.write_all(chunk)?;
|
||||
offset += n;
|
||||
}
|
||||
|
||||
Ok(offset)
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
use crate::buffered_decrypt::buffered_decrypt;
|
||||
use crate::Cli;
|
||||
use clap::Args;
|
||||
use std::fs::File;
|
||||
@ -16,10 +17,34 @@ pub struct ArgsKGM {
|
||||
/// Path to input file, e.g. /export/Music/song.kgm
|
||||
#[arg(name = "input")]
|
||||
input: PathBuf,
|
||||
|
||||
/// File mode, one of "kgm" or "db", default to "kgm"
|
||||
#[arg(short, long, default_value = "kgm")]
|
||||
file_mode: String,
|
||||
}
|
||||
|
||||
impl ArgsKGM {
|
||||
pub fn run(&self, cli: &Cli) -> anyhow::Result<i32> {
|
||||
match self.file_mode.as_str() {
|
||||
"kgm" => self.decrypt_kgm_file(cli),
|
||||
"db" => self.decrypt_db_file(),
|
||||
_ => anyhow::bail!("Invalid file mode: {}", self.file_mode),
|
||||
}
|
||||
}
|
||||
|
||||
fn decrypt_db_file(&self) -> anyhow::Result<i32> {
|
||||
let mut file_input = File::open(&self.input)?;
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
file_input.read_to_end(&mut buffer)?;
|
||||
umc_kgm::decrypt_db(&mut buffer)?;
|
||||
let mut file_output = File::create(&self.output)?;
|
||||
file_output.write_all(&buffer)?;
|
||||
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
fn decrypt_kgm_file(&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)?;
|
||||
@ -27,18 +52,15 @@ impl ArgsKGM {
|
||||
let decipher = Decipher::new(&kgm_header)?;
|
||||
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;
|
||||
}
|
||||
buffered_decrypt(
|
||||
&mut file_input,
|
||||
&mut file_output,
|
||||
cli.buffer_size,
|
||||
|buffer, offset| {
|
||||
decipher.decrypt(buffer, offset);
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(0)
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
use crate::buffered_decrypt::buffered_decrypt;
|
||||
use crate::Cli;
|
||||
use clap::Args;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
use std::io::{Seek, SeekFrom, Write};
|
||||
use std::path::PathBuf;
|
||||
use umc_ncm::header::NCMFile;
|
||||
|
||||
@ -71,18 +72,14 @@ impl ArgsNCM {
|
||||
file_input.seek(SeekFrom::Start(ncm.audio_data_offset as u64))?;
|
||||
let mut file_output = File::create(output_path)?;
|
||||
|
||||
let mut offset = 0usize;
|
||||
let mut buffer = vec![0u8; cli.buffer_size].into_boxed_slice();
|
||||
|
||||
while let Ok(n) = file_input.read(&mut buffer) {
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
ncm.decrypt(&mut buffer[..n], offset);
|
||||
file_output.write_all(&buffer[..n])?;
|
||||
offset += n;
|
||||
}
|
||||
buffered_decrypt(
|
||||
&mut file_input,
|
||||
&mut file_output,
|
||||
cli.buffer_size,
|
||||
|buffer, offset| {
|
||||
ncm.decrypt(buffer, offset);
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(0)
|
||||
|
@ -1,8 +1,8 @@
|
||||
use crate::buffered_decrypt::buffered_decrypt;
|
||||
use crate::Cli;
|
||||
use anyhow::Result;
|
||||
use clap::Args;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Decrypt a QMCv1 file (QQMusic)
|
||||
@ -22,17 +22,12 @@ impl ArgsQMCv1 {
|
||||
let mut file_input = File::open(&self.input)?;
|
||||
let mut file_output = File::create(&self.output)?;
|
||||
|
||||
let mut offset = 0usize;
|
||||
let mut buffer = vec![0u8; cli.buffer_size].into_boxed_slice();
|
||||
while let Ok(n) = file_input.read(&mut buffer) {
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
umc_qmc::v1::decrypt(&mut buffer[..n], offset);
|
||||
file_output.write_all(&buffer[..n])?;
|
||||
offset += n;
|
||||
}
|
||||
buffered_decrypt(
|
||||
&mut file_input,
|
||||
&mut file_output,
|
||||
cli.buffer_size,
|
||||
umc_qmc::v1::decrypt,
|
||||
)?;
|
||||
|
||||
Ok(0)
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
use crate::buffered_decrypt::buffered_decrypt;
|
||||
use crate::Cli;
|
||||
use anyhow::{bail, Result};
|
||||
use clap::Args;
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
|
||||
use std::io::{BufReader, Read, Seek, SeekFrom};
|
||||
use std::path::PathBuf;
|
||||
use umc_qmc::{footer, QMCv2Cipher};
|
||||
use umc_utils::base64;
|
||||
@ -97,22 +98,19 @@ impl ArgsQMCv2 {
|
||||
|
||||
let mut file_output = match &self.output {
|
||||
None => bail!("--output is required"),
|
||||
Some(output) => BufWriter::new(File::create(output)?),
|
||||
Some(output) => File::create(output)?,
|
||||
};
|
||||
|
||||
let mut buffer = vec![0u8; cli.buffer_size];
|
||||
let reader = BufReader::with_capacity(cli.buffer_size, file_input);
|
||||
let mut reader = reader.take(input_size - footer_len as u64);
|
||||
let mut offset = 0usize;
|
||||
while let Ok(n) = reader.read(&mut buffer) {
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
cipher.decrypt(&mut buffer[..n], offset);
|
||||
file_output.write_all(&buffer[..n])?;
|
||||
offset += n;
|
||||
}
|
||||
buffered_decrypt(
|
||||
&mut reader,
|
||||
&mut file_output,
|
||||
cli.buffer_size,
|
||||
|buffer, offset| {
|
||||
cipher.decrypt(buffer, offset);
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(0)
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ use clap::Parser;
|
||||
use std::process::exit;
|
||||
use std::time::Instant;
|
||||
|
||||
mod buffered_decrypt;
|
||||
mod cmd;
|
||||
|
||||
/// um_cli (rust ver.)
|
||||
|
@ -5,6 +5,8 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
byteorder = "1.5.0"
|
||||
itertools = "0.13.0"
|
||||
thiserror = "2.0.7"
|
||||
umc_utils = { path = "../utils" }
|
||||
aes = "0.8.4"
|
||||
cbc = "0.1.2"
|
||||
block-padding = "0.3.3"
|
||||
|
@ -1,12 +1,17 @@
|
||||
pub mod header;
|
||||
mod pc_db_decrypt;
|
||||
pub mod v2;
|
||||
pub mod v3;
|
||||
|
||||
pub use pc_db_decrypt::decrypt_db;
|
||||
|
||||
use crate::header::Header;
|
||||
use crate::v2::DecipherV2;
|
||||
use crate::v3::DecipherV3;
|
||||
use thiserror::Error;
|
||||
|
||||
use block_padding::UnpadError;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum KugouError {
|
||||
#[error("Header too small, need at least {0} bytes.")]
|
||||
@ -23,6 +28,18 @@ pub enum KugouError {
|
||||
|
||||
#[error("Unsupported cipher (self-test failed)")]
|
||||
SelfTestFailed,
|
||||
|
||||
#[error("Failed decrypt kugou db data: {0}")]
|
||||
DecryptKugouDbError(UnpadError),
|
||||
|
||||
#[error("Invalid database size: {0}")]
|
||||
InvalidDatabaseSize(usize),
|
||||
|
||||
#[error("Failed to decrypt page 1 (invalid header)")]
|
||||
DecryptPage1Failed,
|
||||
|
||||
#[error("Database does not seem valid")]
|
||||
InvalidPage1Header,
|
||||
}
|
||||
|
||||
pub enum Decipher {
|
||||
|
114
um_crypto/kgm/src/pc_db_decrypt/key_derive.rs
Normal file
114
um_crypto/kgm/src/pc_db_decrypt/key_derive.rs
Normal file
@ -0,0 +1,114 @@
|
||||
use byteorder::{ByteOrder, LE};
|
||||
use umc_utils::md5;
|
||||
|
||||
use aes::cipher::{
|
||||
block_padding::NoPadding, generic_array::GenericArray, BlockDecryptMut, KeyIvInit,
|
||||
};
|
||||
|
||||
use crate::KugouError;
|
||||
|
||||
type Aes128CbcDec = cbc::Decryptor<aes::Aes128Dec>;
|
||||
|
||||
const DEFAULT_MASTER_KEY: [u8; 0x18] = [
|
||||
// master key (0x10 bytes)
|
||||
0x1D, 0x61, 0x31, 0x45, 0xB2, 0x47, 0xBF, 0x7F, 0x3D, 0x18, 0x96, 0x72, 0x14, 0x4F, 0xE4, 0xBF,
|
||||
0x00, 0x00, 0x00, 0x00, // page number (le)
|
||||
0x73, 0x41, 0x6C, 0x54, // fixed value
|
||||
];
|
||||
|
||||
fn next_page_iv(seed: u32) -> u32 {
|
||||
let left = seed.wrapping_mul(0x9EF4);
|
||||
let right = seed.wrapping_div(0xce26).wrapping_mul(0x7FFFFF07);
|
||||
let value = left.wrapping_sub(right);
|
||||
match value & 0x8000_0000 {
|
||||
0 => value,
|
||||
_ => value.wrapping_add(0x7FFF_FF07),
|
||||
}
|
||||
}
|
||||
|
||||
fn derive_page_aes_key(seed: u32) -> [u8; 0x10] {
|
||||
let mut master_key = DEFAULT_MASTER_KEY;
|
||||
LE::write_u32(&mut master_key[0x10..0x14], seed);
|
||||
md5(&mut master_key)
|
||||
}
|
||||
|
||||
fn derive_page_aes_iv(seed: u32) -> [u8; 0x10] {
|
||||
let mut buffer = [0u8; 0x10];
|
||||
let mut iv = seed + 1;
|
||||
for i in (0..0x10).step_by(4) {
|
||||
iv = next_page_iv(iv);
|
||||
LE::write_u32(&mut buffer[i..i + 4], iv);
|
||||
}
|
||||
md5(buffer)
|
||||
}
|
||||
|
||||
/// Page number starts from 1.
|
||||
/// Buffer should have size of ().
|
||||
pub fn decrypt_db_page(buffer: &mut [u8], page_number: u32) -> Result<(), KugouError> {
|
||||
let key = derive_page_aes_key(page_number);
|
||||
let iv = derive_page_aes_iv(page_number);
|
||||
|
||||
let key = GenericArray::from(key);
|
||||
let iv = GenericArray::from(iv);
|
||||
let dec = Aes128CbcDec::new(&key, &iv);
|
||||
dec.decrypt_padded_mut::<NoPadding>(buffer)
|
||||
.map_err(KugouError::DecryptKugouDbError)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn test_derive(page_no: u32, expected_key: [u8; 0x10], expected_iv: [u8; 0x10]) {
|
||||
let aes_key = derive_page_aes_key(page_no);
|
||||
assert_eq!(aes_key, expected_key, "key mismatch for page {}", page_no);
|
||||
|
||||
let aes_iv = derive_page_aes_iv(page_no);
|
||||
assert_eq!(aes_iv, expected_iv, "iv mismatch for page {}", page_no);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_derive_page_0_iv() {
|
||||
test_derive(
|
||||
0,
|
||||
[
|
||||
0x19, 0x62, 0xc0, 0x5f, 0xa2, 0xeb, 0xbe, 0x24, 0x28, 0xff, 0x52, 0x2b, 0x9e, 0x03,
|
||||
0xea, 0xd4,
|
||||
],
|
||||
[
|
||||
0x05, 0x5a, 0x67, 0x35, 0x93, 0x89, 0x2d, 0xdf, 0x3a, 0xb3, 0xb3, 0xc6, 0x21, 0xc3,
|
||||
0x48, 0x02,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_derive_page_12345_iv() {
|
||||
test_derive(
|
||||
12345,
|
||||
[
|
||||
0xc1, 0x70, 0x06, 0x4e, 0xf8, 0x1e, 0x15, 0x35, 0xc2, 0x9a, 0x65, 0xe4, 0xb6, 0xf5,
|
||||
0x78, 0xe9,
|
||||
],
|
||||
[
|
||||
0xd0, 0xcd, 0x91, 0xd0, 0x23, 0xc5, 0x1e, 0x21, 0xbc, 0x01, 0xaa, 0xd2, 0x81, 0x4c,
|
||||
0x9b, 0xb8,
|
||||
],
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_derive_page_498651347_iv() {
|
||||
test_derive(
|
||||
498651347,
|
||||
[
|
||||
0x5a, 0x69, 0xb3, 0xdc, 0x58, 0xca, 0x16, 0x2e, 0xb4, 0xa7, 0x71, 0x4e, 0xf2, 0x73,
|
||||
0x6b, 0xf7,
|
||||
],
|
||||
[
|
||||
0x62, 0xa7, 0x22, 0x26, 0x64, 0x08, 0x89, 0xb8, 0xff, 0x5d, 0xdc, 0x31, 0x7e, 0x7c,
|
||||
0x7e, 0xcc,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
71
um_crypto/kgm/src/pc_db_decrypt/mod.rs
Normal file
71
um_crypto/kgm/src/pc_db_decrypt/mod.rs
Normal file
@ -0,0 +1,71 @@
|
||||
mod key_derive;
|
||||
|
||||
use crate::KugouError;
|
||||
use byteorder::{ByteOrder, LE};
|
||||
use key_derive::decrypt_db_page;
|
||||
|
||||
const PAGE_SIZE: usize = 0x400;
|
||||
const SQLITE_HEADER: [u8; 0x10] = *b"SQLite format 3\0";
|
||||
|
||||
fn validate_page_1_header(header: &[u8]) -> Result<(), KugouError> {
|
||||
let o10 = LE::read_u32(&header[0x10..0x14]);
|
||||
let o14 = LE::read_u32(&header[0x14..0x18]);
|
||||
|
||||
let v6 = ((o10 & 0xff) << 8) | ((o10 & 0xff00) << 16);
|
||||
let ok = o14 == 0x20204000 && (v6 - 0x200) <= 0xFE00 && ((v6 - 1) & v6) == 0;
|
||||
if !ok {
|
||||
Err(KugouError::InvalidPage1Header)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn decrypt_db<T: AsMut<[u8]> + ?Sized>(buffer: &mut T) -> Result<(), KugouError> {
|
||||
let buffer = buffer.as_mut();
|
||||
let db_size = buffer.len();
|
||||
|
||||
// not encrypted
|
||||
if buffer.starts_with(&SQLITE_HEADER) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if db_size % PAGE_SIZE != 0 || db_size == 0 {
|
||||
Err(KugouError::InvalidDatabaseSize(db_size))?;
|
||||
}
|
||||
|
||||
let last_page = db_size / PAGE_SIZE;
|
||||
|
||||
// page 1 is the header
|
||||
decrypt_page_1(&mut buffer[0..PAGE_SIZE])?;
|
||||
|
||||
let mut offset = PAGE_SIZE;
|
||||
for page_no in 2..=last_page {
|
||||
decrypt_db_page(&mut buffer[offset..offset + PAGE_SIZE], page_no as u32)?;
|
||||
offset += PAGE_SIZE;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decrypt_page_1(page: &mut [u8]) -> Result<(), KugouError> {
|
||||
validate_page_1_header(page)?;
|
||||
|
||||
// Backup expected hdr value
|
||||
let mut expected_hdr_value = [0u8; 0x08];
|
||||
expected_hdr_value.copy_from_slice(&page[0x10..0x18]);
|
||||
|
||||
// Copy encrypted hdr over
|
||||
let (hdr, encrypted_page_data) = page.split_at_mut(0x10);
|
||||
encrypted_page_data[0..0x08].copy_from_slice(&hdr[0x08..0x10]);
|
||||
|
||||
decrypt_db_page(encrypted_page_data, 1)?;
|
||||
|
||||
// Validate header
|
||||
if encrypted_page_data[..8] != expected_hdr_value[..8] {
|
||||
Err(KugouError::DecryptPage1Failed)?;
|
||||
}
|
||||
|
||||
// Apply SQLite header
|
||||
hdr.copy_from_slice(&SQLITE_HEADER);
|
||||
|
||||
Ok(())
|
||||
}
|
@ -6,7 +6,6 @@ edition = "2021"
|
||||
[dependencies]
|
||||
aes = "0.8.4"
|
||||
byteorder = "1.5.0"
|
||||
cbc = "0.1.2"
|
||||
ctr = "0.9.2"
|
||||
thiserror = "2.0.7"
|
||||
umc_utils = { path = "../utils" }
|
||||
|
@ -1,5 +1,6 @@
|
||||
use md5::{Digest, Md5};
|
||||
|
||||
/// Calculate the MD5 hash (non-modified) of a buffer.
|
||||
pub fn md5<T: AsRef<[u8]>>(buffer: T) -> [u8; 16] {
|
||||
Md5::digest(buffer).into()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user