Compare commits

...

7 Commits

Author SHA1 Message Date
1dc5f759d7 feat: add qmc2 cli 2024-09-06 00:52:23 +01:00
1f4b01a71c refactor: unify qmc2 decrypt api 2024-09-06 00:52:13 +01:00
2195896fe0 feat: add ekey 2024-09-06 00:51:56 +01:00
b643b53913 refactor: move base64 to shared utils package 2024-09-06 00:51:08 +01:00
fdc9c5d138 chore: improve ignore rules 2024-09-06 00:48:19 +01:00
72a724b9a2 feat: footer parser 2024-09-05 23:37:55 +01:00
ea346f408e chore: run cargo fmt 2024-09-05 23:04:17 +01:00
33 changed files with 1047 additions and 264 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ target/
pkg/
pkg-*/
node_modules/
*.local

View File

@ -1,15 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/um_wasm/src" isTestSource="false" />
<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" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/um_wasm/src" isTestSource="false" />
<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" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/lib_um_crypto.iml" filepath="$PROJECT_DIR$/.idea/lib_um_crypto.iml" />
</modules>
</component>
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/lib_um_crypto.iml" filepath="$PROJECT_DIR$/.idea/lib_um_crypto.iml" />
</modules>
</component>
</project>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

113
Cargo.lock generated
View File

@ -69,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"
@ -146,6 +152,17 @@ 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"
@ -176,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"
@ -198,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"
@ -216,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"
@ -254,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"
@ -282,6 +354,7 @@ dependencies = [
"clap",
"umc_kuwo",
"umc_qmc",
"umc_utils",
]
[[package]]
@ -301,9 +374,9 @@ name = "umc_kuwo"
version = "0.1.0"
dependencies = [
"anyhow",
"base64",
"itertools",
"thiserror",
"umc_utils",
]
[[package]]
@ -311,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]]
@ -338,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"
@ -531,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",
]

View File

@ -8,3 +8,4 @@ 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" }

View File

@ -1,9 +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),
}

View File

@ -1,9 +1,9 @@
use crate::Cli;
use anyhow::Result;
use clap::Args;
use std::fs::File;
use std::io::{Read, Write};
use std::path::PathBuf;
use crate::Cli;
/// Decrypt a QMCv1 file
#[derive(Args)]

91
um_cli/src/cmd/qmc2.rs Normal file
View 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)
}
}

View File

