mirror of
https://github.com/jixunmoe/tc_tea_rust
synced 2026-03-07 20:19:49 +00:00
feat: first version
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/target
|
||||||
|
Cargo.lock
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
/.idea
|
||||||
|
/.vscode
|
||||||
25
Cargo.toml
Normal file
25
Cargo.toml
Normal file
@@ -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 <jixun.moe@gmail.com>"]
|
||||||
|
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"]
|
||||||
50
README.md
Normal file
50
README.md
Normal file
@@ -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
|
||||||
8
src/lib.rs
Normal file
8
src/lib.rs
Normal file
@@ -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::*;
|
||||||
84
src/stream_ext.rs
Normal file
84
src/stream_ext.rs
Normal file
@@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
175
src/tc_tea.rs
Normal file
175
src/tc_tea.rs
Normal file
@@ -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<T: AsRef<[u8]>, K: AsRef<[u8]>>(plaintext: T, key: K) -> Option<Box<[u8]>> {
|
||||||
|
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<T: AsRef<[u8]>, K: AsRef<[u8]>>(encrypted: T, key: K) -> Option<Box<[u8]>> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/tc_tea_internal.rs
Normal file
63
src/tc_tea_internal.rs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user