Add base62 encoding

This commit is contained in:
Michael Mikovsky
2025-11-10 22:18:21 -07:00
parent 0881e46a17
commit 2b5074153b
21 changed files with 981 additions and 134 deletions
+71
View File
@@ -0,0 +1,71 @@
use crate::{base62::Base62, hash};
use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit};
use cbc::cipher::block_padding::Pkcs7;
use regex::Regex;
fn pkcs7_padded_length(input_len: usize) -> usize {
let block_size = 16;
((input_len / block_size) + 1) * block_size
}
pub fn encrypt_aes(plaintext: &str, key_str: &str, iv: [u8; 16]) -> String {
// Hash the env key to get a 32-byte (256-bit) AES key
let key = hash(key_str.as_bytes());
let plaintext = plaintext.as_bytes();
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);
let ct = cbc::Encryptor::<aes::Aes256>::new(&key.into(), &iv.into())
.encrypt_padded_mut::<Pkcs7>(&mut buf, pt_len)
.unwrap();
Base62::encode_full(ct, &key)
}
pub fn encrypt_aes_lines(plaintext: &str, key_str: &str, iv: [u8; 16]) -> String {
format!("_{}_", encrypt_aes(plaintext, key_str, iv))
}
pub fn decrypt_aes(input: &str, key_str: &str, iv: [u8; 16]) -> String {
// Hash the env key to get a 32-byte (256-bit) AES key
let key = hash(key_str.as_bytes());
let cipher_bytes = Base62::decode_full(input, &key).unwrap();
// 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);
if let Ok(pt) = cbc::Decryptor::<aes::Aes256>::new(&key.into(), &iv.into())
.decrypt_padded_mut::<Pkcs7>(&mut buf)
{
String::from_utf8_lossy(pt).to_string()
} else {
"<decryption failed>".to_string()
}
}
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;
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();
let decrypted_block = decrypt_aes(&aes_block, key_str, iv);
let range = (range.start + total_offset as usize)..(range.end + total_offset as usize);
// 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);
decrypted_result.replace_range(range, &decrypted_block);
}
decrypted_result
}
+225
View File
@@ -0,0 +1,225 @@
use crate::{STATIC_BYTE_MAP, hash};
// Randomly mapped Base62 characters
pub struct Base62 {
charset: [char; 62],
}
pub const BASE62_CHARS: [char; 62] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I',
'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b',
'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u',
'v', 'w', 'x', 'y', 'z',
];
// Const for ratio
const ENCODING_RATIO: f64 = 8.0 / 5.954196310386875; // 8.0 / log2(62.0)
impl Base62 {
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
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_indicies = (0..62).map(|i| i as usize).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];
let index_index = rand as usize % current_indicies.len();
let put_index = current_indicies.remove(index_index);
charset[put_index] = BASE62_CHARS[i];
}
return Self { charset };
}
// Convert character to base-62 value using custom charset
fn char_to_value(&self, ch: char) -> Result<u8, String> {
self.charset
.iter()
.position(|&c| c == ch)
.map(|pos| pos as u8)
.ok_or_else(|| format!("Invalid character for this charset: '{}'", ch))
}
/// Encodes a byte slice into a base-62 string using a custom character set
/// Supports arbitrary length input by using big integer arithmetic
pub fn encode(&self, data: &[u8]) -> String {
if data.is_empty() {
return String::new();
}
// Count leading zeros
let leading_zeros = data.iter().take_while(|&&b| b == 0).count();
// Skip leading zeros for conversion
let data = &data[leading_zeros..];
if data.is_empty() {
return self.charset[0].to_string().repeat(leading_zeros);
}
let mut result = Vec::new();
let mut num = data.to_vec();
// Convert to base-62 using division
while !is_zero(&num) {
let remainder = div_mod_62(&mut num);
result.push(self.charset[remainder]);
}
// Add leading zeros
for _ in 0..leading_zeros {
result.push(self.charset[0]);
}
// Reverse since we built it backwards
result.reverse();
result.into_iter().collect()
}
/// Decodes a base-62 string back into bytes using a custom character set
/// Supports arbitrary length output
pub fn decode(&self, encoded: &str) -> Result<Vec<u8>, String> {
if encoded.is_empty() {
return Ok(Vec::new());
}
// Count leading zeros (first character in charset)
let zero_char = self.charset[0];
let leading_zeros = encoded.chars().take_while(|&c| c == zero_char).count();
// Skip leading zeros for conversion
let encoded = &encoded[leading_zeros..];
if encoded.is_empty() {
return Ok(vec![0; leading_zeros]);
}
// Convert base-62 string to bytes using multiplication
let mut num = vec![0u8];
for ch in encoded.chars() {
let value = self.char_to_value(ch)?;
mul_add(&mut num, 62, value);
}
// Add leading zero bytes
let mut result = vec![0u8; leading_zeros];
result.append(&mut num);
Ok(result)
}
pub fn encode_full(data: &[u8], key: &[u8]) -> String {
// Predict the length of the encoded data
let length = predict_base62_len(data);
let base = Base62::new(&key, length % 255);
let encoded = base.encode(data);
if encoded.len() != length {
let len = encoded.len();
let base = Base62::new(&key, len % 255);
let encoded = base.encode(data);
println!("Fallback");
assert_eq!(encoded.len(), len);
encoded
} else {
encoded
}
}
pub fn decode_full(data: &str, key: &[u8]) -> Result<Vec<u8>, String> {
let base = Base62::new(&key, data.len() % 255);
base.decode(data)
}
// pub fn encode_full
}
// Helper: Check if big integer (as bytes) 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
fn div_mod_62(num: &mut Vec<u8>) -> usize {
let mut remainder = 0u16;
let mut all_zero = true;
for byte in num.iter_mut() {
let current = (remainder << 8) | (*byte as u16);
*byte = (current / 62) as u8;
remainder = current % 62;
if *byte != 0 {
all_zero = false;
}
}
// Remove leading zeros from quotient
if all_zero {
num.clear();
num.push(0);
} else {
let first_nonzero = num.iter().position(|&b| b != 0).unwrap_or(0);
if first_nonzero > 0 {
num.drain(0..first_nonzero);
}
}
remainder as usize
}
// Helper: Multiply big integer by 62 and add a value
// Modifies num in place
fn mul_add(num: &mut Vec<u8>, multiplier: u16, add: u8) {
let mut carry = add as u16;
for byte in num.iter_mut().rev() {
let product = (*byte as u16) * multiplier + carry;
*byte = (product & 0xFF) as u8;
carry = product >> 8;
}
// Add remaining carry bytes
while carry > 0 {
num.insert(0, (carry & 0xFF) as u8);
carry >>= 8;
}
}
/// Predicts the byte length of the decoded output given a base-62 encoded string
/// This calculates the length without performing the full decoding
pub fn predict_base62_len(input_bytes: &[u8]) -> usize {
if input_bytes.is_empty() {
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
}
}
+35
View File
@@ -0,0 +1,35 @@
pub mod aes;
pub mod base62;
pub const ENV_KEY_NAME: &str = "OBFUSCATION_KEY";
pub const BACKUP_ENV_KEY: &str = "OBFUSCATION_KEY_DO_NOT_USE";
pub const STATIC_IV: [u8; 16] = [
0x6d, 0x79, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x69, 0x63, 0x5f, 0x69, 0x76, 0x5f, 0x30, 0x31, 0x32,
];
pub const STATIC_BYTE_MAP: [u8; 256] = [
58, 177, 23, 16, 227, 134, 93, 239, 201, 3, 74, 162, 228, 195, 126, 157, 136, 57, 98, 86, 175,
111, 71, 39, 205, 49, 139, 116, 143, 182, 250, 222, 59, 36, 18, 79, 37, 84, 190, 42, 7, 142,
167, 168, 105, 54, 218, 230, 203, 83, 52, 129, 144, 184, 41, 73, 29, 72, 128, 75, 160, 149, 20,
32, 207, 155, 131, 125, 199, 220, 56, 76, 94, 78, 247, 214, 165, 33, 19, 241, 69, 206, 172,
113, 225, 90, 150, 242, 107, 232, 8, 77, 100, 187, 240, 104, 31, 180, 53, 253, 63, 192, 252,
30, 140, 158, 1, 210, 24, 44, 243, 145, 197, 80, 202, 65, 196, 45, 51, 11, 55, 236, 186, 22,
224, 118, 200, 204, 153, 114, 117, 229, 47, 159, 96, 219, 234, 183, 13, 70, 81, 137, 46, 211,
254, 255, 127, 138, 246, 87, 61, 89, 189, 66, 208, 221, 85, 251, 188, 43, 248, 102, 146, 170,
132, 213, 178, 103, 62, 92, 27, 6, 38, 122, 185, 181, 215, 12, 179, 4, 169, 226, 209, 0, 112,
154, 120, 17, 101, 64, 194, 193, 212, 198, 121, 135, 99, 115, 244, 14, 133, 26, 156, 10, 5,
238, 163, 164, 25, 88, 95, 152, 40, 108, 216, 21, 109, 2, 123, 233, 237, 235, 119, 60, 82, 191,
68, 151, 161, 124, 48, 35, 249, 171, 50, 141, 166, 34, 15, 176, 97, 148, 147, 91, 9, 28, 223,
67, 130, 217, 231, 106, 245, 110, 173, 174,
];
use sha2::{Digest, Sha256};
pub fn hash(input: &[u8]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(input);
hasher.finalize().into()
}
pub use getrandom::fill;