@ -16,18 +16,19 @@ pub struct Cli {
command: Option<Commands>,
/// Be more verbose about what is going on.
#[clap(long, short, action=clap::ArgAction::SetTrue)]
verbose: Option<bool>,
#[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
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");
@ -43,7 +44,7 @@ fn main() {
-1
});
let duration = start.elapsed();
if let Some(true) = cli.verbose {
if cli.verbose {
eprintln!("time: {:?}", duration);
};
exit(code);

View File

@ -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" }

View File

@ -1,108 +1,108 @@
pub const KEY_RND_SHIFTS: [u8; 16] = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1];
pub const KEY_SHIFT_MASKS: [u64; 3] = [0, 0x100001, 0x300003];
pub const KEY_SHIFT_LEFT_MASKS: [u64; 16] = {
let mut result = [0u64; 16];
let mut i = 0usize;
while i < 16 {
result[i] = KEY_SHIFT_MASKS[KEY_RND_SHIFTS[i] as usize];
i += 1;
}
result
};
pub const SBOXES: [[u8; 64]; 8] = [
[
13, 7, 10, 0, 6, 9, 5, 15, 8, 4, 3, 10, 11, 14, 12, 5, 2, 11, 9, 6, 15, 12, 0, 3, 4, 1, 14,
13, 1, 2, 7, 8, 1, 2, 12, 15, 10, 4, 0, 3, 13, 14, 6, 9, 7, 8, 9, 6, 15, 1, 5, 12, 3, 10,
14, 5, 8, 7, 11, 0, 4, 13, 2, 11,
],
[
4, 1, 3, 10, 15, 12, 5, 0, 2, 11, 9, 6, 8, 7, 6, 9, 11, 4, 12, 15, 0, 3, 10, 5, 14, 13, 7,
8, 13, 14, 1, 2, 13, 6, 14, 9, 4, 1, 2, 14, 11, 13, 5, 0, 1, 10, 8, 3, 0, 11, 3, 5, 9, 4,
15, 2, 7, 8, 12, 15, 10, 7, 6, 12,
],
[
12, 9, 0, 7, 9, 2, 14, 1, 10, 15, 3, 4, 6, 12, 5, 11, 1, 14, 13, 0, 2, 8, 7, 13, 15, 5, 4,
10, 8, 3, 11, 6, 10, 4, 6, 11, 7, 9, 0, 6, 4, 2, 13, 1, 9, 15, 3, 8, 15, 3, 1, 14, 12, 5,
11, 0, 2, 12, 14, 7, 5, 10, 8, 13,
],
[
2, 4, 8, 15, 7, 10, 13, 6, 4, 1, 3, 12, 11, 7, 14, 0, 12, 2, 5, 9, 10, 13, 0, 3, 1, 11, 15,
5, 6, 8, 9, 14, 14, 11, 5, 6, 4, 1, 3, 10, 2, 12, 15, 0, 13, 2, 8, 5, 11, 8, 0, 15, 7, 14,
9, 4, 12, 7, 10, 9, 1, 13, 6, 3,
],
[
7, 10, 1, 15, 0, 12, 11, 5, 14, 9, 8, 3, 9, 7, 4, 8, 13, 6, 2, 1, 6, 11, 12, 2, 3, 0, 5,
14, 10, 13, 15, 4, 13, 3, 4, 9, 6, 10, 1, 12, 11, 0, 2, 5, 0, 13, 14, 2, 8, 15, 7, 4, 15,
1, 10, 7, 5, 6, 12, 11, 3, 8, 9, 14,
],
[
10, 13, 1, 11, 6, 8, 11, 5, 9, 4, 12, 2, 15, 3, 2, 14, 0, 6, 13, 1, 3, 15, 4, 10, 14, 9, 7,
12, 5, 0, 8, 7, 13, 1, 2, 4, 3, 6, 12, 11, 0, 13, 5, 14, 6, 8, 15, 2, 7, 10, 8, 15, 4, 9,
11, 5, 9, 0, 14, 3, 10, 7, 1, 12,
],
[
15, 0, 9, 5, 6, 10, 12, 9, 8, 7, 2, 12, 3, 13, 5, 2, 1, 14, 7, 8, 11, 4, 0, 3, 14, 11, 13,
6, 4, 1, 10, 15, 3, 13, 12, 11, 15, 3, 6, 0, 4, 10, 1, 7, 8, 4, 11, 14, 13, 8, 0, 6, 2, 15,
9, 5, 7, 1, 10, 12, 14, 2, 5, 9,
],
[
14, 4, 3, 15, 2, 13, 5, 3, 13, 14, 6, 9, 11, 2, 0, 5, 4, 1, 10, 12, 15, 6, 9, 10, 1, 8, 12,
7, 8, 11, 7, 0, 0, 15, 10, 5, 14, 4, 9, 10, 7, 8, 12, 3, 13, 1, 3, 6, 15, 12, 6, 11, 2, 9,
5, 0, 4, 2, 11, 14, 1, 7, 8, 13,
],
];
// custom
pub const PBOX: [u8; 32] = [
15, 6, 19, 20, 28, 11, 27, 16, 0, 14, 22, 25, 4, 17, 30, 9, 1, 7, 23, 13, 31, 26, 2, 8, 18, 12,
29, 5, 21, 10, 3, 24,
];
// custom
pub const IP: [u8; 64] = [
57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3, 61, 53, 45, 37, 29, 21, 13, 5, 63,
55, 47, 39, 31, 23, 15, 7, 56, 48, 40, 32, 24, 16, 8, 0, 58, 50, 42, 34, 26, 18, 10, 2, 60, 52,
44, 36, 28, 20, 12, 4, 62, 54, 46, 38, 30, 22, 14, 6,
];
// custom
pub const IP_INV: [u8; 64] = [
39, 7, 47, 15, 55, 23, 63, 31, 38, 6, 46, 14, 54, 22, 62, 30, 37, 5, 45, 13, 53, 21, 61, 29,
36, 4, 44, 12, 52, 20, 60, 28, 35, 3, 43, 11, 51, 19, 59, 27, 34, 2, 42, 10, 50, 18, 58, 26,
33, 1, 41, 9, 49, 17, 57, 25, 32, 0, 40, 8, 48, 16, 56, 24,
];
// custom
pub const KEY_PERMUTATION_TABLE: [u8; 56] = [
//key_param_c + key_param_d
56, 48, 40, 32, 24, 16, 8, 0, 57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59,
51, 43, 35, 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 60, 52, 44, 36, 28,
20, 12, 4, 27, 19, 11, 3,
];
// custom
pub const KEY_COMPRESSION: [u8; 64] = [
13, 16, 10, 23, 0, 4, 255, 255, 2, 27, 14, 5, 20, 9, 255, 255, 22, 18, 11, 3, 25, 7, 255, 255,
15, 6, 26, 19, 12, 1, 255, 255, 40, 51, 30, 36, 46, 54, 255, 255, 29, 39, 50, 44, 32, 47, 255,
255, 43, 48, 38, 55, 33, 52, 255, 255, 45, 41, 49, 35, 28, 31, 255, 255,
];
// custom
pub const KEY_EXPANSION: [u8; 64] = [
31, 0, 1, 2, 3, 4, 255, 255, 3, 4, 5, 6, 7, 8, 255, 255, 7, 8, 9, 10, 11, 12, 255, 255, 11, 12,
13, 14, 15, 16, 255, 255, 15, 16, 17, 18, 19, 20, 255, 255, 19, 20, 21, 22, 23, 24, 255, 255,
23, 24, 25, 26, 27, 28, 255, 255, 27, 28, 29, 30, 31, 30, 255, 255,
];
pub const U64_SHIFT_TABLE_CACHE: [u64; 64] = {
let mut data = [0u64; 64];
let mut i = 0;
while i < 32 {
data[i] = 1u64 << i;
data[i + 32] = 1u64 << (i + 32);
i += 1;
}
data
};
pub const KEY_RND_SHIFTS: [u8; 16] = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1];
pub const KEY_SHIFT_MASKS: [u64; 3] = [0, 0x100001, 0x300003];
pub const KEY_SHIFT_LEFT_MASKS: [u64; 16] = {
let mut result = [0u64; 16];
let mut i = 0usize;
while i < 16 {
result[i] = KEY_SHIFT_MASKS[KEY_RND_SHIFTS[i] as usize];
i += 1;
}
result
};
pub const SBOXES: [[u8; 64]; 8] = [
[
13, 7, 10, 0, 6, 9, 5, 15, 8, 4, 3, 10, 11, 14, 12, 5, 2, 11, 9, 6, 15, 12, 0, 3, 4, 1, 14,
13, 1, 2, 7, 8, 1, 2, 12, 15, 10, 4, 0, 3, 13, 14, 6, 9, 7, 8, 9, 6, 15, 1, 5, 12, 3, 10,
14, 5, 8, 7, 11, 0, 4, 13, 2, 11,
],
[
4, 1, 3, 10, 15, 12, 5, 0, 2, 11, 9, 6, 8, 7, 6, 9, 11, 4, 12, 15, 0, 3, 10, 5, 14, 13, 7,
8, 13, 14, 1, 2, 13, 6, 14, 9, 4, 1, 2, 14, 11, 13, 5, 0, 1, 10, 8, 3, 0, 11, 3, 5, 9, 4,
15, 2, 7, 8, 12, 15, 10, 7, 6, 12,
],
[
12, 9, 0, 7, 9, 2, 14, 1, 10, 15, 3, 4, 6, 12, 5, 11, 1, 14, 13, 0, 2, 8, 7, 13, 15, 5, 4,
10, 8, 3, 11, 6, 10, 4, 6, 11, 7, 9, 0, 6, 4, 2, 13, 1, 9, 15, 3, 8, 15, 3, 1, 14, 12, 5,
11, 0, 2, 12, 14, 7, 5, 10, 8, 13,
],
[
2, 4, 8, 15, 7, 10, 13, 6, 4, 1, 3, 12, 11, 7, 14, 0, 12, 2, 5, 9, 10, 13, 0, 3, 1, 11, 15,
5, 6, 8, 9, 14, 14, 11, 5, 6, 4, 1, 3, 10, 2, 12, 15, 0, 13, 2, 8, 5, 11, 8, 0, 15, 7, 14,
9, 4, 12, 7, 10, 9, 1, 13, 6, 3,
],
[
7, 10, 1, 15, 0, 12, 11, 5, 14, 9, 8, 3, 9, 7, 4, 8, 13, 6, 2, 1, 6, 11, 12, 2, 3, 0, 5,
14, 10, 13, 15, 4, 13, 3, 4, 9, 6, 10, 1, 12, 11, 0, 2, 5, 0, 13, 14, 2, 8, 15, 7, 4, 15,
1, 10, 7, 5, 6, 12, 11, 3, 8, 9, 14,
],
[
10, 13, 1, 11, 6, 8, 11, 5, 9, 4, 12, 2, 15, 3, 2, 14, 0, 6, 13, 1, 3, 15, 4, 10, 14, 9, 7,
12, 5, 0, 8, 7, 13, 1, 2, 4, 3, 6, 12, 11, 0, 13, 5, 14, 6, 8, 15, 2, 7, 10, 8, 15, 4, 9,
11, 5, 9, 0, 14, 3, 10, 7, 1, 12,
],
[
15, 0, 9, 5, 6, 10, 12, 9, 8, 7, 2, 12, 3, 13, 5, 2, 1, 14, 7, 8, 11, 4, 0, 3, 14, 11, 13,
6, 4, 1, 10, 15, 3, 13, 12, 11, 15, 3, 6, 0, 4, 10, 1, 7, 8, 4, 11, 14, 13, 8, 0, 6, 2, 15,
9, 5, 7, 1, 10, 12, 14, 2, 5, 9,
],
[
14, 4, 3, 15, 2, 13, 5, 3, 13, 14, 6, 9, 11, 2, 0, 5, 4, 1, 10, 12, 15, 6, 9, 10, 1, 8, 12,
7, 8, 11, 7, 0, 0, 15, 10, 5, 14, 4, 9, 10, 7, 8, 12, 3, 13, 1, 3, 6, 15, 12, 6, 11, 2, 9,
5, 0, 4, 2, 11, 14, 1, 7, 8, 13,
],
];
// custom
pub const PBOX: [u8; 32] = [
15, 6, 19, 20, 28, 11, 27, 16, 0, 14, 22, 25, 4, 17, 30, 9, 1, 7, 23, 13, 31, 26, 2, 8, 18, 12,
29, 5, 21, 10, 3, 24,
];
// custom
pub const IP: [u8; 64] = [
57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3, 61, 53, 45, 37, 29, 21, 13, 5, 63,
55, 47, 39, 31, 23, 15, 7, 56, 48, 40, 32, 24, 16, 8, 0, 58, 50, 42, 34, 26, 18, 10, 2, 60, 52,
44, 36, 28, 20, 12, 4, 62, 54, 46, 38, 30, 22, 14, 6,
];
// custom
pub const IP_INV: [u8; 64] = [
39, 7, 47, 15, 55, 23, 63, 31, 38, 6, 46, 14, 54, 22, 62, 30, 37, 5, 45, 13, 53, 21, 61, 29,
36, 4, 44, 12, 52, 20, 60, 28, 35, 3, 43, 11, 51, 19, 59, 27, 34, 2, 42, 10, 50, 18, 58, 26,
33, 1, 41, 9, 49, 17, 57, 25, 32, 0, 40, 8, 48, 16, 56, 24,
];
// custom
pub const KEY_PERMUTATION_TABLE: [u8; 56] = [
//key_param_c + key_param_d
56, 48, 40, 32, 24, 16, 8, 0, 57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59,
51, 43, 35, 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 60, 52, 44, 36, 28,
20, 12, 4, 27, 19, 11, 3,
];
// custom
pub const KEY_COMPRESSION: [u8; 64] = [
13, 16, 10, 23, 0, 4, 255, 255, 2, 27, 14, 5, 20, 9, 255, 255, 22, 18, 11, 3, 25, 7, 255, 255,
15, 6, 26, 19, 12, 1, 255, 255, 40, 51, 30, 36, 46, 54, 255, 255, 29, 39, 50, 44, 32, 47, 255,
255, 43, 48, 38, 55, 33, 52, 255, 255, 45, 41, 49, 35, 28, 31, 255, 255,
];
// custom
pub const KEY_EXPANSION: [u8; 64] = [
31, 0, 1, 2, 3, 4, 255, 255, 3, 4, 5, 6, 7, 8, 255, 255, 7, 8, 9, 10, 11, 12, 255, 255, 11, 12,
13, 14, 15, 16, 255, 255, 15, 16, 17, 18, 19, 20, 255, 255, 19, 20, 21, 22, 23, 24, 255, 255,
23, 24, 25, 26, 27, 28, 255, 255, 27, 28, 29, 30, 31, 30, 255, 255,
];
pub const U64_SHIFT_TABLE_CACHE: [u64; 64] = {
let mut data = [0u64; 64];
let mut i = 0;
while i < 32 {
data[i] = 1u64 << i;
data[i + 32] = 1u64 << (i + 32);
i += 1;
}
data
};

