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

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