feat: add kugou db decryption logic

This commit is contained in:
鲁树人 2025-02-24 09:21:27 +09:00
parent acf3a814bd
commit 02f0bb9a93
13 changed files with 300 additions and 54 deletions

7
Cargo.lock generated
View File

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

View 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)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ use clap::Parser;
use std::process::exit;
use std::time::Instant;
mod buffered_decrypt;
mod cmd;
/// um_cli (rust ver.)

View File

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

View File

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

View 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,
],
);
}
}

View 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(())
}

View File

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

View File

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