Compare commits
10 Commits
1a282c0912
...
e6fcb07ed2
Author | SHA1 | Date | |
---|---|---|---|
e6fcb07ed2 | |||
a9c7ba9fd4 | |||
c4249226a2 | |||
e92dc08964 | |||
bfa66c6e39 | |||
15bfd296f0 | |||
4b89ad9962 | |||
6c5c82ee1c | |||
0f1233b45a | |||
2c53e4d950 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@ target/
|
||||
pkg/
|
||||
pkg-*/
|
||||
node_modules/
|
||||
*.local
|
||||
|
@ -6,6 +6,8 @@
|
||||
<sourceFolder url="file://$MODULE_DIR$/um_wasm/tests" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/um_crypto/kuwo/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/um_crypto/qmc/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/um_cli/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/um_crypto/utils/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
|
253
Cargo.lock
generated
253
Cargo.lock
generated
@ -2,6 +2,55 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.86"
|
||||
@ -20,6 +69,12 @@ version = "3.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.1.15"
|
||||
@ -35,6 +90,52 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
|
||||
|
||||
[[package]]
|
||||
name = "console_error_panic_hook"
|
||||
version = "0.1.7"
|
||||
@ -51,6 +152,29 @@ version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
@ -69,6 +193,12 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.158"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.22"
|
||||
@ -91,6 +221,15 @@ version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.86"
|
||||
@ -109,6 +248,36 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
@ -130,6 +299,12 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.77"
|
||||
@ -141,6 +316,16 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tc_tea"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cab26285e70ee5cbec8582c76b8124dfe65b564b43dad14b9caca6fd127d66d2"
|
||||
dependencies = [
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.63"
|
||||
@ -161,6 +346,17 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "um_cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"umc_kuwo",
|
||||
"umc_qmc",
|
||||
"umc_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "um_wasm"
|
||||
version = "0.1.0"
|
||||
@ -178,9 +374,9 @@ name = "umc_kuwo"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
"itertools",
|
||||
"thiserror",
|
||||
"umc_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -188,9 +384,18 @@ name = "umc_qmc"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
"byteorder",
|
||||
"itertools",
|
||||
"tc_tea",
|
||||
"thiserror",
|
||||
"umc_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "umc_utils"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -199,6 +404,12 @@ version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
@ -209,6 +420,12 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.93"
|
||||
@ -318,7 +535,16 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -393,3 +619,24 @@ name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
@ -1,6 +1,6 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["um_crypto/*", "um_wasm"]
|
||||
members = ["um_crypto/*", "um_wasm", "um_cli"]
|
||||
|
||||
[profile.release.package.um_wasm]
|
||||
# Tell `rustc` to optimize for small code size.
|
||||
|
11
um_cli/Cargo.toml
Normal file
11
um_cli/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "um_cli"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.86"
|
||||
clap = { version = "4.5.17", features = ["derive"] }
|
||||
umc_kuwo = { path = "../um_crypto/kuwo" }
|
||||
umc_qmc = { path = "../um_crypto/qmc" }
|
||||
umc_utils = { path = "../um_crypto/utils" }
|
12
um_cli/src/cmd/mod.rs
Normal file
12
um_cli/src/cmd/mod.rs
Normal file
@ -0,0 +1,12 @@
|
||||
use clap::Subcommand;
|
||||
|
||||
pub mod qmc1;
|
||||
pub mod qmc2;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
#[command(name = "qmc1")]
|
||||
QMCv1(qmc1::ArgsQMCv1),
|
||||
#[command(name = "qmc2")]
|
||||
QMCv2(qmc2::ArgsQMCv2),
|
||||
}
|
39
um_cli/src/cmd/qmc1.rs
Normal file
39
um_cli/src/cmd/qmc1.rs
Normal file
@ -0,0 +1,39 @@
|
||||
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
|
||||
#[derive(Args)]
|
||||
pub struct ArgsQMCv1 {
|
||||
/// Path to output file, e.g. /export/Music/song.flac
|
||||
#[clap(short, long)]
|
||||
output: PathBuf,
|
||||
|
||||
/// Path to input file, e.g. /export/Music/song.qmcflac
|
||||
#[arg(name = "input")]
|
||||
input: PathBuf,
|
||||
}
|
||||
|
||||
impl ArgsQMCv1 {
|
||||
pub fn run(&self, cli: &Cli) -> Result<i32> {
|
||||
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;
|
||||
}
|
||||
|
||||
Ok(0)
|
||||
}
|
||||
}
|
91
um_cli/src/cmd/qmc2.rs
Normal file
91
um_cli/src/cmd/qmc2.rs
Normal file
@ -0,0 +1,91 @@
|
||||
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_qmc::{footer, QMCv2Cipher};
|
||||
use umc_utils::base64;
|
||||
|
||||
/// Decrypt a QMCv1 file
|
||||
#[derive(Args)]
|
||||
pub struct ArgsQMCv2 {
|
||||
/// Path to output file, e.g. /export/Music/song.flac
|
||||
#[arg(short, long)]
|
||||
output: Option<PathBuf>,
|
||||
|
||||
/// Path to input file, e.g. /export/Music/song.qmcflac
|
||||
#[arg(name = "input")]
|
||||
input: PathBuf,
|
||||
|
||||
/// Override EKey for this file.
|
||||
/// Prefix with "decrypted:" to use base64 encoded raw key.
|
||||
#[arg(short = 'K', long = "ekey")]
|
||||
ekey: Option<String>,
|
||||
|
||||
/// Print info about this file, and do not perform decryption.
|
||||
#[arg(short = 'I', long, action=clap::ArgAction::SetTrue, default_value_t=false)]
|
||||
info_only: bool,
|
||||
}
|
||||
|
||||
impl ArgsQMCv2 {
|
||||
pub fn run(&self, cli: &Cli) -> Result<i32> {
|
||||
let mut file_input = File::open(&self.input)?;
|
||||
let mut footer_detection_buffer = vec![0u8; footer::INITIAL_DETECTION_LEN];
|
||||
file_input.seek(SeekFrom::End(-(footer::INITIAL_DETECTION_LEN as i64)))?;
|
||||
file_input.read_exact(&mut footer_detection_buffer)?;
|
||||
let input_size = file_input.stream_position()?;
|
||||
file_input.seek(SeekFrom::Start(0))?;
|
||||
|
||||
let (footer_len, ekey) = match footer::from_byte_slice(&footer_detection_buffer) {
|
||||
Ok(Some(metadata)) => {
|
||||
if self.info_only || cli.verbose {
|
||||
println!("metadata: {:?}", metadata);
|
||||
}
|
||||
(metadata.size, metadata.ekey.or_else(|| self.ekey.clone()))
|
||||
}
|
||||
Ok(None) => {
|
||||
eprintln!("could not find any qmc metadata.");
|
||||
(0usize, self.ekey.clone())
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("failed to parse qmc metadata: {}", err);
|
||||
(0usize, self.ekey.clone())
|
||||
}
|
||||
};
|
||||
|
||||
if self.info_only {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let key = match ekey {
|
||||
None => bail!("--ekey is required when embedded ekey is not present."),
|
||||
Some(ekey) => match ekey.strip_suffix("decrypted:") {
|
||||
Some(decrypted) => base64::decode(decrypted)?.into_boxed_slice(),
|
||||
None => umc_qmc::ekey::decrypt(ekey)?,
|
||||
},
|
||||
};
|
||||
let cipher = QMCv2Cipher::new(key)?;
|
||||
|
||||
let mut file_output = match &self.output {
|
||||
None => bail!("--output is required"),
|
||||
Some(output) => BufWriter::new(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;
|
||||
}
|
||||
|
||||
Ok(0)
|
||||
}
|
||||
}
|
51
um_cli/src/main.rs
Normal file
51
um_cli/src/main.rs
Normal file
@ -0,0 +1,51 @@
|
||||
use crate::cmd::Commands;
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use std::process::exit;
|
||||
use std::time::Instant;
|
||||
|
||||
mod cmd;
|
||||
|
||||
/// um_cli (rust ver.)
|
||||
/// A cli-tool to unlock encrypted audio files.
|
||||
#[derive(Parser)]
|
||||
#[command(name = "um_cli")]
|
||||
#[command(version = "0.1")]
|
||||
pub struct Cli {
|
||||
#[clap(subcommand)]
|
||||
command: Option<Commands>,
|
||||
|
||||
/// Be more verbose about what is going on.
|
||||
#[clap(long, short, action=clap::ArgAction::SetTrue, default_value_t=false)]
|
||||
verbose: bool,
|
||||
|
||||
/// Preferred buffer size when reading file, in bytes.
|
||||
/// Default to 4MiB.
|
||||
#[clap(long, short = 'B', default_value_t=4*1024*1024)]
|
||||
buffer_size: usize,
|
||||
}
|
||||
|
||||
fn run_command(cli: &Cli) -> Result<i32> {
|
||||
match &cli.command {
|
||||
Some(Commands::QMCv1(cmd)) => cmd.run(&cli),
|
||||
Some(Commands::QMCv2(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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
let start = Instant::now();
|
||||
let code = run_command(&cli).unwrap_or_else(|err| {
|
||||
eprintln!("failed to run command: {}", err);
|
||||
-1
|
||||
});
|
||||
let duration = start.elapsed();
|
||||
if cli.verbose {
|
||||
eprintln!("time: {:?}", duration);
|
||||
};
|
||||
exit(code);
|
||||
}
|
@ -4,7 +4,7 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.22.1"
|
||||
itertools = "0.13.0"
|
||||
anyhow = "1.0.86"
|
||||
itertools = "0.13.0"
|
||||
thiserror = "1.0.63"
|
||||
umc_utils = { path = "../utils" }
|
||||
|
@ -1,22 +1,14 @@
|
||||
use anyhow::Result;
|
||||
use base64::alphabet;
|
||||
use base64::engine::{DecodePaddingMode, GeneralPurpose as Base64Engine, GeneralPurposeConfig};
|
||||
use base64::prelude::*;
|
||||
|
||||
mod constants;
|
||||
mod core;
|
||||
mod helper;
|
||||
use core::{KuwoDes, Mode};
|
||||
|
||||
/// Don't add padding when encoding, and require no padding when decoding.
|
||||
const B64: Base64Engine = Base64Engine::new(
|
||||
&alphabet::STANDARD,
|
||||
GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent),
|
||||
);
|
||||
use umc_utils::base64;
|
||||
|
||||
/// Decrypt string content
|
||||
pub fn decrypt_ksing(data: &str, key: &[u8; 8]) -> Result<String> {
|
||||
let mut decoded = B64.decode(data)?;
|
||||
let mut decoded = base64::decode(data)?;
|
||||
|
||||
let des = KuwoDes::new(key, Mode::Decrypt);
|
||||
des.transform(&mut decoded[..])?;
|
||||
@ -35,7 +27,7 @@ pub fn encrypt_ksing<T: AsRef<[u8]>>(data: T, key: &[u8; 8]) -> Result<String> {
|
||||
|
||||
let des = KuwoDes::new(key, Mode::Encrypt);
|
||||
des.transform(&mut data[..])?;
|
||||
Ok(B64.encode(data))
|
||||
Ok(base64::encode(data))
|
||||
}
|
||||
|
||||
pub fn decode_ekey(data: &str, key: &[u8; 8]) -> Result<String> {
|
||||
|
@ -4,7 +4,9 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.22.1"
|
||||
itertools = "0.13.0"
|
||||
anyhow = "1.0.86"
|
||||
byteorder = "1.5.0"
|
||||
itertools = "0.13.0"
|
||||
tc_tea = "0.1.4"
|
||||
thiserror = "1.0.63"
|
||||
umc_utils = { path = "../utils" }
|
73
um_crypto/qmc/src/ekey.rs
Normal file
73
um_crypto/qmc/src/ekey.rs
Normal file
@ -0,0 +1,73 @@
|
||||
use anyhow::Result;
|
||||
use itertools::Itertools;
|
||||
use std::ops::Mul;
|
||||
use thiserror::Error;
|
||||
use umc_utils::base64;
|
||||
|
||||
/// Base64 encoded prefix: "QQMusic EncV2,Key:"
|
||||
const EKEY_V2_PREFIX: &[u8; 24] = b"UVFNdXNpYyBFbmNWMixLZXk6";
|
||||
const EKEY_V2_KEY1: [u8; 16] = [
|
||||
0x33, 0x38, 0x36, 0x5A, 0x4A, 0x59, 0x21, 0x40, 0x23, 0x2A, 0x24, 0x25, 0x5E, 0x26, 0x29, 0x28,
|
||||
];
|
||||
const EKEY_V2_KEY2: [u8; 16] = [
|
||||
0x2A, 0x2A, 0x23, 0x21, 0x28, 0x23, 0x24, 0x25, 0x26, 0x5E, 0x61, 0x31, 0x63, 0x5A, 0x2C, 0x54,
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Error)]
|
||||
pub enum EKeyDecryptError {
|
||||
#[error("EKey is too short for decryption")]
|
||||
EKeyTooShort,
|
||||
#[error("Error when decrypting ekey v1")]
|
||||
FailDecryptV1,
|
||||
#[error("Error when decrypting ekey v2")]
|
||||
FailDecryptV2,
|
||||
}
|
||||
|
||||
fn make_simple_key<const N: usize>() -> [u8; N] {
|
||||
let mut result = [0u8; N];
|
||||
|
||||
for (i, v) in result.iter_mut().enumerate() {
|
||||
let i = i as f32;
|
||||
let value = 106.0 + i * 0.1;
|
||||
let value = value.tan().abs().mul(100.0);
|
||||
*v = value as u8;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn decrypt_v1(ekey: &[u8]) -> Result<Box<[u8]>> {
|
||||
if ekey.len() < 12 {
|
||||
Err(EKeyDecryptError::EKeyTooShort)?;
|
||||
}
|
||||
|
||||
let ekey = base64::decode(ekey)?;
|
||||
let (header, cipher) = ekey.split_at(8);
|
||||
|
||||
let simple_key = make_simple_key::<8>();
|
||||
let tea_key = simple_key
|
||||
.iter()
|
||||
.zip(header)
|
||||
.flat_map(|(&simple_part, &header_part)| [simple_part, header_part])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let plaintext = tc_tea::decrypt(cipher, tea_key).ok_or(EKeyDecryptError::FailDecryptV1)?;
|
||||
Ok([header, &plaintext].concat().into())
|
||||
}
|
||||
|
||||
pub fn decrypt_v2(ekey: &[u8]) -> Result<Box<[u8]>> {
|
||||
let ekey = base64::decode(ekey)?;
|
||||
let ekey = tc_tea::decrypt(ekey, EKEY_V2_KEY1).ok_or(EKeyDecryptError::FailDecryptV2)?;
|
||||
let ekey = tc_tea::decrypt(ekey, EKEY_V2_KEY2).ok_or(EKeyDecryptError::FailDecryptV2)?;
|
||||
let ekey = ekey.iter().take_while(|&&b| b != 0).copied().collect_vec();
|
||||
|
||||
decrypt_v1(&ekey)
|
||||
}
|
||||
|
||||
pub fn decrypt<T: AsRef<[u8]>>(ekey: T) -> Result<Box<[u8]>> {
|
||||
let ekey = ekey.as_ref();
|
||||
match ekey.strip_prefix(EKEY_V2_PREFIX) {
|
||||
Some(v2_ekey) => decrypt_v2(v2_ekey),
|
||||
None => decrypt_v1(ekey),
|
||||
}
|
||||
}
|
51
um_crypto/qmc/src/footer/android_qtag.rs
Normal file
51
um_crypto/qmc/src/footer/android_qtag.rs
Normal file
@ -0,0 +1,51 @@
|
||||
use crate::footer::utils::is_base64;
|
||||
use crate::footer::{Data, FooterParseError, Metadata, MetadataParser};
|
||||
use byteorder::{ByteOrder, BE};
|
||||
use itertools::Itertools;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct QTagMetadata {
|
||||
/// The old, numeric id of the resource.
|
||||
pub resource_id: u64,
|
||||
}
|
||||
|
||||
impl MetadataParser for QTagMetadata {
|
||||
fn from_byte_slice(buffer: &[u8]) -> anyhow::Result<Option<Metadata>> {
|
||||
if buffer.len() < 8 {
|
||||
Err(FooterParseError::BufferTooSmall(8))?;
|
||||
}
|
||||
|
||||
if let Some(footer) = buffer.strip_suffix(b"QTag") {
|
||||
let (payload, payload_len) = footer.split_at(footer.len() - 4);
|
||||
let actual_payload_len = BE::read_u32(payload_len) as usize;
|
||||
if payload.len() < actual_payload_len {
|
||||
Err(FooterParseError::BufferTooSmall(actual_payload_len + 8))?;
|
||||
}
|
||||
|
||||
// CSV: ekey,resource_id,version
|
||||
let payload = String::from_utf8_lossy(&payload[payload.len() - actual_payload_len..]);
|
||||
if let Some((ekey, resource_id, version)) = payload.split(',').collect_tuple() {
|
||||
if version != "2" {
|
||||
Err(FooterParseError::QTagInvalidVersion(version.to_string()))?;
|
||||
}
|
||||
if !resource_id.as_bytes().iter().all(|&b| b.is_ascii_digit()) {
|
||||
Err(FooterParseError::QTagInvalidId(resource_id.to_string()))?;
|
||||
}
|
||||
if !is_base64(ekey.as_bytes()) {
|
||||
Err(FooterParseError::QTagInvalidEKey(ekey.to_string()))?;
|
||||
}
|
||||
|
||||
return Ok(Some(Metadata {
|
||||
ekey: Some(ekey.into()),
|
||||
size: actual_payload_len + 8,
|
||||
data: Data::AndroidQTag(QTagMetadata {
|
||||
resource_id: resource_id.parse()?,
|
||||
}),
|
||||
}));
|
||||
}
|
||||
|
||||
Err(FooterParseError::STagInvalidCSV(payload.to_string()))?;
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
50
um_crypto/qmc/src/footer/android_stag.rs
Normal file
50
um_crypto/qmc/src/footer/android_stag.rs
Normal file
@ -0,0 +1,50 @@
|
||||
use crate::footer::{Data, FooterParseError, Metadata, MetadataParser};
|
||||
use byteorder::{ByteOrder, BE};
|
||||
use itertools::Itertools;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct STagMetadata {
|
||||
/// Resource identifier (aka. `file.media_mid`).
|
||||
pub media_mid: String,
|
||||
|
||||
/// Resource id (numeric)
|
||||
pub resource_id: u64,
|
||||
}
|
||||
|
||||
impl MetadataParser for STagMetadata {
|
||||
fn from_byte_slice(buffer: &[u8]) -> anyhow::Result<Option<Metadata>> {
|
||||
if buffer.len() < 8 {
|
||||
Err(FooterParseError::BufferTooSmall(8))?;
|
||||
}
|
||||
|
||||
if let Some(footer) = buffer.strip_suffix(b"STag") {
|
||||
let (payload, payload_len) = footer.split_at(footer.len() - 4);
|
||||
let actual_payload_len = BE::read_u32(payload_len) as usize;
|
||||
if payload.len() < actual_payload_len {
|
||||
Err(FooterParseError::BufferTooSmall(actual_payload_len + 8))?;
|
||||
}
|
||||
|
||||
let payload = String::from_utf8_lossy(&payload[payload.len() - actual_payload_len..]);
|
||||
if let Some((id, version, media_mid)) = payload.split(',').collect_tuple() {
|
||||
if version != "2" {
|
||||
Err(FooterParseError::STagInvalidVersion(version.to_string()))?;
|
||||
}
|
||||
if !id.as_bytes().iter().all(|&b| b.is_ascii_digit()) {
|
||||
Err(FooterParseError::STagInvalidId(id.to_string()))?;
|
||||
}
|
||||
|
||||
return Ok(Some(Metadata {
|
||||
ekey: None,
|
||||
size: actual_payload_len + 8,
|
||||
data: Data::AndroidSTag(STagMetadata {
|
||||
resource_id: id.parse()?,
|
||||
media_mid: media_mid.to_string(),
|
||||
}),
|
||||
}));
|
||||
}
|
||||
|
||||
Err(FooterParseError::STagInvalidCSV(payload.to_string()))?;
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
BIN
um_crypto/qmc/src/footer/fixtures/ekey_android_qtag.bin
Normal file
BIN
um_crypto/qmc/src/footer/fixtures/ekey_android_qtag.bin
Normal file
Binary file not shown.
BIN
um_crypto/qmc/src/footer/fixtures/ekey_android_stag.bin
Normal file
BIN
um_crypto/qmc/src/footer/fixtures/ekey_android_stag.bin
Normal file
Binary file not shown.
BIN
um_crypto/qmc/src/footer/fixtures/ekey_pc_enc_v1.bin
Normal file
BIN
um_crypto/qmc/src/footer/fixtures/ekey_pc_enc_v1.bin
Normal file
Binary file not shown.
BIN
um_crypto/qmc/src/footer/fixtures/ekey_pc_enc_v2.bin
Normal file
BIN
um_crypto/qmc/src/footer/fixtures/ekey_pc_enc_v2.bin
Normal file
Binary file not shown.
167
um_crypto/qmc/src/footer/mod.rs
Normal file
167
um_crypto/qmc/src/footer/mod.rs
Normal file
@ -0,0 +1,167 @@
|
||||
pub mod android_qtag;
|
||||
pub mod android_stag;
|
||||
mod musicex_v1;
|
||||
pub mod pc_v1_legacy;
|
||||
pub mod pc_v2_musicex;
|
||||
mod utils;
|
||||
|
||||
use crate::footer::{
|
||||
android_qtag::QTagMetadata, android_stag::STagMetadata, pc_v1_legacy::PcV1Legacy,
|
||||
pc_v2_musicex::PcV2MusicEx,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use thiserror::Error;
|
||||
|
||||
pub const INITIAL_DETECTION_LEN: usize = 1024;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum FooterParseError {
|
||||
#[error("Footer: Buffer too small, require at least {0} bytes")]
|
||||
BufferTooSmall(usize),
|
||||
#[error("PCv1/EKey: Buffer too large, might not be valid EKey (len={0})")]
|
||||
PCv1EKeyTooLarge(usize),
|
||||
#[error("PCv1/EKey: Found invalid EKey char")]
|
||||
PCv1EKeyInvalid,
|
||||
|
||||
#[error("PCv2/MusicEx: Invalid metadata version {0}")]
|
||||
PCv2InvalidVersion(u32),
|
||||
#[error("PCv2/MusicEx: Invalid `MusicEx` size: {0}")]
|
||||
PCv2MusicExUnsupportedPayloadSize(usize),
|
||||
|
||||
#[error("Android/STag: Invalid ID field: {0}")]
|
||||
STagInvalidId(String),
|
||||
#[error("Android/STag: Invalid Version: {0}")]
|
||||
STagInvalidVersion(String),
|
||||
#[error("Android/STag: Invalid CSV metadata: {0}")]
|
||||
STagInvalidCSV(String),
|
||||
|
||||
#[error("Android/QTag: Invalid ID field: {0}")]
|
||||
QTagInvalidId(String),
|
||||
#[error("Android/QTag: Invalid Version: {0}")]
|
||||
QTagInvalidVersion(String),
|
||||
#[error("Android/QTag: Invalid EKey field: {0}")]
|
||||
QTagInvalidEKey(String),
|
||||
}
|
||||
|
||||
/// Footer type
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Data {
|
||||
/// No extra metadata.
|
||||
PCv1Legacy(pc_v1_legacy::PcV1Legacy),
|
||||
/// "MusicEx" footer.
|
||||
PCv2MusicEx(pc_v2_musicex::PcV2MusicEx),
|
||||
|
||||
/// Android "QTag", with ekey.
|
||||
AndroidQTag(android_qtag::QTagMetadata),
|
||||
/// Android "STag", metadata only.
|
||||
AndroidSTag(android_stag::STagMetadata),
|
||||
}
|
||||
|
||||
/// File Footer metadata
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Metadata {
|
||||
/// Footer size to trim off.
|
||||
pub size: usize,
|
||||
|
||||
/// Embedded key (not decrypted).
|
||||
pub ekey: Option<String>,
|
||||
|
||||
/// data/type
|
||||
pub data: Data,
|
||||
}
|
||||
|
||||
pub trait MetadataParser {
|
||||
fn from_byte_slice(buffer: &[u8]) -> Result<Option<Metadata>>;
|
||||
}
|
||||
|
||||
pub fn from_byte_slice(buffer: &[u8]) -> Result<Option<Metadata>> {
|
||||
if let Some(metadata) = STagMetadata::from_byte_slice(buffer)? {
|
||||
return Ok(Some(metadata));
|
||||
}
|
||||
if let Some(metadata) = QTagMetadata::from_byte_slice(buffer)? {
|
||||
return Ok(Some(metadata));
|
||||
}
|
||||
if let Some(metadata) = PcV2MusicEx::from_byte_slice(buffer)? {
|
||||
return Ok(Some(metadata));
|
||||
}
|
||||
if let Some(metadata) = PcV1Legacy::from_byte_slice(buffer)? {
|
||||
return Ok(Some(metadata));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::footer::android_qtag::QTagMetadata;
|
||||
use crate::footer::android_stag::STagMetadata;
|
||||
use crate::footer::pc_v1_legacy::PcV1Legacy;
|
||||
|
||||
#[test]
|
||||
fn test_qtag() {
|
||||
let payload = include_bytes!("fixtures/ekey_android_qtag.bin");
|
||||
let payload = from_byte_slice(payload)
|
||||
.expect("Should not fail")
|
||||
.expect("should parse to qtag");
|
||||
|
||||
assert_eq!(payload.ekey, Some("00112233aBcD+/=".into()));
|
||||
assert_eq!(payload.size, 0x23);
|
||||
assert_eq!(
|
||||
payload.data,
|
||||
Data::AndroidQTag(QTagMetadata {
|
||||
resource_id: 326454301
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stag() {
|
||||
let payload = include_bytes!("fixtures/ekey_android_stag.bin");
|
||||
let payload = from_byte_slice(payload)
|
||||
.expect("Should not fail")
|
||||
.expect("should parse to stag");
|
||||
|
||||
assert_eq!(payload.ekey, None);
|
||||
assert_eq!(payload.size, 0x20);
|
||||
assert_eq!(
|
||||
payload.data,
|
||||
Data::AndroidSTag(STagMetadata {
|
||||
media_mid: "001y7CaR29k6YP".into(),
|
||||
resource_id: 5177785,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pc_enc_v1() {
|
||||
let payload = include_bytes!("fixtures/ekey_pc_enc_v1.bin");
|
||||
let payload = from_byte_slice(payload)
|
||||
.expect("Should not fail")
|
||||
.expect("should parse pc v1");
|
||||
|
||||
let ekey = payload.ekey.expect("ekey should be present");
|
||||
assert!(ekey.starts_with("NUZ6b0la"));
|
||||
|
||||
assert_eq!(payload.size, 0x2C4);
|
||||
assert_eq!(payload.data, Data::PCv1Legacy(PcV1Legacy))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pc_enc_v2() {
|
||||
let payload = include_bytes!("fixtures/ekey_pc_enc_v2.bin");
|
||||
let payload = from_byte_slice(payload)
|
||||
.expect("Should not fail")
|
||||
.expect("should parse pc v2");
|
||||
|
||||
assert_eq!(payload.ekey, None);
|
||||
assert_eq!(payload.size, 0xC0);
|
||||
assert_eq!(
|
||||
payload.data,
|
||||
Data::PCv2MusicEx(PcV2MusicEx {
|
||||
mid: "AaBbCcDdEeFfGg".into(),
|
||||
media_filename: "F0M000112233445566.mflac".into()
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
77
um_crypto/qmc/src/footer/musicex_v1.rs
Normal file
77
um_crypto/qmc/src/footer/musicex_v1.rs
Normal file
@ -0,0 +1,77 @@
|
||||
use std::io::{Cursor, Read};
|
||||
use byteorder::{ByteOrder,ReadBytesExt, LE};
|
||||
use crate::footer::{Data, FooterParseError, Metadata};
|
||||
use crate::footer::pc_v2_musicex::PcV2MusicEx;
|
||||
use crate::footer::utils::from_ascii_utf16;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct MusicExV1 {
|
||||
/// unused & unknown
|
||||
unknown_0: u32,
|
||||
/// unused & unknown
|
||||
unknown_1: u32,
|
||||
/// unused & unknown
|
||||
unknown_2: u32,
|
||||
|
||||
/// Media ID
|
||||
mid: [u8; 30 * 2],
|
||||
/// Media file name
|
||||
media_filename: [u8; 50 * 2],
|
||||
|
||||
/// unused; uninitialized memory?
|
||||
unknown_3: u32,
|
||||
}
|
||||
|
||||
impl Default for MusicExV1 {
|
||||
fn default() -> Self {
|
||||
MusicExV1 {
|
||||
unknown_0: 0,
|
||||
unknown_1: 0,
|
||||
unknown_2: 0,
|
||||
mid: [0; 30 * 2],
|
||||
media_filename: [0; 50 * 2],
|
||||
unknown_3: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MusicExV1 {
|
||||
pub fn from_bytes(buffer: &[u8]) -> anyhow::Result<MusicExV1> {
|
||||
assert_eq!(buffer.len(), 0xC0 - 0x10);
|
||||
|
||||
let mut cursor = Cursor::new(&buffer);
|
||||
let mut result = MusicExV1::default();
|
||||
result.unknown_0 = cursor.read_u32::<LE>()?;
|
||||
result.unknown_1 = cursor.read_u32::<LE>()?;
|
||||
result.unknown_2 = cursor.read_u32::<LE>()?;
|
||||
cursor.read(&mut result.mid)?;
|
||||
cursor.read(&mut result.media_filename)?;
|
||||
result.unknown_3 = cursor.read_u32::<LE>()?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_v1(footer: &[u8]) -> anyhow::Result<Option<Metadata>> {
|
||||
let (payload, payload_len) = footer.split_at(footer.len() - 4);
|
||||
let payload_len = LE::read_u32(&payload_len) as usize;
|
||||
if payload_len != 0xC0 {
|
||||
Err(FooterParseError::PCv2MusicExUnsupportedPayloadSize(
|
||||
payload_len,
|
||||
))?;
|
||||
}
|
||||
|
||||
let payload = &payload[payload.len() - (payload_len - 0x10)..];
|
||||
let payload = MusicExV1::from_bytes(payload)?;
|
||||
let mid = from_ascii_utf16(&payload.mid);
|
||||
let media_filename = from_ascii_utf16(&payload.media_filename);
|
||||
|
||||
Ok(Some(Metadata {
|
||||
ekey: None,
|
||||
size: payload_len,
|
||||
data: Data::PCv2MusicEx(PcV2MusicEx {
|
||||
mid,
|
||||
media_filename,
|
||||
}),
|
||||
}))
|
||||
}
|
44
um_crypto/qmc/src/footer/pc_v1_legacy.rs
Normal file
44
um_crypto/qmc/src/footer/pc_v1_legacy.rs
Normal file
@ -0,0 +1,44 @@
|
||||
use crate::footer::utils::is_base64;
|
||||
use crate::footer::{Data, FooterParseError, Metadata, MetadataParser};
|
||||
use byteorder::{ByteOrder, LE};
|
||||
|
||||
pub const MAX_ALLOWED_EKEY_LEN: usize = 0x500;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct PcV1Legacy;
|
||||
|
||||
impl MetadataParser for PcV1Legacy {
|
||||
fn from_byte_slice(buffer: &[u8]) -> anyhow::Result<Option<Metadata>> {
|
||||
if buffer.len() < 8 {
|
||||
Err(FooterParseError::BufferTooSmall(8))?;
|
||||
}
|
||||
|
||||
let (payload, payload_len) = buffer.split_at(buffer.len() - 4);
|
||||
let payload_len = LE::read_u32(payload_len) as usize;
|
||||
|
||||
// EKey payload is too large, probably not a valid V1 footer.
|
||||
if payload_len > MAX_ALLOWED_EKEY_LEN {
|
||||
Err(FooterParseError::PCv1EKeyTooLarge(payload_len))?;
|
||||
}
|
||||
if payload.len() < payload_len {
|
||||
Err(FooterParseError::BufferTooSmall(payload_len + 4))?;
|
||||
}
|
||||
|
||||
let payload = &payload[payload.len() - payload_len..];
|
||||
let ekey = payload
|
||||
.iter()
|
||||
.take_while(|&&b| b != 0)
|
||||
.map(|&b| b)
|
||||
.collect::<Vec<_>>();
|
||||
let ekey = String::from_utf8_lossy(ekey.as_slice());
|
||||
if !is_base64(ekey.as_bytes()) {
|
||||
Err(FooterParseError::PCv1EKeyInvalid)?;
|
||||
}
|
||||
|
||||
Ok(Some(Metadata {
|
||||
ekey: Some(ekey.into()),
|
||||
size: payload_len + 4,
|
||||
data: Data::PCv1Legacy(PcV1Legacy),
|
||||
}))
|
||||
}
|
||||
}
|
32
um_crypto/qmc/src/footer/pc_v2_musicex.rs
Normal file
32
um_crypto/qmc/src/footer/pc_v2_musicex.rs
Normal file
@ -0,0 +1,32 @@
|
||||
use crate::footer::{musicex_v1, FooterParseError, Metadata, MetadataParser};
|
||||
use anyhow::Result;
|
||||
use byteorder::{ByteOrder, LE};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct PcV2MusicEx {
|
||||
/// Resource identifier (`.mid`)
|
||||
pub mid: String,
|
||||
|
||||
/// The actual file name used for `ekey` lookup (`.file.media_mid` + extension).
|
||||
pub media_filename: String,
|
||||
}
|
||||
|
||||
impl MetadataParser for PcV2MusicEx {
|
||||
fn from_byte_slice(payload: &[u8]) -> Result<Option<Metadata>> {
|
||||
if payload.len() < 16 {
|
||||
Err(FooterParseError::BufferTooSmall(16))?;
|
||||
}
|
||||
|
||||
if let Some(payload) = payload.strip_suffix(b"musicex\x00") {
|
||||
let (payload, version) = payload.split_at(payload.len() - 4);
|
||||
let version = LE::read_u32(version);
|
||||
|
||||
return match version {
|
||||
1 => musicex_v1::parse_v1(payload),
|
||||
_ => Err(FooterParseError::PCv2InvalidVersion(version))?,
|
||||
};
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
17
um_crypto/qmc/src/footer/utils.rs
Normal file
17
um_crypto/qmc/src/footer/utils.rs
Normal file
@ -0,0 +1,17 @@
|
||||
fn is_base64_chr(chr: u8) -> bool {
|
||||
chr.is_ascii_alphanumeric() || (chr == b'+') || (chr == b'/') || (chr == b'=')
|
||||
}
|
||||
|
||||
pub fn is_base64(s: &[u8]) -> bool {
|
||||
s.iter().all(|&c| is_base64_chr(c))
|
||||
}
|
||||
|
||||
/// Convert UTF-16 LE string (within ASCII char range) to UTF-8
|
||||
pub fn from_ascii_utf16(data: &[u8]) -> String {
|
||||
let data = data
|
||||
.chunks_exact(2)
|
||||
.take_while(|chunk| chunk[0] != 0 && chunk[0].is_ascii() && chunk[1] == 0)
|
||||
.map(|chunk| chunk[0])
|
||||
.collect::<Vec<_>>();
|
||||
String::from_utf8_lossy(&data).to_string()
|
||||
}
|
@ -1,5 +1,10 @@
|
||||
use crate::v2_map::QMC2Map;
|
||||
use crate::v2_rc4::cipher::QMC2RC4;
|
||||
use anyhow::Result;
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod ekey;
|
||||
pub mod footer;
|
||||
pub mod v1;
|
||||
pub mod v2_map;
|
||||
pub mod v2_rc4;
|
||||
@ -9,3 +14,47 @@ pub enum QmcCryptoError {
|
||||
#[error("QMC V2/Map Cipher: Key is empty")]
|
||||
QMCV2MapKeyEmpty,
|
||||
}
|
||||
|
||||
pub enum QMCv2Cipher {
|
||||
MapL(QMC2Map),
|
||||
RC4(QMC2RC4),
|
||||
}
|
||||
|
||||
impl QMCv2Cipher {
|
||||
pub fn new<T>(key: T) -> Result<Self>
|
||||
where
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
let key = key.as_ref();
|
||||
let cipher = match key.len() {
|
||||
0 => Err(QmcCryptoError::QMCV2MapKeyEmpty)?,
|
||||
..=300 => QMCv2Cipher::MapL(QMC2Map::new(key)?),
|
||||
_ => QMCv2Cipher::RC4(QMC2RC4::new(key)),
|
||||
};
|
||||
Ok(cipher)
|
||||
}
|
||||
|
||||
pub fn decrypt<T>(&self, data: &mut T, offset: usize)
|
||||
where
|
||||
T: AsMut<[u8]> + ?Sized,
|
||||
{
|
||||
match self {
|
||||
QMCv2Cipher::MapL(cipher) => cipher.decrypt(data, offset),
|
||||
QMCv2Cipher::RC4(cipher) => cipher.decrypt(data, offset),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
pub fn generate_key(len: usize) -> Vec<u8> {
|
||||
(1..=len).map(|i| i as u8).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn generate_key_128() -> [u8; 128] {
|
||||
generate_key(128)
|
||||
.try_into()
|
||||
.expect("failed to make test key")
|
||||
}
|
||||
}
|
||||
|
@ -11,3 +11,30 @@ pub fn qmc1_transform(key: &[u8; V1_KEY_SIZE], value: u8, offset: usize) -> u8 {
|
||||
|
||||
value ^ key[offset % V1_KEY_SIZE]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test::generate_key_128;
|
||||
|
||||
#[test]
|
||||
fn test_decode_start() {
|
||||
let test_key = generate_key_128();
|
||||
let mut data = *b"igohj&pg{fo";
|
||||
for (i, datum) in data.iter_mut().enumerate() {
|
||||
*datum = qmc1_transform(&test_key, *datum, i);
|
||||
}
|
||||
assert_eq!(data, *b"hello world");
|
||||
}
|
||||
#[test]
|
||||
fn test_decode_boundary() {
|
||||
let test_key = generate_key_128();
|
||||
let mut data = [
|
||||
0x13, 0x19, 0x11, 0x12, 0x10, 0xa0, 0x75, 0x6c, 0x76, 0x69, 0x62,
|
||||
];
|
||||
for (i, datum) in data.iter_mut().enumerate() {
|
||||
*datum = qmc1_transform(&test_key, *datum, 0x7FFA + i);
|
||||
}
|
||||
assert_eq!(data, *b"hello world");
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,10 @@ impl QMC2Map {
|
||||
Ok(Self { key })
|
||||
}
|
||||
|
||||
pub fn decrypt<T: AsMut<[u8]>>(&self, data: &mut T, offset: usize) {
|
||||
pub fn decrypt<T>(&self, data: &mut T, offset: usize)
|
||||
where
|
||||
T: AsMut<[u8]> + ?Sized,
|
||||
{
|
||||
for (i, datum) in data.as_mut().iter_mut().enumerate() {
|
||||
*datum = qmc1_transform(&self.key, *datum, offset + i);
|
||||
}
|
||||
|
@ -27,16 +27,16 @@ impl QMC2RC4 {
|
||||
}
|
||||
}
|
||||
|
||||
fn transform_first_segment(&mut self, offset: usize, dst: &mut [u8]) {
|
||||
fn process_first_segment(&self, data: &mut [u8], offset: usize) {
|
||||
let n = self.key.len();
|
||||
|
||||
for (value, offset) in dst.iter_mut().zip(offset..) {
|
||||
for (datum, offset) in data.iter_mut().zip(offset..) {
|
||||
let idx = get_segment_key(offset as u64, self.key[offset % n], self.hash) as usize;
|
||||
*value ^= self.key[idx % n];
|
||||
*datum ^= self.key[idx % n];
|
||||
}
|
||||
}
|
||||
|
||||
fn transform_other_segment(&mut self, offset: usize, data: &mut [u8]) {
|
||||
fn process_other_segment(&self, data: &mut [u8], offset: usize) {
|
||||
let n = self.key.len();
|
||||
|
||||
let id = offset / OTHER_SEGMENT_SIZE;
|
||||
@ -52,14 +52,17 @@ impl QMC2RC4 {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transform(&mut self, start_offset: usize, data: &mut [u8]) {
|
||||
let mut offset = start_offset;
|
||||
let mut buffer = data;
|
||||
pub fn decrypt<T>(&self, data: &mut T, offset: usize)
|
||||
where
|
||||
T: AsMut<[u8]> + ?Sized,
|
||||
{
|
||||
let mut offset = offset;
|
||||
let mut buffer = data.as_mut();
|
||||
if offset < FIRST_SEGMENT_SIZE {
|
||||
let n = min(FIRST_SEGMENT_SIZE - offset, buffer.len());
|
||||
let (block, rest) = buffer.split_at_mut(n);
|
||||
buffer = rest;
|
||||
self.transform_first_segment(offset, block);
|
||||
self.process_first_segment(block, offset);
|
||||
offset += n;
|
||||
}
|
||||
|
||||
@ -69,7 +72,7 @@ impl QMC2RC4 {
|
||||
let n = min(OTHER_SEGMENT_SIZE - excess, buffer.len());
|
||||
let (block, rest) = buffer.split_at_mut(n);
|
||||
buffer = rest;
|
||||
self.transform_other_segment(offset, block);
|
||||
self.process_other_segment(block, offset);
|
||||
offset += n;
|
||||
}
|
||||
};
|
||||
@ -78,7 +81,7 @@ impl QMC2RC4 {
|
||||
let n = min(OTHER_SEGMENT_SIZE, buffer.len());
|
||||
let (block, rest) = buffer.split_at_mut(n);
|
||||
buffer = rest;
|
||||
self.transform_other_segment(offset, block);
|
||||
self.process_other_segment(block, offset);
|
||||
offset += n;
|
||||
}
|
||||
}
|
||||
@ -118,8 +121,8 @@ mod tests {
|
||||
.take(512)
|
||||
.collect::<Vec<u8>>();
|
||||
|
||||
let mut cipher = QMC2RC4::new(&key);
|
||||
cipher.transform(0, &mut data);
|
||||
let cipher = QMC2RC4::new(&key);
|
||||
cipher.decrypt(&mut data, 0);
|
||||
assert_eq!(data, [0u8; 256]);
|
||||
}
|
||||
}
|
||||
|
7
um_crypto/utils/Cargo.toml
Normal file
7
um_crypto/utils/Cargo.toml
Normal file
@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "umc_utils"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.22.1"
|
22
um_crypto/utils/src/base64.rs
Normal file
22
um_crypto/utils/src/base64.rs
Normal file
@ -0,0 +1,22 @@
|
||||
use base64::engine::{DecodePaddingMode, GeneralPurpose as Base64Engine, GeneralPurposeConfig};
|
||||
use base64::{alphabet, DecodeError, Engine};
|
||||
|
||||
/// Don't add padding when encoding, and require no padding when decoding.
|
||||
pub const ENGINE: Base64Engine = Base64Engine::new(
|
||||
&alphabet::STANDARD,
|
||||
GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent),
|
||||
);
|
||||
|
||||
pub fn encode<T>(data: T) -> String
|
||||
where
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
ENGINE.encode(data)
|
||||
}
|
||||
|
||||
pub fn decode<T>(data: T) -> Result<Vec<u8>, DecodeError>
|
||||
where
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
ENGINE.decode(data)
|
||||
}
|
1
um_crypto/utils/src/lib.rs
Normal file
1
um_crypto/utils/src/lib.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod base64;
|
Loading…
Reference in New Issue
Block a user