From 0da553c4dd065f1c68ceef52d9bede9c7702bed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Fri, 20 Sep 2024 23:11:51 +0100 Subject: [PATCH] [mg3d] feat #3: implement migu 3d decipher with improved key guessing --- .idea/lib_um_crypto.iml | 1 + Cargo.lock | 10 +++++ um_cli/Cargo.toml | 1 + um_cli/src/cmd/mg3d.rs | 70 +++++++++++++++++++++++++++++++ um_cli/src/cmd/mod.rs | 30 +++++++++----- um_cli/src/main.rs | 11 ++--- um_crypto/mg3d/Cargo.toml | 9 ++++ um_crypto/mg3d/Readme.MD | 29 +++++++++++++ um_crypto/mg3d/src/guess_m4a.rs | 73 +++++++++++++++++++++++++++++++++ um_crypto/mg3d/src/guess_wav.rs | 28 +++++++++++++ um_crypto/mg3d/src/lib.rs | 61 +++++++++++++++++++++++++++ um_crypto/utils/src/lib.rs | 2 +- um_crypto/utils/src/md5.rs | 7 ++++ 13 files changed, 316 insertions(+), 16 deletions(-) create mode 100644 um_cli/src/cmd/mg3d.rs create mode 100644 um_crypto/mg3d/Cargo.toml create mode 100644 um_crypto/mg3d/Readme.MD create mode 100644 um_crypto/mg3d/src/guess_m4a.rs create mode 100644 um_crypto/mg3d/src/guess_wav.rs create mode 100644 um_crypto/mg3d/src/lib.rs diff --git a/.idea/lib_um_crypto.iml b/.idea/lib_um_crypto.iml index 1ce6676..7f78325 100644 --- a/.idea/lib_um_crypto.iml +++ b/.idea/lib_um_crypto.iml @@ -15,6 +15,7 @@ + diff --git a/Cargo.lock b/Cargo.lock index d622263..9fe22fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -535,6 +535,7 @@ dependencies = [ "umc_joox", "umc_kgm", "umc_kuwo", + "umc_mg3d", "umc_ncm", "umc_qmc", "umc_qtfm", @@ -600,6 +601,15 @@ dependencies = [ "umc_utils", ] +[[package]] +name = "umc_mg3d" +version = "0.1.0" +dependencies = [ + "hex", + "thiserror", + "umc_utils", +] + [[package]] name = "umc_ncm" version = "0.1.0" diff --git a/um_cli/Cargo.toml b/um_cli/Cargo.toml index 1abfc9e..82e0c4a 100644 --- a/um_cli/Cargo.toml +++ b/um_cli/Cargo.toml @@ -10,6 +10,7 @@ hex = "0.4.3" umc_joox = { path = "../um_crypto/joox" } umc_kgm = { path = "../um_crypto/kgm" } umc_kuwo = { path = "../um_crypto/kuwo" } +umc_mg3d = { path = "../um_crypto/mg3d" } umc_ncm = { path = "../um_crypto/ncm" } umc_qmc = { path = "../um_crypto/qmc" } umc_qtfm = { path = "../um_crypto/qtfm" } diff --git a/um_cli/src/cmd/mg3d.rs b/um_cli/src/cmd/mg3d.rs new file mode 100644 index 0000000..c4cd748 --- /dev/null +++ b/um_cli/src/cmd/mg3d.rs @@ -0,0 +1,70 @@ +use crate::Cli; +use anyhow::{bail, Result}; +use clap::Args; +use std::fs::File; +use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write}; +use std::path::PathBuf; +use umc_mg3d::{guess_key, Decipher}; + +/// Decrypt a mg3d file (Migu 3D Audio) +#[derive(Args)] +pub struct ArgsMigu3D { + /// Path to output file, e.g. /export/Music/song.wav + #[clap(short, long)] + output: PathBuf, + + /// Path to input file, e.g. /export/Music/song.mg3d + #[arg(name = "input")] + input: PathBuf, + + /// File key (androidFileKey/iosFileKey). Leave empty to guess the key. + #[clap(short = 'k', long = "file-key")] + file_key: Option, +} + +impl ArgsMigu3D { + pub fn run(&self, cli: &Cli) -> Result { + let mut reader = BufReader::with_capacity(cli.buffer_size, File::open(&self.input)?); + let mut writer = BufWriter::with_capacity(cli.buffer_size, File::create(&self.output)?); + + let decipher = self.make_decipher(&mut reader)?; + if cli.verbose { + println!("final key: {}", hex::encode(decipher.get_key())); + } + + let mut offset = 0usize; + let mut buffer = vec![0u8; cli.buffer_size]; + loop { + let n = reader.read(&mut buffer[..])?; + if n == 0 { + break; + } + decipher.decrypt(&mut buffer[..n], offset); + writer.write_all(&buffer[..n])?; + offset += n; + } + + Ok(0) + } + + fn make_decipher(&self, reader: &mut T) -> Result + where + T: Read + Seek + ?Sized, + { + let decipher = match &self.file_key { + None => { + let mut buffer = [0u8; 0x100]; + reader.read_exact(&mut buffer)?; + reader.seek(SeekFrom::Current(-0x100))?; + let key = match guess_key(&buffer) { + Some(key) => key, + None => bail!("failed to guess a valid key"), + }; + Decipher::new_from_final_key(&key)? + } + Some(key) => Decipher::new_from_file_key(key)?, + }; + + Ok(decipher) + } +} diff --git a/um_cli/src/cmd/mod.rs b/um_cli/src/cmd/mod.rs index a31853c..bdea98a 100644 --- a/um_cli/src/cmd/mod.rs +++ b/um_cli/src/cmd/mod.rs @@ -2,6 +2,7 @@ use clap::Subcommand; pub mod joox; pub mod kgm; +pub mod mg3d; pub mod ncm; pub mod qmc1; pub mod qmc2; @@ -11,20 +12,29 @@ pub mod xmly; #[derive(Subcommand)] pub enum Commands { + #[command(name = "ncm")] + NCM(ncm::ArgsNCM), + + #[command(name = "kgm")] + KGM(kgm::ArgsKGM), + + #[command(name = "mg3d")] + Migu3D(mg3d::ArgsMigu3D), + + #[command(name = "joox")] + JOOX(joox::ArgsJoox), + #[command(name = "qmc1")] QMCv1(qmc1::ArgsQMCv1), #[command(name = "qmc2")] QMCv2(qmc2::ArgsQMCv2), - #[command(name = "ncm")] - NCM(ncm::ArgsNCM), - #[command(name = "kgm")] - KGM(kgm::ArgsKGM), - #[command(name = "joox")] - JOOX(joox::ArgsJoox), - #[command(name = "xmly")] - XMLY(xmly::ArgsXimalaya), - #[command(name = "xiami")] - Xiami(xiami::ArgsXiami), + #[command(name = "qtfm")] QTFM(qtfm::ArgsQingTingFM), + + #[command(name = "xiami")] + Xiami(xiami::ArgsXiami), + + #[command(name = "xmly")] + XMLY(xmly::ArgsXimalaya), } diff --git a/um_cli/src/main.rs b/um_cli/src/main.rs index 1a37c2e..6cf6f86 100644 --- a/um_cli/src/main.rs +++ b/um_cli/src/main.rs @@ -27,14 +27,15 @@ pub struct Cli { fn run_command(cli: &Cli) -> Result { match &cli.command { + Some(Commands::JOOX(cmd)) => cmd.run(&cli), + Some(Commands::KGM(cmd)) => cmd.run(&cli), + Some(Commands::Migu3D(cmd)) => cmd.run(&cli), + Some(Commands::NCM(cmd)) => cmd.run(&cli), 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), - Some(Commands::JOOX(cmd)) => cmd.run(&cli), - Some(Commands::XMLY(cmd)) => cmd.run(&cli), - Some(Commands::Xiami(cmd)) => cmd.run(&cli), Some(Commands::QTFM(cmd)) => cmd.run(&cli), + Some(Commands::Xiami(cmd)) => cmd.run(&cli), + Some(Commands::XMLY(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"); diff --git a/um_crypto/mg3d/Cargo.toml b/um_crypto/mg3d/Cargo.toml new file mode 100644 index 0000000..f8f6d7e --- /dev/null +++ b/um_crypto/mg3d/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "umc_mg3d" +version = "0.1.0" +edition = "2021" + +[dependencies] +hex = "0.4.3" +thiserror = "1.0.63" +umc_utils = { path = "../utils" } diff --git a/um_crypto/mg3d/Readme.MD b/um_crypto/mg3d/Readme.MD new file mode 100644 index 0000000..ce4833e --- /dev/null +++ b/um_crypto/mg3d/Readme.MD @@ -0,0 +1,29 @@ +# 咪咕 3D 音乐 + +整个文件加密,每个文件有独立的密钥。 + +```py +mg3d_file_key <- md5(b"AC89EC47A70B76F307CB39A0D74BCCB0" + file_key).hex(upper=true) +``` + +生成最终的 `mg3d_file_key` 后,每个字节依序减少密钥的字节内容。 + +## WAV 格式 + +几乎固定的文件头。 + +```text +000:0000 52 49 46 46 ?? ?? ?? ?? 57 41 56 45 66 6D 74 20 RIFF....WAVEfmt +000:0010 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ................ +000:0020 06 00 18 00 6A 75 6E 6B 34 00 00 00 00 00 00 00 ....junk4....... +000:0030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ +000:0040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ +000:0050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ +000:0060 64 61 74 61 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? data............ +``` + +验证一下前四字节是否解密得到 `RIFF`、以及 0x60 处是否位 `data` 来确定。 + +## M4A 格式 + +前 0x1C 字节固定 (其实前 4 字节有可能会变,但他们用的编码器好像不会变) diff --git a/um_crypto/mg3d/src/guess_m4a.rs b/um_crypto/mg3d/src/guess_m4a.rs new file mode 100644 index 0000000..9afca97 --- /dev/null +++ b/um_crypto/mg3d/src/guess_m4a.rs @@ -0,0 +1,73 @@ +use crate::is_valid_password_chr; +use std::collections::HashMap; + +const GUESS_PLAIN_TEXT: [u8; 0x20] = [ + 0x00, 0x00, 0x00, 0x00, 0x66, 0x74, 0x79, 0x70, 0x4D, 0x34, 0x41, 0x20, 0x00, 0x00, 0x00, 0x00, + 0x4D, 0x34, 0x41, 0x20, 0x6D, 0x70, 0x34, 0x32, 0x69, 0x73, 0x6F, 0x6D, 0x00, 0x00, 0x00, 0x00, +]; + +type ByteFreq = HashMap; +fn get_highest_freq_item(freq: &ByteFreq) -> u8 { + let mut current_item = 0u8; + let mut current_count = 0usize; + + for (&item, &count) in freq.iter() { + if count > current_count { + current_item = item; + current_count = count; + } + } + + current_item +} + +pub fn guess_key(buffer: &[u8]) -> Option<[u8; 0x20]> { + if buffer.len() < 0x100 { + // buffer too small + None? + } + + let mut key = [0u8; 0x20]; + key.copy_from_slice(&buffer[0..0x20]); + + for (k, plain) in key.iter_mut().zip(GUESS_PLAIN_TEXT) { + *k = k.wrapping_sub(plain); + } + if !&key[0x04..0x1C].iter().all(|&k| is_valid_password_chr(k)) { + // Includes non-password chr + None? + } + + let mut password_0x03_freq = ByteFreq::new(); + let mut password_0x1c_freq = ByteFreq::new(); + let mut password_0x1d_freq = ByteFreq::new(); + let mut password_0x1e_freq = ByteFreq::new(); + let mut password_0x1f_freq = ByteFreq::new(); + + let increment_password_freq_count = |freq: &mut ByteFreq, item: u8| { + if is_valid_password_chr(item) { + freq.entry(item) + .and_modify(|counter| *counter += 1) + .or_insert(1); + } + }; + + for chunk in buffer[..0x100].chunks(0x20) { + increment_password_freq_count(&mut password_0x03_freq, chunk[0x03]); + increment_password_freq_count(&mut password_0x1c_freq, chunk[0x1c]); + increment_password_freq_count(&mut password_0x1d_freq, chunk[0x1d]); + increment_password_freq_count(&mut password_0x1e_freq, chunk[0x1e]); + increment_password_freq_count(&mut password_0x1f_freq, chunk[0x1f]); + } + key[0x03] = get_highest_freq_item(&password_0x03_freq); + key[0x1C] = get_highest_freq_item(&password_0x1c_freq); + key[0x1D] = get_highest_freq_item(&password_0x1d_freq); + key[0x1E] = get_highest_freq_item(&password_0x1e_freq); + key[0x1F] = get_highest_freq_item(&password_0x1f_freq); + + if is_valid_password_chr(key[0x03]) && key[0x1c..].iter().all(|&c| is_valid_password_chr(c)) { + Some(key) + } else { + None + } +} diff --git a/um_crypto/mg3d/src/guess_wav.rs b/um_crypto/mg3d/src/guess_wav.rs new file mode 100644 index 0000000..3416b89 --- /dev/null +++ b/um_crypto/mg3d/src/guess_wav.rs @@ -0,0 +1,28 @@ +use crate::{is_valid_password_chr, raw_decrypt}; + +pub fn guess_key(buffer: &[u8]) -> Option<[u8; 0x20]> { + if buffer.len() < 0x100 { + // buffer too small + None? + } + + let mut key = [0u8; 0x20]; + key.copy_from_slice(&buffer[0x40..0x60]); + if !key.iter().all(|&k| is_valid_password_chr(k)) { + // Not valid password + None? + } + + let mut test_riff = [0u8; 4]; + test_riff.copy_from_slice(&buffer[0..4]); + raw_decrypt(&mut test_riff, &key, 0x00); + + let mut test_data = [0u8; 4]; + test_data.copy_from_slice(&buffer[0x60..0x64]); + raw_decrypt(&mut test_data, &key, 0x60); + + match (&test_riff, &test_data) { + (b"RIFF", b"data") => Some(key), + (_, _) => None, + } +} diff --git a/um_crypto/mg3d/src/lib.rs b/um_crypto/mg3d/src/lib.rs new file mode 100644 index 0000000..12e1508 --- /dev/null +++ b/um_crypto/mg3d/src/lib.rs @@ -0,0 +1,61 @@ +use thiserror::Error; +use umc_utils::md5_2; + +mod guess_m4a; +mod guess_wav; + +pub use guess_m4a::guess_key as guess_m4a_key; +pub use guess_wav::guess_key as guess_wav_key; + +pub fn guess_key(buffer: &[u8]) -> Option<[u8; 0x20]> { + guess_wav_key(buffer).or_else(|| guess_m4a_key(buffer)) +} + +fn raw_decrypt + ?Sized>(buffer: &mut T, key: &[u8; 0x20], offset: usize) { + for (b, i) in buffer.as_mut().iter_mut().zip(offset..) { + *b = (*b).wrapping_sub(key[i % key.len()]); + } +} + +#[derive(Error, Debug)] +pub enum Migu3dError { + #[error("Invalid FileKey")] + InvalidFileKey, + + #[error("Convert hash to key error")] + ConvertKeyError, +} + +fn is_valid_password_chr(chr: u8) -> bool { + matches!(chr, b'0'..=b'9' | b'A'..=b'F') +} + +pub struct Decipher { + key: [u8; 0x20], +} + +impl Decipher { + /// Init decipher from "file_key" (androidFileKey or iosFileKey) + pub fn new_from_file_key(file_key: &str) -> Result { + let hash = md5_2(b"AC89EC47A70B76F307CB39A0D74BCCB0", file_key.as_bytes()); + let key = hex::encode_upper(hash); + let key = key + .as_bytes() + .try_into() + .map_err(|_| Migu3dError::ConvertKeyError)?; + Ok(Self { key }) + } + + /// Init decipher from "key" (the final hash) + pub fn new_from_final_key(key: &[u8; 0x20]) -> Result { + Ok(Self { key: *key }) + } + + pub fn decrypt + ?Sized>(&self, buffer: &mut T, offset: usize) { + raw_decrypt(buffer, &self.key, offset) + } + + pub fn get_key(&self) -> [u8; 0x20] { + self.key + } +} diff --git a/um_crypto/utils/src/lib.rs b/um_crypto/utils/src/lib.rs index 173ad65..0f36d4d 100644 --- a/um_crypto/utils/src/lib.rs +++ b/um_crypto/utils/src/lib.rs @@ -1,3 +1,3 @@ pub mod base64; mod md5; -pub use md5::md5; +pub use md5::{md5, md5_2}; diff --git a/um_crypto/utils/src/md5.rs b/um_crypto/utils/src/md5.rs index 78b39b8..ca38833 100644 --- a/um_crypto/utils/src/md5.rs +++ b/um_crypto/utils/src/md5.rs @@ -3,3 +3,10 @@ use md5::{Digest, Md5}; pub fn md5>(buffer: T) -> [u8; 16] { Md5::digest(buffer).into() } + +pub fn md5_2, T2: AsRef<[u8]>>(buffer1: T1, buffer2: T2) -> [u8; 16] { + let mut md5_digest = Md5::default(); + md5_digest.update(buffer1); + md5_digest.update(buffer2); + md5_digest.finalize().into() +}