View File

@ -1,49 +1,49 @@
pub const fn make_u64(hi32: u32, lo32: u32) -> u64 {
((hi32 as u64) << 32) | (lo32 as u64)
}
pub const fn swap_u64_side(value: u64) -> u64 {
(value.wrapping_shr(32)) | (value.wrapping_shl(32))
}
pub const fn u64_get_lo32(value: u64) -> u32 {
value as u32
}
pub const fn u64_get_hi32(value: u64) -> u32 {
value.wrapping_shr(32) as u32
}
pub fn get_u64_by_shift_idx(value: u8) -> u64 {
if value == 255 {
return 0;
}
if cfg!(target_pointer_width = "64") {
1u64.wrapping_shl(value as u32)
} else {
super::constants::U64_SHIFT_TABLE_CACHE
.get(value as usize)
.copied()
.unwrap_or_default()
}
}
#[test]
fn test_get_u64_by_shift_idx() {
assert_eq!(get_u64_by_shift_idx(0), 1);
assert_eq!(get_u64_by_shift_idx(63), 0x8000000000000000);
}
pub fn map_u64(src_value: u64, table: &[u8]) -> u64 {
table.iter().enumerate().fold(0u64, |acc, (i, &idx)| {
match get_u64_by_shift_idx(idx) & src_value {
0 => acc,
_ => acc | get_u64_by_shift_idx(i as u8),
}
})
}
pub fn map_u32(src_value: u32, table: &[u8]) -> u32 {
map_u64(src_value as u64, table) as u32
}
pub const fn make_u64(hi32: u32, lo32: u32) -> u64 {
((hi32 as u64) << 32) | (lo32 as u64)
}
pub const fn swap_u64_side(value: u64) -> u64 {
(value.wrapping_shr(32)) | (value.wrapping_shl(32))
}
pub const fn u64_get_lo32(value: u64) -> u32 {
value as u32
}
pub const fn u64_get_hi32(value: u64) -> u32 {
value.wrapping_shr(32) as u32
}
pub fn get_u64_by_shift_idx(value: u8) -> u64 {
if value == 255 {
return 0;
}
if cfg!(target_pointer_width = "64") {
1u64.wrapping_shl(value as u32)
} else {
super::constants::U64_SHIFT_TABLE_CACHE
.get(value as usize)
.copied()
.unwrap_or_default()
}
}
#[test]
fn test_get_u64_by_shift_idx() {
assert_eq!(get_u64_by_shift_idx(0), 1);
assert_eq!(get_u64_by_shift_idx(63), 0x8000000000000000);
}
pub fn map_u64(src_value: u64, table: &[u8]) -> u64 {
table.iter().enumerate().fold(0u64, |acc, (i, &idx)| {
match get_u64_by_shift_idx(idx) & src_value {
0 => acc,
_ => acc | get_u64_by_shift_idx(i as u8),
}
})
}
pub fn map_u32(src_value: u32, table: &[u8]) -> u32 {
map_u64(src_value as u64, table) as u32
}

