Compare commits
5 Commits
fd09693656
...
7f03b5ed76
Author | SHA1 | Date | |
---|---|---|---|
7f03b5ed76 | |||
87d1d8152a | |||
0da553c4dd | |||
cf320a5669 | |||
9ea6a73ca4 |
@ -15,6 +15,7 @@
|
|||||||
<sourceFolder url="file://$MODULE_DIR$/um_crypto/xmly/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/um_crypto/xmly/src" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/um_crypto/xiami/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/um_crypto/xiami/src" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/um_crypto/qtfm/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/um_crypto/qtfm/src" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/um_crypto/mg3d/src" isTestSource="false" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/um_wasm_loader/dist" />
|
<excludeFolder url="file://$MODULE_DIR$/um_wasm_loader/dist" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/um_wasm_loader/pkg" />
|
<excludeFolder url="file://$MODULE_DIR$/um_wasm_loader/pkg" />
|
||||||
|
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -535,6 +535,7 @@ dependencies = [
|
|||||||
"umc_joox",
|
"umc_joox",
|
||||||
"umc_kgm",
|
"umc_kgm",
|
||||||
"umc_kuwo",
|
"umc_kuwo",
|
||||||
|
"umc_mg3d",
|
||||||
"umc_ncm",
|
"umc_ncm",
|
||||||
"umc_qmc",
|
"umc_qmc",
|
||||||
"umc_qtfm",
|
"umc_qtfm",
|
||||||
@ -554,8 +555,10 @@ dependencies = [
|
|||||||
"umc_joox",
|
"umc_joox",
|
||||||
"umc_kgm",
|
"umc_kgm",
|
||||||
"umc_kuwo",
|
"umc_kuwo",
|
||||||
|
"umc_mg3d",
|
||||||
"umc_ncm",
|
"umc_ncm",
|
||||||
"umc_qmc",
|
"umc_qmc",
|
||||||
|
"umc_qtfm",
|
||||||
"umc_xiami",
|
"umc_xiami",
|
||||||
"umc_xmly",
|
"umc_xmly",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@ -599,6 +602,15 @@ dependencies = [
|
|||||||
"umc_utils",
|
"umc_utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "umc_mg3d"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"hex",
|
||||||
|
"thiserror",
|
||||||
|
"umc_utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "umc_ncm"
|
name = "umc_ncm"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -10,6 +10,7 @@ hex = "0.4.3"
|
|||||||
umc_joox = { path = "../um_crypto/joox" }
|
umc_joox = { path = "../um_crypto/joox" }
|
||||||
umc_kgm = { path = "../um_crypto/kgm" }
|
umc_kgm = { path = "../um_crypto/kgm" }
|
||||||
umc_kuwo = { path = "../um_crypto/kuwo" }
|
umc_kuwo = { path = "../um_crypto/kuwo" }
|
||||||
|
umc_mg3d = { path = "../um_crypto/mg3d" }
|
||||||
umc_ncm = { path = "../um_crypto/ncm" }
|
umc_ncm = { path = "../um_crypto/ncm" }
|
||||||
umc_qmc = { path = "../um_crypto/qmc" }
|
umc_qmc = { path = "../um_crypto/qmc" }
|
||||||
umc_qtfm = { path = "../um_crypto/qtfm" }
|
umc_qtfm = { path = "../um_crypto/qtfm" }
|
||||||
|
70
um_cli/src/cmd/mg3d.rs
Normal file
70
um_cli/src/cmd/mg3d.rs
Normal file
@ -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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ArgsMigu3D {
|
||||||
|
pub fn run(&self, cli: &Cli) -> Result<i32> {
|
||||||
|
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<T>(&self, reader: &mut T) -> Result<Decipher>
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ use clap::Subcommand;
|
|||||||
|
|
||||||
pub mod joox;
|
pub mod joox;
|
||||||
pub mod kgm;
|
pub mod kgm;
|
||||||
|
pub mod mg3d;
|
||||||
pub mod ncm;
|
pub mod ncm;
|
||||||
pub mod qmc1;
|
pub mod qmc1;
|
||||||
pub mod qmc2;
|
pub mod qmc2;
|
||||||
@ -11,20 +12,29 @@ pub mod xmly;
|
|||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
pub enum Commands {
|
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")]
|
#[command(name = "qmc1")]
|
||||||
QMCv1(qmc1::ArgsQMCv1),
|
QMCv1(qmc1::ArgsQMCv1),
|
||||||
#[command(name = "qmc2")]
|
#[command(name = "qmc2")]
|
||||||
QMCv2(qmc2::ArgsQMCv2),
|
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")]
|
#[command(name = "qtfm")]
|
||||||
QTFM(qtfm::ArgsQingTingFM),
|
QTFM(qtfm::ArgsQingTingFM),
|
||||||
|
|
||||||
|
#[command(name = "xiami")]
|
||||||
|
Xiami(xiami::ArgsXiami),
|
||||||
|
|
||||||
|
#[command(name = "xmly")]
|
||||||
|
XMLY(xmly::ArgsXimalaya),
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,12 @@ impl ArgsQingTingFM {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let device_key = self.make_device_key()?;
|
let device_key = self.make_device_key()?;
|
||||||
let iv = make_decipher_iv(file_name)?;
|
let iv = make_decipher_iv(&file_name)?;
|
||||||
|
if cli.verbose {
|
||||||
|
eprintln!(" file_name: {}", file_name);
|
||||||
|
eprintln!("device_key: {}", hex::encode(&device_key));
|
||||||
|
eprintln!(" file_iv: {}", hex::encode(&iv));
|
||||||
|
}
|
||||||
|
|
||||||
let decipher = Decipher::new(&device_key, &iv);
|
let decipher = Decipher::new(&device_key, &iv);
|
||||||
|
|
||||||
|
@ -27,14 +27,15 @@ pub struct Cli {
|
|||||||
|
|
||||||
fn run_command(cli: &Cli) -> Result<i32> {
|
fn run_command(cli: &Cli) -> Result<i32> {
|
||||||
match &cli.command {
|
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::QMCv1(cmd)) => cmd.run(&cli),
|
||||||
Some(Commands::QMCv2(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::QTFM(cmd)) => cmd.run(&cli),
|
||||||
|
Some(Commands::Xiami(cmd)) => cmd.run(&cli),
|
||||||
|
Some(Commands::XMLY(cmd)) => cmd.run(&cli),
|
||||||
None => {
|
None => {
|
||||||
// https://github.com/clap-rs/clap/issues/3857#issuecomment-1161796261
|
// https://github.com/clap-rs/clap/issues/3857#issuecomment-1161796261
|
||||||
todo!("implement a sensible default command, similar to um/cli");
|
todo!("implement a sensible default command, similar to um/cli");
|
||||||
|
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;
|
pub mod base64;
|
||||||
mod md5;
|
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] {
|
pub fn md5<T: AsRef<[u8]>>(buffer: T) -> [u8; 16] {
|
||||||
Md5::digest(buffer).into()
|
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()
|
||||||
|
}
|
||||||
|
@ -26,8 +26,10 @@ console_error_panic_hook = { version = "0.1.7", optional = true }
|
|||||||
umc_joox = { path = "../um_crypto/joox" }
|
umc_joox = { path = "../um_crypto/joox" }
|
||||||
umc_kgm = { path = "../um_crypto/kgm" }
|
umc_kgm = { path = "../um_crypto/kgm" }
|
||||||
umc_kuwo = { path = "../um_crypto/kuwo" }
|
umc_kuwo = { path = "../um_crypto/kuwo" }
|
||||||
|
umc_mg3d = { path = "../um_crypto/mg3d" }
|
||||||
umc_ncm = { path = "../um_crypto/ncm" }
|
umc_ncm = { path = "../um_crypto/ncm" }
|
||||||
umc_qmc = { path = "../um_crypto/qmc" }
|
umc_qmc = { path = "../um_crypto/qmc" }
|
||||||
|
umc_qtfm = { path = "../um_crypto/qtfm" }
|
||||||
umc_xiami = { path = "../um_crypto/xiami" }
|
umc_xiami = { path = "../um_crypto/xiami" }
|
||||||
umc_xmly = { path = "../um_crypto/xmly" }
|
umc_xmly = { path = "../um_crypto/xmly" }
|
||||||
um_audio = { path = "../um_audio" }
|
um_audio = { path = "../um_audio" }
|
||||||
|
30
um_wasm/src/exports/mg3d.rs
Normal file
30
um_wasm/src/exports/mg3d.rs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
use umc_mg3d::{guess_key, Decipher};
|
||||||
|
use wasm_bindgen::prelude::wasm_bindgen;
|
||||||
|
use wasm_bindgen::JsError;
|
||||||
|
|
||||||
|
/// Migu3D MG3D file decipher.
|
||||||
|
#[wasm_bindgen(js_name=Migu3D)]
|
||||||
|
pub struct JsMigu3D(Decipher);
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_class=Migu3D)]
|
||||||
|
impl JsMigu3D {
|
||||||
|
/// Create a new decipher and guess its key from first 0x100 bytes.
|
||||||
|
#[wasm_bindgen(js_name=fromHeader)]
|
||||||
|
pub fn from_header(header: &[u8]) -> Result<JsMigu3D, JsError> {
|
||||||
|
let key = guess_key(header).ok_or_else(|| JsError::new("failed to guess key"))?;
|
||||||
|
let decipher = Decipher::new_from_final_key(&key)?;
|
||||||
|
Ok(JsMigu3D(decipher))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new decipher from file_key
|
||||||
|
#[wasm_bindgen(js_name=fromFileKey)]
|
||||||
|
pub fn from_file_key(file_key: &str) -> Result<JsMigu3D, JsError> {
|
||||||
|
let decipher = Decipher::new_from_file_key(file_key)?;
|
||||||
|
Ok(JsMigu3D(decipher))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypt encrypted buffer part.
|
||||||
|
pub fn decrypt(&self, buffer: &mut [u8], offset: usize) {
|
||||||
|
self.0.decrypt(buffer, offset)
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,9 @@ pub mod audio;
|
|||||||
pub mod joox;
|
pub mod joox;
|
||||||
pub mod kgm;
|
pub mod kgm;
|
||||||
pub mod kuwo;
|
pub mod kuwo;
|
||||||
|
pub mod mg3d;
|
||||||
pub mod ncm;
|
pub mod ncm;
|
||||||
pub mod qmc;
|
pub mod qmc;
|
||||||
mod xiami;
|
pub mod qtfm;
|
||||||
|
pub mod xiami;
|
||||||
pub mod xmly;
|
pub mod xmly;
|
||||||
|
40
um_wasm/src/exports/qtfm.rs
Normal file
40
um_wasm/src/exports/qtfm.rs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
use std::convert::TryInto;
|
||||||
|
use umc_qtfm::{nonce, secret, Decipher};
|
||||||
|
use wasm_bindgen::prelude::wasm_bindgen;
|
||||||
|
use wasm_bindgen::JsError;
|
||||||
|
|
||||||
|
/// QingTingFM QTA file decipher.
|
||||||
|
#[wasm_bindgen(js_name=QingTingFM)]
|
||||||
|
pub struct JsQingTingFM(Decipher);
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = QingTingFM)]
|
||||||
|
impl JsQingTingFM {
|
||||||
|
#[wasm_bindgen(js_name=getDeviceKey)]
|
||||||
|
pub fn get_device_key(
|
||||||
|
product: &str,
|
||||||
|
device: &str,
|
||||||
|
manufacturer: &str,
|
||||||
|
brand: &str,
|
||||||
|
board: &str,
|
||||||
|
model: &str,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
secret::make_device_secret(product, device, manufacturer, brand, board, model).to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name=getFileIV)]
|
||||||
|
pub fn get_file_iv(file_name: &str) -> Result<Vec<u8>, JsError> {
|
||||||
|
let iv = nonce::make_decipher_iv(file_name)?;
|
||||||
|
Ok(iv.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new(device_key: &[u8], file_iv: &[u8]) -> Result<JsQingTingFM, JsError> {
|
||||||
|
let decipher = Decipher::new(device_key.try_into()?, file_iv.try_into()?);
|
||||||
|
Ok(JsQingTingFM(decipher))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypt encrypted buffer part.
|
||||||
|
pub fn decrypt(&self, buffer: &mut [u8], offset: usize) {
|
||||||
|
self.0.decrypt(buffer, offset)
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,13 @@
|
|||||||
mod errors;
|
|
||||||
pub mod exports;
|
|
||||||
mod utils;
|
|
||||||
|
|
||||||
use utils::set_panic_hook;
|
use utils::set_panic_hook;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
mod errors;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
pub mod exports;
|
||||||
|
|
||||||
/// Init panic hook
|
/// Init panic hook
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen(js_name=initPanicHook)]
|
||||||
pub fn init() {
|
pub fn init_panic_hook() {
|
||||||
set_panic_hook();
|
set_panic_hook();
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@unlock-music/crypto",
|
"name": "@unlock-music/crypto",
|
||||||
"version": "0.0.0-alpha.15",
|
"version": "0.0.0-alpha.16",
|
||||||
"description": "Project Unlock Music: 加解密支持库",
|
"description": "Project Unlock Music: 加解密支持库",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node build.js",
|
"build": "node build.js",
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import umWasm from '../pkg/um_wasm_bg.wasm';
|
import umWasm from '../pkg/um_wasm_bg.wasm';
|
||||||
import { __wbg_init, initSync } from '../pkg/um_wasm.js';
|
import { __wbg_init, initPanicHook, initSync } from '../pkg/um_wasm.js';
|
||||||
|
|
||||||
export * from '../pkg/um_wasm.js';
|
export * from '../pkg/um_wasm.js';
|
||||||
|
|
||||||
function loader() {
|
function loader() {
|
||||||
if (process.env.UMC_INLINE_BUILD === '1') {
|
if (process.env.UMC_INLINE_BUILD === '1') {
|
||||||
initSync({ module: umWasm() });
|
initSync({ module: umWasm() });
|
||||||
|
initPanicHook();
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
} else {
|
} else {
|
||||||
const url = new URL('um_wasm_bg.wasm', import.meta.url);
|
const url = new URL('um_wasm_bg.wasm', import.meta.url);
|
||||||
@ -17,7 +18,7 @@ function loader() {
|
|||||||
console.log('read wasm failed', err);
|
console.log('read wasm failed', err);
|
||||||
})
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
return __wbg_init({ module_or_path: wasm }).then(() => true);
|
return __wbg_init({ module_or_path: wasm }).then(() => (initPanicHook(), true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user