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