View File

@ -1,57 +1,49 @@
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),
);
/// Decrypt string content
pub fn decrypt_ksing(data: &str, key: &[u8; 8]) -> Result<String> {
let mut decoded = B64.decode(data)?;
let des = KuwoDes::new(key, Mode::Decrypt);
des.transform(&mut decoded[..])?;
let result = String::from_utf8_lossy(&decoded[..])
.trim_end_matches('\x00')
.to_string();
Ok(result)
}
pub fn encrypt_ksing<T: AsRef<[u8]>>(data: T, key: &[u8; 8]) -> Result<String> {
let mut data = Vec::from(data.as_ref());
let padded_len = ((data.len() + 7) / 8) * 8;
data.resize(padded_len, 0u8);
let des = KuwoDes::new(key, Mode::Encrypt);
des.transform(&mut data[..])?;
Ok(B64.encode(data))
}
pub fn decode_ekey(data: &str, key: &[u8; 8]) -> Result<String> {
let decoded = decrypt_ksing(data, key)?;
Ok(decoded[16..].to_string())
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_ksing_decode() {
let expected = "hello world";
let decoded =
decrypt_ksing("tx5ct5ilzeLs7pN1C4RI6w==", b"12345678").expect("decrypt failed");
assert_eq!(decoded, expected);
}
}
use anyhow::Result;
mod constants;
mod core;
mod helper;
use core::{KuwoDes, Mode};
use umc_utils::base64;
/// Decrypt string content
pub fn decrypt_ksing(data: &str, key: &[u8; 8]) -> Result<String> {
let mut decoded = base64::decode(data)?;
let des = KuwoDes::new(key, Mode::Decrypt);
des.transform(&mut decoded[..])?;
let result = String::from_utf8_lossy(&decoded[..])
.trim_end_matches('\x00')
.to_string();
Ok(result)
}
pub fn encrypt_ksing<T: AsRef<[u8]>>(data: T, key: &[u8; 8]) -> Result<String> {
let mut data = Vec::from(data.as_ref());
let padded_len = ((data.len() + 7) / 8) * 8;
data.resize(padded_len, 0u8);
let des = KuwoDes::new(key, Mode::Encrypt);
des.transform(&mut data[..])?;
Ok(base64::encode(data))
}
pub fn decode_ekey(data: &str, key: &[u8; 8]) -> Result<String> {
let decoded = decrypt_ksing(data, key)?;
Ok(decoded[16..].to_string())
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_ksing_decode() {
let expected = "hello world";
let decoded =
decrypt_ksing("tx5ct5ilzeLs7pN1C4RI6w==", b"12345678").expect("decrypt failed");
assert_eq!(decoded, expected);
}
}

View File

@ -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
View 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),
}
}

View 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)
}
}

View 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)
}
}

Binary file not shown.

Binary file not shown.

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

View 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,
}),
}))
}

View 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),
}))
}
}

View 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)
}
}

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

View File

@ -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;
@ -10,6 +15,36 @@ pub enum QmcCryptoError {
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> {

View File

@ -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);
}

View File

@ -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]);
}
}

View File

@ -0,0 +1,7 @@
[package]
name = "umc_utils"
version = "0.1.0"
edition = "2021"
[dependencies]
base64 = "0.22.1"

View 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)
}

View File

@ -0,0 +1 @@
pub mod base64;