diff --git a/Cargo.lock b/Cargo.lock index 04dbae6..050feb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/um_cli/src/buffered_decrypt.rs b/um_cli/src/buffered_decrypt.rs new file mode 100644 index 0000000..79383ff --- /dev/null +++ b/um_cli/src/buffered_decrypt.rs @@ -0,0 +1,28 @@ +use std::io::{Read, Write}; + +pub fn buffered_decrypt( + f_in: &mut R, + f_out: &mut W, + buffer_size: usize, + decipher: T, +) -> anyhow::Result +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) +} diff --git a/um_cli/src/cmd/kgm.rs b/um_cli/src/cmd/kgm.rs index fe5fa6e..1114188 100644 --- a/um_cli/src/cmd/kgm.rs +++ b/um_cli/src/cmd/kgm.rs @@ -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 { + 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 { + 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 { 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) } diff --git a/um_cli/src/cmd/ncm.rs b/um_cli/src/cmd/ncm.rs index 735bc9c..8b3a01a 100644 --- a/um_cli/src/cmd/ncm.rs +++ b/um_cli/src/cmd/ncm.rs @@ -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) diff --git a/um_cli/src/cmd/qmc1.rs b/um_cli/src/cmd/qmc1.rs index 2941149..4140300 100644 --- a/um_cli/src/cmd/qmc1.rs +++ b/um_cli/src/cmd/qmc1.rs @@ -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) } diff --git a/um_cli/src/cmd/qmc2.rs b/um_cli/src/cmd/qmc2.rs index e9cb80b..20e7021 100644 --- a/um_cli/src/cmd/qmc2.rs +++ b/um_cli/src/cmd/qmc2.rs @@ -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) } diff --git a/um_cli/src/main.rs b/um_cli/src/main.rs index 6cf6f86..6ca0f0d 100644 --- a/um_cli/src/main.rs +++ b/um_cli/src/main.rs @@ -4,6 +4,7 @@ use clap::Parser; use std::process::exit; use std::time::Instant; +mod buffered_decrypt; mod cmd; /// um_cli (rust ver.) diff --git a/um_crypto/kgm/Cargo.toml b/um_crypto/kgm/Cargo.toml index 190c10a..562d697 100644 --- a/um_crypto/kgm/Cargo.toml +++ b/um_crypto/kgm/Cargo.toml @@ -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" diff --git a/um_crypto/kgm/src/lib.rs b/um_crypto/kgm/src/lib.rs index fc26197..5b2d367 100644 --- a/um_crypto/kgm/src/lib.rs +++ b/um_crypto/kgm/src/lib.rs @@ -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 { diff --git a/um_crypto/kgm/src/pc_db_decrypt/key_derive.rs b/um_crypto/kgm/src/pc_db_decrypt/key_derive.rs new file mode 100644 index 0000000..0730eb4 --- /dev/null +++ b/um_crypto/kgm/src/pc_db_decrypt/key_derive.rs @@ -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; + +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::(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, + ], + ); + } +} diff --git a/um_crypto/kgm/src/pc_db_decrypt/mod.rs b/um_crypto/kgm/src/pc_db_decrypt/mod.rs new file mode 100644 index 0000000..c5c7d8b --- /dev/null +++ b/um_crypto/kgm/src/pc_db_decrypt/mod.rs @@ -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 + ?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(()) +} diff --git a/um_crypto/qtfm/Cargo.toml b/um_crypto/qtfm/Cargo.toml index f58e9ef..aaa383c 100644 --- a/um_crypto/qtfm/Cargo.toml +++ b/um_crypto/qtfm/Cargo.toml @@ -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" } diff --git a/um_crypto/utils/src/md5.rs b/um_crypto/utils/src/md5.rs index ca38833..d71d58d 100644 --- a/um_crypto/utils/src/md5.rs +++ b/um_crypto/utils/src/md5.rs @@ -1,5 +1,6 @@ use md5::{Digest, Md5}; +/// Calculate the MD5 hash (non-modified) of a buffer. pub fn md5>(buffer: T) -> [u8; 16] { Md5::digest(buffer).into() }