commit 00c5b318fd545f056c9806e7325a443496b61cb7 Author: Jixun Wu Date: Tue Dec 28 22:33:04 2021 +0000 feat: first version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93c712f --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/target +Cargo.lock + +# IDE +/.idea +/.vscode diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..377bee7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,25 @@ +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[package] +name = "tc_tea" +version = "0.1.0" +authors = ["Jixun Wu "] +edition = "2021" + +license = "MIT OR Apache-2.0" +keywords = ["tea", "tencent"] +readme = "README.md" +description = "Rusty implementation of Tencent's varient of TEA (tc_tea)." +repository = "https://github.com/jixunmoe/tc_tea_rust/" +categories = ["cryptography"] + +[badges] +maintenance = { status = "as-is" } + +[dependencies] +rand = { version = "0.8.0", features = ["rand_chacha"] } +rand_chacha = "0.3.1" + +[features] +default = ["secure_random"] +secure_random = ["rand/getrandom"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d07a79 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# tc_tea + +Rusty implementation of _Tencent modified TEA_ (tc_tea). + +Test data generated using its C++ implementation: [tc_tea.cpp][tc_tea_cpp] (BSD-3-Clause). + +Code implemented according to the spec described in +[iweizime/StepChanger:腾讯 TEA 加密算法][tc_tea_spec]. + +## Features + +* `secure_random` (default: `on`): Enable secure RNG when generating padding bytes for tc_tea. + +## Install + +Add the following to `[dependencies]` section in your `Cargo.toml` file: + +```toml +tc_tea = "0.1.0" +``` + +## Failures + +* Key size needs to be `>= 16` bytes. + * `None` will be returned in this case. + * Only first 16 bytes are used. +* Encrypted size are always multiple of 8. + * `None` will be returned in this case. + +## Usage + +```rust +use tc_tea; + +fn hello_tc_tea() { + let key = "12345678ABCDEFGH"; + let encrypted = tc_tea::encrypt(&"hello", &key).unwrap(); + let decrypted = tc_tea::decrypt(&encrypted, &key).unwrap(); + assert_eq!("hello", std::str::from_utf8(&decrypted).unwrap()); +} +``` + +## License + +Dual licensed under MIT OR Apache-2.0 license. + +`SPDX-License-Identifier: MIT OR Apache-2.0` + +[tc_tea_cpp]: https://github.com/TarsCloud/TarsCpp/blob/a6d5ed8/util/src/tc_tea.cpp +[tc_tea_spec]: https://github.com/iweizime/StepChanger/wiki/%E8%85%BE%E8%AE%AFTEA%E5%8A%A0%E5%AF%86%E7%AE%97%E6%B3%95 diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..0ecf359 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,8 @@ +//! _Tencent modified TEA_ (tc_tea) is a variant of the standard TEA (Tiny Encryption Algorithm). +//! Notably, it uses a different round number and uses a "tweaked" CBC mode. + +mod stream_ext; +mod tc_tea; +mod tc_tea_internal; + +pub use tc_tea::*; diff --git a/src/stream_ext.rs b/src/stream_ext.rs new file mode 100644 index 0000000..28ef856 --- /dev/null +++ b/src/stream_ext.rs @@ -0,0 +1,84 @@ +pub trait StreamExt { + fn read_u32_be(&self, offset: usize) -> u32; + fn write_u32_be(&mut self, offset: usize, value: u32); + fn xor_block(&mut self, dst_offset: usize, size: usize, src: &[u8], src_offset: usize); + fn is_all_zeros(&self) -> bool; + + fn xor_prev_tea_block(&mut self, offset: usize); + fn copy_tea_block(&mut self, offset: usize, src: &[u8], src_offset: usize); + fn xor_tea_block(&mut self, dst_offset: usize, src: &[u8], src_offset: usize); +} + +impl StreamExt for [u8] { + #[inline] + fn read_u32_be(&self, offset: usize) -> u32 { + (u32::from(self[offset]) << 24) + | (u32::from(self[offset + 1]) << 16) + | (u32::from(self[offset + 2]) << 8) + | (u32::from(self[offset + 3])) + } + + #[inline] + fn write_u32_be(&mut self, offset: usize, value: u32) { + self[offset..offset + 4].copy_from_slice(&value.to_be_bytes()); + } + + #[inline] + fn xor_block(&mut self, dst_offset: usize, size: usize, src: &[u8], src_offset: usize) { + for i in 0..size { + self[dst_offset + i] ^= src[src_offset + i]; + } + } + + /// Constant time all zero comparison + /// Attempts to do constant time comparison, + /// but probably gets optimised away by llvm... lol + fn is_all_zeros(&self) -> bool { + let mut sum = 0; + + for b in self { + sum |= b; + } + + return sum == 0; + } + + #[inline] + fn xor_prev_tea_block(&mut self, offset: usize) { + for i in offset..offset + 8 { + self[i] ^= self[i - 8]; + } + } + + #[inline] + fn copy_tea_block(&mut self, offset: usize, src: &[u8], src_offset: usize) { + self[offset..offset + 8] + .as_mut() + .copy_from_slice(&src[src_offset..src_offset + 8]); + } + + #[inline] + fn xor_tea_block(&mut self, dst_offset: usize, src: &[u8], src_offset: usize) { + self.xor_block(dst_offset, 8, src, src_offset); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_read_u32_be() { + let v1 = [1, 2, 3, 4]; + let v2 = [0x7f, 0xff, 0xee, 0xdd, 0xcc]; + assert_eq!(v1.read_u32_be(0), 0x01020304); + assert_eq!(v2.read_u32_be(1), 0xffeeddcc); + } + + #[test] + fn test_write_u32_be() { + let v2 = &mut [0x7fu8, 0xff, 0xee, 0xdd, 0xcc]; + v2.write_u32_be(0, 0x01020304); + assert_eq!(v2, &[1u8, 2, 3, 4, 0xcc]); + } +} diff --git a/src/tc_tea.rs b/src/tc_tea.rs new file mode 100644 index 0000000..e999805 --- /dev/null +++ b/src/tc_tea.rs @@ -0,0 +1,175 @@ +use rand::prelude::*; +use rand_chacha::ChaCha20Rng; + +use super::stream_ext::StreamExt; +use super::tc_tea_internal::{ecb_decrypt, ecb_encrypt, parse_key}; + +const SALT_LEN: usize = 2; +const ZERO_LEN: usize = 7; +const FIXED_PADDING_LEN: usize = 1 + SALT_LEN + ZERO_LEN; + +pub fn calc_encrypted_size(body_size: usize) -> usize { + let len = FIXED_PADDING_LEN + body_size; + let pad_len = (8 - (len & 0b0111)) & 0b0111; + len + pad_len +} + +/// Encrypts an arbitrary length sized data in the following way: +/// * PadLen (1 byte) +/// * Padding (variable, 0-7byte) +/// * Salt (2 bytes) +/// * Body (? bytes) +/// * Zero (7 bytes) +/// PadLen/Padding/Salt is random bytes. Minimum of 3 bytes. +/// PadLen is taken from the last 3 bit of the first byte. +/// +/// # Panics +/// +/// If random number generator fails, it will panic. +pub fn encrypt, K: AsRef<[u8]>>(plaintext: T, key: K) -> Option> { + let plaintext = plaintext.as_ref(); + let key = parse_key(key.as_ref())?; + + // buffer size calculation + let len = FIXED_PADDING_LEN + plaintext.len(); + let pad_len = (8 - (len & 0b0111)) & 0b0111; + let len = len + pad_len; // add our padding + debug_assert_eq!( + len, + calc_encrypted_size(plaintext.len()), + "encrypted size calculation mismatch" + ); + let header_len = 1 + pad_len + SALT_LEN; + + // Setup buffer + let mut encrypted = vec![0u8; len].into_boxed_slice(); + let mut iv1 = vec![0u8; len].into_boxed_slice(); + + // Setup a header with random padding/salt + #[cfg(feature = "secure_random")] + ChaCha20Rng::from_entropy().fill_bytes(&mut encrypted[0..header_len]); + + #[cfg(not(feature = "secure_random"))] + ChaCha20Rng::from_rng(thread_rng()) + .unwrap() + .fill_bytes(&mut encrypted[0..header_len]); + + encrypted[0] = (encrypted[0] & 0b1111_1100) | ((pad_len as u8) & 0b0000_0111); + + // Copy input to destination buffer. + encrypted[header_len..header_len + plaintext.len()] + .as_mut() + .copy_from_slice(plaintext); + + // First block + iv1.copy_tea_block(0, &encrypted, 0); // preserve iv2 for first block + ecb_encrypt(&mut encrypted[0..8], &key); // transform first block + + // Rest of the block + for i in (8..len).step_by(8) { + encrypted.xor_prev_tea_block(i); // XOR iv2 + iv1.copy_tea_block(i, &encrypted, i); // store iv1 + ecb_encrypt(&mut encrypted[i..i + 8], &key); // TEA ECB + encrypted.xor_tea_block(i, &iv1, i - 8); // XOR iv1 (from prev block) + } + + // Done. + Some(encrypted) +} + +/// Decrypts a byte array containing the following: +/// * PadLen (1 byte) +/// * Padding (variable, 0-7byte) +/// * Salt (2 bytes) +/// * Body (? bytes) +/// * Zero (7 bytes) +/// PadLen/Padding/Salt is random bytes. Minimum of 3 bytes. +/// PadLen is taken from the last 3 bit of the first byte. +pub fn decrypt, K: AsRef<[u8]>>(encrypted: T, key: K) -> Option> { + let encrypted = encrypted.as_ref(); + let key = parse_key(key.as_ref())?; + let len = encrypted.len(); + if (len < FIXED_PADDING_LEN) || (len % 8 != 0) { + return None; + } + + let mut decrypted_buf = encrypted.to_vec(); + + // First block + ecb_decrypt(&mut decrypted_buf[0..8], &key); + + // Rest of the block + for i in (8..len).step_by(8) { + decrypted_buf.xor_prev_tea_block(i); // xor iv1 + ecb_decrypt(&mut decrypted_buf[i..i + 8], &key); + } + + // Finalise: XOR iv2 (cipher text) + decrypted_buf.xor_block(8, len - 8, encrypted, 0); + + let pad_size = usize::from(decrypted_buf[0] & 0b111); + + // Prefixed with "pad_size", "padding", "salt" + let start_loc = 1 + pad_size + SALT_LEN; + let end_loc = len - ZERO_LEN; + + if decrypted_buf[end_loc..].is_all_zeros() { + Some( + decrypted_buf[start_loc..end_loc] + .to_vec() + .into_boxed_slice(), + ) + } else { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Known good data, generated from its C++ implementation + const GOOD_ENCRYPTED_DATA: [u8; 24] = [ + 0x91, 0x09, 0x51, 0x62, 0xe3, 0xf5, 0xb6, 0xdc, // + 0x6b, 0x41, 0x4b, 0x50, 0xd1, 0xa5, 0xb8, 0x4e, // + 0xc5, 0x0d, 0x0c, 0x1b, 0x11, 0x96, 0xfd, 0x3c, // + ]; + + const ENCRYPTION_KEY: &'static str = "12345678ABCDEFGH"; + + const GOOD_DECRYPTED_DATA: [u8; 8] = [1u8, 2, 3, 4, 5, 6, 7, 8]; + + #[test] + fn tc_tea_basic_decryption() { + let result = decrypt(GOOD_ENCRYPTED_DATA, ENCRYPTION_KEY).unwrap(); + assert_eq!(result, GOOD_DECRYPTED_DATA.into()); + } + + #[test] + fn tc_tea_decryption_reject_non_zero_byte() { + let mut bad_data = GOOD_ENCRYPTED_DATA.clone(); + bad_data[23] ^= 0xff; // last byte + assert!(decrypt(bad_data, ENCRYPTION_KEY).is_none()); + } + + #[test] + fn tc_tea_basic_encryption() { + let encrypted = encrypt(GOOD_DECRYPTED_DATA, ENCRYPTION_KEY).unwrap(); + assert_eq!(encrypted.len(), 24); + + // Since encryption utilises random numbers, we are just going to + let decrypted = decrypt(encrypted, ENCRYPTION_KEY).unwrap(); + assert_eq!(decrypted, GOOD_DECRYPTED_DATA.into()); + } + + #[test] + fn test_calc_encrypted_size() { + assert_eq!(calc_encrypted_size(0), 16); + assert_eq!(calc_encrypted_size(1), 16); + assert_eq!(calc_encrypted_size(6), 16); + + assert_eq!(calc_encrypted_size(7), 24); + assert_eq!(calc_encrypted_size(14), 24); + assert_eq!(calc_encrypted_size(15), 32); + } +} diff --git a/src/tc_tea_internal.rs b/src/tc_tea_internal.rs new file mode 100644 index 0000000..89a4728 --- /dev/null +++ b/src/tc_tea_internal.rs @@ -0,0 +1,63 @@ +use super::stream_ext::StreamExt; + +const ROUNDS: u32 = 16; +const DELTA: u32 = 0x9e3779b9; + +#[inline] +pub fn parse_key(key: &[u8]) -> Option<[u32; 4]> { + if key.len() < 16 { + return None; + } + + let mut k = [0u32; 4]; + for (i, k) in k.iter_mut().enumerate() { + *k = key.read_u32_be(i * 4); + } + return Some(k); +} + +#[inline] +/// Perform a single round of encrypting/decrypting wrapping arithmetics +fn tc_tea_single_round_arithmetic(value: u32, sum: u32, key1: u32, key2: u32) -> u32 { + // ((y << 4) + k[2]) ^ (y + sum) ^ ((y >> 5) + k[3]); + + value.wrapping_shl(4).wrapping_add(key1) + ^ sum.wrapping_add(value) + ^ value.wrapping_shr(5).wrapping_add(key2) +} + +#[inline] +/// Perform a single operation of TEA ECB decryption. +pub fn ecb_decrypt(block: &mut [u8], k: &[u32; 4]) { + let mut y = block.read_u32_be(0); + let mut z = block.read_u32_be(4); + let mut sum = DELTA.wrapping_mul(ROUNDS); + + for _ in 0..ROUNDS { + z = z.wrapping_sub(tc_tea_single_round_arithmetic(y, sum, k[2], k[3])); + y = y.wrapping_sub(tc_tea_single_round_arithmetic(z, sum, k[0], k[1])); + + sum = sum.wrapping_sub(DELTA); + } + + block.write_u32_be(0, y); + block.write_u32_be(4, z); +} + +#[inline] +/// Perform a single operation of TEA ECB encryption. +pub fn ecb_encrypt(block: &mut [u8], k: &[u32; 4]) { + let mut y = block.read_u32_be(0); + let mut z = block.read_u32_be(4); + let mut sum = 0_u32; + + for _ in 0..ROUNDS { + sum = sum.wrapping_add(DELTA); + + y = y.wrapping_add(tc_tea_single_round_arithmetic(z, sum, k[0], k[1])); + z = z.wrapping_add(tc_tea_single_round_arithmetic(y, sum, k[2], k[3])); + } + + block.write_u32_be(0, y); + block.write_u32_be(4, z); +}