mirror of
https://git.um-react.app/um/lib_um_crypto_rust.git
synced 2026-03-07 20:19:51 +00:00
[mg3d] feat #3: implement migu 3d decipher with improved key guessing
This commit is contained in:
9
um_crypto/mg3d/Cargo.toml
Normal file
9
um_crypto/mg3d/Cargo.toml
Normal file
@@ -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" }
|
||||
29
um_crypto/mg3d/Readme.MD
Normal file
29
um_crypto/mg3d/Readme.MD
Normal file
@@ -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 字节有可能会变,但他们用的编码器好像不会变)
|
||||
73
um_crypto/mg3d/src/guess_m4a.rs
Normal file
73
um_crypto/mg3d/src/guess_m4a.rs
Normal file
@@ -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<u8, usize>;
|
||||
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
|
||||
}
|
||||
}
|
||||
28
um_crypto/mg3d/src/guess_wav.rs
Normal file
28
um_crypto/mg3d/src/guess_wav.rs
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
61
um_crypto/mg3d/src/lib.rs
Normal file
61
um_crypto/mg3d/src/lib.rs
Normal file
@@ -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<T: AsMut<[u8]> + ?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<Self, Migu3dError> {
|
||||
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<Self, Migu3dError> {
|
||||
Ok(Self { key: *key })
|
||||
}
|
||||
|
||||
pub fn decrypt<T: AsMut<[u8]> + ?Sized>(&self, buffer: &mut T, offset: usize) {
|
||||
raw_decrypt(buffer, &self.key, offset)
|
||||
}
|
||||
|
||||
pub fn get_key(&self) -> [u8; 0x20] {
|
||||
self.key
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
pub mod base64;
|
||||
mod md5;
|
||||
pub use md5::md5;
|
||||
pub use md5::{md5, md5_2};
|
||||
|
||||
@@ -3,3 +3,10 @@ use md5::{Digest, Md5};
|
||||
pub fn md5<T: AsRef<[u8]>>(buffer: T) -> [u8; 16] {
|
||||
Md5::digest(buffer).into()
|
||||
}
|
||||
|
||||
pub fn md5_2<T1: AsRef<[u8]>, 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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user