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"