Compare commits
7 Commits
4aacbd7b56
...
1dc5f759d7
Author | SHA1 | Date | |
---|---|---|---|
1dc5f759d7 | |||
1f4b01a71c | |||
2195896fe0 | |||
b643b53913 | |||
fdc9c5d138 | |||
72a724b9a2 | |||
ea346f408e |
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@ target/
|
||||
pkg/
|
||||
pkg-*/
|
||||
node_modules/
|
||||
*.local
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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
113
Cargo.lock
generated
@ -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",
|
||||
]
|
||||
|
@ -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" }
|
||||
|
@ -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),
|
||||
}
|
||||
|
@ -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
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)
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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,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
|
||||
};
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
@ -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> {
|
||||
|
@ -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