Improve Rust code clarity across the workspace

Document public APIs and non-obvious control flow so the protocol, simulator, and macro crates are easier to follow. Tighten a few helper paths and feature gates while preserving behavior and keeping the workspace warning-free.
This commit is contained in:
Michael Mikovsky
2026-04-25 11:11:19 -06:00
parent f49af7fa22
commit ba3f28a78c
26 changed files with 571 additions and 402 deletions
+41 -41
View File
@@ -3,71 +3,72 @@ use aes::cipher::{BlockModeDecrypt, BlockModeEncrypt, KeyIvInit};
use cbc::cipher::block_padding::Pkcs7;
use regex::Regex;
/// Returns the next AES block-sized buffer length for PKCS#7 padding.
fn pkcs7_padded_length(input_len: usize) -> usize {
let block_size = 16;
((input_len / block_size) + 1) * block_size
}
fn xor_fold(bytes: &[u8]) -> u8 {
bytes.iter().fold(0, |salt, &byte| salt ^ byte)
}
fn xor_key_with_salt(key: &mut [u8; 32], salt: u8) {
for byte in key.iter_mut() {
*byte ^= salt;
}
}
/// Encrypts `plaintext` with AES-256-CBC and base62-encodes the result.
pub fn encrypt_aes(plaintext: &str, key_str: &str, iv: [u8; 16]) -> String {
let plaintext = plaintext.as_bytes();
// Hash the env key to get a 32-byte (256-bit) AES key
// Hash the environment key into the fixed-width AES-256 key material expected below.
let key = hash(key_str.as_bytes());
// Generate a pseudo-random salt byte based on the plaintext
// I hope this does not break the encryption.
let mut salt = 0;
// This crate intentionally perturbs the key with a single plaintext-derived byte so
// similar inputs diverge quickly after encryption while remaining reversible.
let salt = xor_fold(plaintext);
for byte in plaintext {
salt ^= byte;
}
let mut key_salted = key.clone();
// Salt the key by XORing the salt byte with all the key bytes.
// This ensures that the "hash" generated from the plaintext will
// make the encrypted result extremely different.
for i in 0..32 {
key_salted[i] ^= salt;
}
let mut salted_key = key;
xor_key_with_salt(&mut salted_key, salt);
let buf_len = pkcs7_padded_length(plaintext.len());
let mut buf = vec![0u8; buf_len];
let pt_len = plaintext.len();
buf[..pt_len].copy_from_slice(&plaintext);
buf[..pt_len].copy_from_slice(plaintext);
let mut ct = cbc::Encryptor::<aes::Aes256>::new(&key_salted.into(), &iv.into())
let mut ciphertext = cbc::Encryptor::<aes::Aes256>::new(&salted_key.into(), &iv.into())
.encrypt_padded::<Pkcs7>(&mut buf, pt_len)
.unwrap()
.to_vec();
// Add the salt byte to the key byte,
ct.insert(0, salt);
// Prefix the salt so decryption can rebuild the same salted key.
ciphertext.insert(0, salt);
// Encode result in base62
Base62::encode_full(&ct, &key)
Base62::encode_full(&ciphertext, &key)
}
/// Wraps [`encrypt_aes`] output in `_..._` markers for line-wise replacement.
pub fn encrypt_aes_lines(plaintext: &str, key_str: &str, iv: [u8; 16]) -> String {
format!("_{}_", encrypt_aes(plaintext, key_str, iv))
}
/// Decodes base62 input and decrypts the payload with AES-256-CBC.
pub fn decrypt_aes(input: &str, key_str: &str, iv: [u8; 16]) -> Result<String, String> {
// Hash the env key to get a 32-byte (256-bit) AES key
let mut key = hash(key_str.as_bytes());
let mut cipher_bytes = Base62::decode_full(input, &key).unwrap();
let mut cipher_bytes = Base62::decode_full(input, &key)?;
if cipher_bytes.is_empty() {
return Err("decryption failed".to_string());
}
let salt = cipher_bytes.remove(0);
// XOR the salt bytes with the key bytes
// This replicates
for i in 0..32 {
key[i] ^= salt;
}
xor_key_with_salt(&mut key, salt);
// Create buffer for result
let buf_len = cipher_bytes.len();
let mut buf: Vec<u8> = vec![0; buf_len];
buf[..cipher_bytes.len()].copy_from_slice(&cipher_bytes);
@@ -79,26 +80,25 @@ pub fn decrypt_aes(input: &str, key_str: &str, iv: [u8; 16]) -> Result<String, S
Ok(String::from_utf8_lossy(pt).to_string())
}
/// Replaces every `_..._` AES marker in `input` that decrypts successfully.
pub fn decrypt_aes_lines(input: &str, key_str: &str, iv: [u8; 16]) -> String {
let mut decrypted_result = input.to_string();
let mut total_offset = 0;
let mut replacement_offset: isize = 0;
// Split input by segments of base62 chars, denoted by two _'s, and attempt to decode
for aes_block in Regex::new(r"_([0-9a-zA-Z]*?)_").unwrap().find_iter(&input) {
// Walk the original input and patch into a separate mutable buffer. The offset keeps the
// current match aligned after prior replacements change the string length.
for aes_block in Regex::new(r"_([0-9a-zA-Z]*?)_").unwrap().find_iter(input) {
let range = aes_block.range();
let aes_block = aes_block.as_str()[1..(aes_block.len() - 1)].to_string();
// If the decryption is successful, offset the current offset position
if let Ok(decrypted_block) = decrypt_aes(&aes_block, key_str, iv) {
let range = (range.start + total_offset as usize)..(range.end + total_offset as usize);
let adjusted_start = (range.start as isize + replacement_offset) as usize;
let adjusted_end = (range.end as isize + replacement_offset) as usize;
let adjusted_range = adjusted_start..adjusted_end;
// Offset range by the difference between the decrypted block length and the original range length
total_offset += decrypted_block.len().clone() - (range.end - range.start);
replacement_offset += decrypted_block.len() as isize - adjusted_range.len() as isize;
decrypted_result.replace_range(range, &decrypted_block);
} else {
// If the decode is unsuccessful, leave the underscore-denoted region as is
continue;
decrypted_result.replace_range(adjusted_range, &decrypted_block);
}
}
+32 -46
View File
@@ -1,6 +1,6 @@
use crate::{STATIC_BYTE_MAP, hash};
// Randomly mapped Base62 characters
/// Base-62 encoder/decoder with a deterministic per-key character permutation.
pub struct Base62 {
charset: [char; 62],
}
@@ -12,34 +12,32 @@ pub const BASE62_CHARS: [char; 62] = [
'v', 'w', 'x', 'y', 'z',
];
// Const for ratio
/// `8.0 / log2(62.0)`, used to estimate encoded length from a byte length.
const ENCODING_RATIO: f64 = 8.0 / 5.954196310386875; // 8.0 / log2(62.0)
impl Base62 {
/// Builds the charset permutation for `key` and `nonce`.
pub fn new(key: &[u8], nonce: usize) -> Self {
// Hash key again, for the chance that this random function can be used to derive the key
// My solution to not being good at cryptography lol
// Re-hash the caller-provided key so charset generation always runs on a fixed-width input.
let key = hash(key);
let mut charset: [char; 62] = [0 as char; 62];
// Create a vector of indices from 0 to 61
let mut current_indices = (0..62).map(|i| i as usize).collect::<Vec<usize>>();
let mut available_positions = (0..62).collect::<Vec<usize>>();
// Loop through each byte in the key until all chars are filled
for i in 0..62 as usize {
let rand = STATIC_BYTE_MAP[(key[i as usize % key.len()] as usize + nonce) % 255];
for (char_index, ch) in BASE62_CHARS.iter().copied().enumerate() {
let random_byte = STATIC_BYTE_MAP[(key[char_index % key.len()] as usize + nonce) % 255];
let index_index = rand as usize % current_indices.len();
let put_index = current_indices.remove(index_index);
let choice_index = random_byte as usize % available_positions.len();
let charset_index = available_positions.remove(choice_index);
charset[put_index] = BASE62_CHARS[i];
charset[charset_index] = ch;
}
return Self { charset };
Self { charset }
}
// Convert character to base-62 value using custom charset
/// Converts a character to its base-62 value in this instance's charset.
fn char_to_value(&self, ch: char) -> Result<u8, String> {
self.charset
.iter()
@@ -49,7 +47,7 @@ impl Base62 {
}
/// Encodes a byte slice into a base-62 string using a custom character set
/// Supports arbitrary length input by using big integer arithmetic
/// while preserving leading zero bytes.
pub fn encode(&self, data: &[u8]) -> String {
if data.is_empty() {
return String::new();
@@ -68,7 +66,7 @@ impl Base62 {
let mut result = Vec::new();
let mut num = data.to_vec();
// Convert to base-62 using division
// Repeated division keeps the implementation independent from bigint crates.
while !is_zero(&num) {
let remainder = div_mod_62(&mut num);
result.push(self.charset[remainder]);
@@ -85,7 +83,7 @@ impl Base62 {
}
/// Decodes a base-62 string back into bytes using a custom character set
/// Supports arbitrary length output
/// while preserving leading zero bytes.
pub fn decode(&self, encoded: &str) -> Result<Vec<u8>, String> {
if encoded.is_empty() {
return Ok(Vec::new());
@@ -102,7 +100,7 @@ impl Base62 {
return Ok(vec![0; leading_zeros]);
}
// Convert base-62 string to bytes using multiplication
// Rebuild the big-endian integer via repeated multiply-add.
let mut num = vec![0u8];
for ch in encoded.chars() {
@@ -117,42 +115,41 @@ impl Base62 {
Ok(result)
}
/// Encodes `data` using the nonce convention shared with [`decode_full`].
pub fn encode_full(data: &[u8], key: &[u8]) -> String {
// Predict the length of the encoded data
let length = predict_base62_len(data);
let predicted_len = predict_base62_len(data);
let base = Base62::new(&key, length % 255);
let base = Base62::new(key, predicted_len % 255);
let encoded = base.encode(data);
// For the case that the encoded length is not equal to the predicted length
// The nonce must be derived from this length, so this needs to be ensured
//
// Re-encode with the correct length
if encoded.len() != length {
let len = encoded.len();
let base = Base62::new(&key, len % 255);
// The charset nonce is derived from the final encoded length, so a misprediction must
// trigger one more pass with the actual length-derived nonce.
if encoded.len() != predicted_len {
let actual_len = encoded.len();
let base = Base62::new(key, actual_len % 255);
let encoded = base.encode(data);
assert_eq!(encoded.len(), len);
assert_eq!(encoded.len(), actual_len);
encoded
} else {
encoded
}
}
/// Decodes a string previously produced by [`encode_full`].
pub fn decode_full(data: &str, key: &[u8]) -> Result<Vec<u8>, String> {
let base = Base62::new(&key, data.len() % 255);
let base = Base62::new(key, data.len() % 255);
base.decode(data)
}
}
// Helper: Check if big integer (as bytes) is zero
/// Returns whether the big-endian integer represented by `num` is zero.
fn is_zero(num: &[u8]) -> bool {
num.iter().all(|&b| b == 0)
}
// Helper: Divide big integer by 62 and return remainder
// Modifies num in place to be the quotient
/// Divides an in-place big-endian integer by `62`, returning the remainder.
fn div_mod_62(num: &mut Vec<u8>) -> usize {
let mut remainder = 0u16;
let mut all_zero = true;
@@ -166,7 +163,7 @@ fn div_mod_62(num: &mut Vec<u8>) -> usize {
}
}
// Remove leading zeros from quotient
// Keep a canonical representation so the next loop iteration can stop at `[0]`.
if all_zero {
num.clear();
num.push(0);
@@ -180,8 +177,7 @@ fn div_mod_62(num: &mut Vec<u8>) -> usize {
remainder as usize
}
// Helper: Multiply big integer by 62 and add a value
// Modifies num in place
/// Multiplies an in-place big-endian integer by `multiplier` and adds `add`.
fn mul_add(num: &mut Vec<u8>, multiplier: u16, add: u8) {
let mut carry = add as u16;
@@ -191,7 +187,6 @@ fn mul_add(num: &mut Vec<u8>, multiplier: u16, add: u8) {
carry = product >> 8;
}
// Add remaining carry bytes
while carry > 0 {
num.insert(0, (carry & 0xFF) as u8);
carry >>= 8;
@@ -205,22 +200,13 @@ pub fn predict_base62_len(input_bytes: &[u8]) -> usize {
return 0;
}
// 1. Count leading zero bytes.
let num_leading_zeros = input_bytes.iter().take_while(|&&b| b == 0).count();
// 2. Calculate length of the rest of the bytes.
let num_rest_bytes = input_bytes.len() - num_leading_zeros;
if num_rest_bytes == 0 {
// If all bytes were zeros, the length is just the number of zeros.
num_leading_zeros
} else {
// 3. Calculate the mathematical upper bound for the non-zero part.
// This is ceil(num_rest_bytes * 8_bits / log2(62))
// which is ceil(num_rest_bytes * log_62(256))
let rest_len = (num_rest_bytes as f64 * ENCODING_RATIO).ceil();
// 4. Total length is zeros + rest_len
num_leading_zeros + rest_len as usize
}
}
+8 -1
View File
@@ -1,10 +1,12 @@
//! Base62 encoding helpers plus the AES wrapper used by `ush-obfuscate`.
mod aes;
mod base62;
// Exports
pub use aes::{decrypt_aes, decrypt_aes_lines, encrypt_aes, encrypt_aes_lines};
pub use base62::Base62;
/// Static IV shared by the proc-macro crate and the runtime decoder.
pub const STATIC_IV: [u8; 16] = [
0x6d, 0x79, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x69, 0x63, 0x5f, 0x69, 0x76, 0x5f, 0x30, 0x31, 0x32,
];
@@ -27,12 +29,14 @@ pub const STATIC_BYTE_MAP: [u8; 256] = [
use sha2::{Digest, Sha256};
/// Returns the SHA-256 digest of `input`.
pub fn hash(input: &[u8]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(input);
hasher.finalize().into()
}
/// Encodes a `usize` as a big-endian byte slice without leading zero bytes.
pub fn encode_usize(value: usize) -> Vec<u8> {
if value == 0 {
return vec![0];
@@ -42,6 +46,9 @@ pub fn encode_usize(value: usize) -> Vec<u8> {
bytes[leading_zeros..].to_vec()
}
/// Decodes a big-endian `usize` previously produced by [`encode_usize`].
///
/// The caller must ensure `bytes.len() <= size_of::<usize>()`.
pub fn decode_usize(bytes: &[u8]) -> usize {
let mut buf = [0u8; size_of::<usize>()];
let offset = buf.len() - bytes.len();