diff --git a/.idea/lib_um_crypto.iml b/.idea/lib_um_crypto.iml
index aeb88fa..beec96a 100644
--- a/.idea/lib_um_crypto.iml
+++ b/.idea/lib_um_crypto.iml
@@ -5,6 +5,7 @@
+
diff --git a/Cargo.lock b/Cargo.lock
index 04b7a04..040b006 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -168,6 +168,7 @@ dependencies = [
"anyhow",
"console_error_panic_hook",
"umc_kuwo",
+ "umc_qmc",
"wasm-bindgen",
"wasm-bindgen-test",
]
@@ -182,6 +183,16 @@ dependencies = [
"thiserror",
]
+[[package]]
+name = "umc_qmc"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "base64",
+ "itertools",
+ "thiserror",
+]
+
[[package]]
name = "unicode-ident"
version = "1.0.12"
diff --git a/Cargo.toml b/Cargo.toml
index f7c94cb..a31b498 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,3 +1,7 @@
[workspace]
resolver = "2"
members = ["um_crypto/*", "um_wasm"]
+
+[profile.release.package.um_wasm]
+# Tell `rustc` to optimize for small code size.
+opt-level = "s"
diff --git a/um_crypto/qmc/Cargo.toml b/um_crypto/qmc/Cargo.toml
new file mode 100644
index 0000000..70caf28
--- /dev/null
+++ b/um_crypto/qmc/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "umc_qmc"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+base64 = "0.22.1"
+itertools = "0.13.0"
+anyhow = "1.0.86"
+thiserror = "1.0.63"
diff --git a/um_crypto/qmc/src/lib.rs b/um_crypto/qmc/src/lib.rs
new file mode 100644
index 0000000..65864b6
--- /dev/null
+++ b/um_crypto/qmc/src/lib.rs
@@ -0,0 +1,7 @@
+use thiserror::Error;
+
+pub mod v1;
+pub mod v2_rc4;
+
+#[derive(Error, Debug)]
+pub enum QmcCryptoError {}
diff --git a/um_crypto/qmc/src/v1/cipher.rs b/um_crypto/qmc/src/v1/cipher.rs
new file mode 100644
index 0000000..363142f
--- /dev/null
+++ b/um_crypto/qmc/src/v1/cipher.rs
@@ -0,0 +1,14 @@
+const V1_OFFSET_BOUNDARY: usize = 0x7FFF;
+
+pub const V1_KEY_SIZE: usize = 128;
+
+#[inline]
+pub fn qmc1_transform(key: &[u8; V1_KEY_SIZE], value: u8, offset: usize) -> u8 {
+ let offset = match offset {
+ 0..V1_OFFSET_BOUNDARY => offset,
+ V1_OFFSET_BOUNDARY => V1_OFFSET_BOUNDARY,
+ offset => offset % V1_OFFSET_BOUNDARY,
+ };
+
+ value ^ key[offset % V1_KEY_SIZE]
+}
diff --git a/um_crypto/qmc/src/v1/mod.rs b/um_crypto/qmc/src/v1/mod.rs
new file mode 100644
index 0000000..60e989a
--- /dev/null
+++ b/um_crypto/qmc/src/v1/mod.rs
@@ -0,0 +1,26 @@
+pub mod cipher;
+use cipher::{qmc1_transform, V1_KEY_SIZE};
+
+const V1_STATIC_KEY: [u8; V1_KEY_SIZE] = [
+ 0xc3, 0x4a, 0xd6, 0xca, 0x90, 0x67, 0xf7, 0x52, 0xd8, 0xa1, 0x66, 0x62, 0x9f, 0x5b, 0x09, 0x00,
+ 0xc3, 0x5e, 0x95, 0x23, 0x9f, 0x13, 0x11, 0x7e, 0xd8, 0x92, 0x3f, 0xbc, 0x90, 0xbb, 0x74, 0x0e,
+ 0xc3, 0x47, 0x74, 0x3d, 0x90, 0xaa, 0x3f, 0x51, 0xd8, 0xf4, 0x11, 0x84, 0x9f, 0xde, 0x95, 0x1d,
+ 0xc3, 0xc6, 0x09, 0xd5, 0x9f, 0xfa, 0x66, 0xf9, 0xd8, 0xf0, 0xf7, 0xa0, 0x90, 0xa1, 0xd6, 0xf3,
+ 0xc3, 0xf3, 0xd6, 0xa1, 0x90, 0xa0, 0xf7, 0xf0, 0xd8, 0xf9, 0x66, 0xfa, 0x9f, 0xd5, 0x09, 0xc6,
+ 0xc3, 0x1d, 0x95, 0xde, 0x9f, 0x84, 0x11, 0xf4, 0xd8, 0x51, 0x3f, 0xaa, 0x90, 0x3d, 0x74, 0x47,
+ 0xc3, 0x0e, 0x74, 0xbb, 0x90, 0xbc, 0x3f, 0x92, 0xd8, 0x7e, 0x11, 0x13, 0x9f, 0x23, 0x95, 0x5e,
+ 0xc3, 0x00, 0x09, 0x5b, 0x9f, 0x62, 0x66, 0xa1, 0xd8, 0x52, 0xf7, 0x67, 0x90, 0xca, 0xd6, 0x4a,
+];
+
+pub fn decrypt(data: &mut [u8], offset: usize) {
+ for (i, datum) in data.iter_mut().enumerate() {
+ *datum = qmc1_transform(&V1_STATIC_KEY, *datum, offset + i);
+ }
+}
+
+#[test]
+fn test_decryption() {
+ let mut data = *b"\xab\x2f\xba\xa6\xff\x47\x80\x3d\xaa\xcd\x02";
+ decrypt(&mut data, 0);
+ assert_eq!(data, *b"hello world");
+}
diff --git a/um_crypto/qmc/src/v2_rc4/cipher.rs b/um_crypto/qmc/src/v2_rc4/cipher.rs
new file mode 100644
index 0000000..43fdd51
--- /dev/null
+++ b/um_crypto/qmc/src/v2_rc4/cipher.rs
@@ -0,0 +1,86 @@
+use crate::v2_rc4::hash::hash;
+use crate::v2_rc4::rc4::RC4;
+use crate::v2_rc4::segment_key::get_segment_key;
+use std::cmp::min;
+
+const FIRST_SEGMENT_SIZE: usize = 0x0080;
+const OTHER_SEGMENT_SIZE: usize = 0x1400;
+const RC4_STREAM_CACHE_SIZE: usize = OTHER_SEGMENT_SIZE + 512;
+
+#[derive(Debug, Clone)]
+pub struct QMC2RC4 {
+ hash: f64,
+ key: Box<[u8]>,
+ key_stream: [u8; RC4_STREAM_CACHE_SIZE],
+}
+
+impl QMC2RC4 {
+ pub fn new(key: &[u8]) -> Self {
+ let mut rc4 = RC4::new(key);
+ let mut key_stream = [0u8; RC4_STREAM_CACHE_SIZE];
+ rc4.derive(&mut key_stream);
+
+ Self {
+ hash: hash(key),
+ key: key.into(),
+ key_stream,
+ }
+ }
+
+ fn transform_first_segment(&mut self, offset: usize, dst: &mut [u8]) {
+ let n = self.key.len();
+
+ for (value, offset) in dst.iter_mut().zip(offset..) {
+ let idx = get_segment_key(offset as u64, self.key[offset % n], self.hash);
+ *value ^= self.key[idx % n];
+ }
+ }
+
+ fn transform_other_segment(&mut self, offset: usize, data: &mut [u8]) {
+ let n = self.key.len();
+
+ let id = offset / OTHER_SEGMENT_SIZE;
+ let block_offset = offset % OTHER_SEGMENT_SIZE;
+
+ let skip = get_segment_key(id as u64, self.key[block_offset % n], self.hash);
+
+ debug_assert!(data.len() <= OTHER_SEGMENT_SIZE - block_offset);
+ let key_stream = self.key_stream.iter().skip(skip + block_offset);
+ for (datum, &key) in data.iter_mut().zip(key_stream) {
+ *datum ^= key;
+ }
+ }
+
+ pub fn transform(&mut self, start_offset: usize, data: &mut [u8]) {
+ let mut offset = start_offset;
+ let mut buffer = data;
+ 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);
+ offset += n;
+ }
+
+ match offset % OTHER_SEGMENT_SIZE {
+ 0 => {} // we are already in the boundary, nothing to do.
+ excess => {
+ 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);
+ offset += n;
+ }
+ };
+
+ while !buffer.is_empty() {
+ let n = min(OTHER_SEGMENT_SIZE, buffer.len());
+ let (block, rest) = buffer.split_at_mut(n);
+ buffer = rest;
+ self.transform_other_segment(offset, block);
+ offset += n;
+ }
+ }
+}
+
+// TODO: Add tests here
diff --git a/um_crypto/qmc/src/v2_rc4/hash.rs b/um_crypto/qmc/src/v2_rc4/hash.rs
new file mode 100644
index 0000000..e7b39e8
--- /dev/null
+++ b/um_crypto/qmc/src/v2_rc4/hash.rs
@@ -0,0 +1,24 @@
+pub fn hash>(key: T) -> f64 {
+ let mut hash = 1u32;
+ for &v in key.as_ref().iter() {
+ if v == 0 {
+ continue;
+ }
+
+ let next_hash = hash.wrapping_mul(v as u32);
+ if next_hash == 0 || next_hash <= hash {
+ break;
+ }
+
+ hash = next_hash;
+ }
+
+ hash.into()
+}
+
+#[test]
+fn test_hash() {
+ let expected = 4045008896.0;
+ let actual = hash(b"hello world");
+ assert_eq!(expected, actual);
+}
diff --git a/um_crypto/qmc/src/v2_rc4/mod.rs b/um_crypto/qmc/src/v2_rc4/mod.rs
new file mode 100644
index 0000000..d40c93e
--- /dev/null
+++ b/um_crypto/qmc/src/v2_rc4/mod.rs
@@ -0,0 +1,4 @@
+pub mod cipher;
+pub mod hash;
+pub mod rc4;
+pub mod segment_key;
diff --git a/um_crypto/qmc/src/v2_rc4/rc4.rs b/um_crypto/qmc/src/v2_rc4/rc4.rs
new file mode 100644
index 0000000..c7724e1
--- /dev/null
+++ b/um_crypto/qmc/src/v2_rc4/rc4.rs
@@ -0,0 +1,62 @@
+use std::ops::Rem;
+
+#[derive(Debug, Clone)]
+pub struct RC4 {
+ state: Box<[u8]>,
+ i: usize,
+ j: usize,
+}
+
+fn init_state(key: &[u8]) -> Box<[u8]> {
+ let n = key.len();
+ let mut state: Box<[u8]> = (0..n).map(|i| i as u8).collect();
+
+ let mut j = 0usize;
+ for i in 0..state.len() {
+ j = (j + usize::from(state[i]) + usize::from(key[i % n])) % n;
+ state.swap(i, j);
+ }
+
+ state
+}
+
+impl RC4 {
+ pub fn new>(key: K) -> Self {
+ Self {
+ state: init_state(key.as_ref()),
+ i: 0,
+ j: 0,
+ }
+ }
+
+ pub fn generate(&mut self) -> u8 {
+ let n = self.state.len();
+ self.i = self.i.wrapping_add(1).rem(n);
+ self.j = self.j.wrapping_add(self.state[self.i].into()).rem(n);
+ self.state.swap(self.i, self.j);
+
+ let i = usize::from(self.state[self.i]);
+ let j = usize::from(self.state[self.j]);
+ let index = (i + j) % n;
+
+ self.state[index]
+ }
+
+ pub fn derive(&mut self, buffer: &mut [u8]) {
+ for item in buffer.iter_mut() {
+ *item ^= self.generate();
+ }
+ }
+}
+
+#[test]
+fn test_rc4() {
+ let mut rc4 = RC4::new(b"this is a test key");
+ let rc4_copy = rc4.clone();
+
+ let mut data = *b"hello world";
+ rc4.derive(&mut data[..]);
+
+ assert_ne!(rc4.state, rc4_copy.state);
+ assert_eq!(&data, b"\x68\x75\x6b\x64\x64\x24\x7f\x60\x7c\x7d\x60")
+}
diff --git a/um_crypto/qmc/src/v2_rc4/segment_key.rs b/um_crypto/qmc/src/v2_rc4/segment_key.rs
new file mode 100644
index 0000000..b4ed76b
--- /dev/null
+++ b/um_crypto/qmc/src/v2_rc4/segment_key.rs
@@ -0,0 +1,24 @@
+pub fn get_segment_key(id: u64, seed: u8, hash: f64) -> usize {
+ match seed {
+ 0 => 0,
+ seed => {
+ let result = hash / ((id + 1).wrapping_mul(seed.into()) as f64) * 100.0;
+ result as usize
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_segment_key_nil_seed() {
+ assert_eq!(get_segment_key(1, 0, 12345.0), 0);
+ }
+
+ #[test]
+ fn test_segment_key_123() {
+ assert_eq!(get_segment_key(1, 123, 12345.0), 5018);
+ }
+}
diff --git a/um_wasm/Cargo.toml b/um_wasm/Cargo.toml
index 300ba65..5c84bf0 100644
--- a/um_wasm/Cargo.toml
+++ b/um_wasm/Cargo.toml
@@ -23,10 +23,7 @@ anyhow = "1.0.86"
# code size when deploying.
console_error_panic_hook = { version = "0.1.7", optional = true }
umc_kuwo = { path = "../um_crypto/kuwo" }
+umc_qmc = { path = "../um_crypto/qmc" }
[dev-dependencies]
wasm-bindgen-test = "0.3.34"
-
-[profile.release]
-# Tell `rustc` to optimize for small code size.
-opt-level = "s"