aegis_vm 0.2.52

Advanced Rust code virtualization and obfuscation framework
Documentation
//! Runtime string decryption for obfuscated strings
//!
//! This module provides the runtime decryption functionality for strings
//! that were encrypted at compile time by the `#[obfuscate_strings]` macro.
//!
//! ## How It Works
//!
//! 1. Macro encrypts strings at compile time with build-seed-derived keys
//! 2. Encrypted bytes are embedded in the binary
//! 3. This module decrypts them on first access
//! 4. Decrypted strings are cached for subsequent access
//!
//! ## Security
//!
//! - Keys are derived from build_seed (not stored in binary)
//! - Same algorithm as compile-time (FNV-1a based key stream)
//! - Each string has a unique ID for key derivation

use crate::build_config::get_build_seed;

#[cfg(not(feature = "std"))]
use alloc::boxed::Box;
#[cfg(not(feature = "std"))]
use alloc::string::String;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;

/// Decrypt an obfuscated string at runtime
///
/// This function is called by code generated by the `#[obfuscate_strings]` macro.
/// It derives the same key stream used at compile time and XORs to decrypt.
///
/// # Arguments
/// * `encrypted` - The encrypted bytes (embedded in binary)
/// * `string_id` - Unique identifier for key derivation
///
/// # Returns
/// The decrypted string
#[inline(never)] // Prevent inlining to make analysis harder
pub fn decrypt_string(encrypted: &[u8], string_id: u64) -> String {
    let seed = get_build_seed();
    let mut decrypted = Vec::with_capacity(encrypted.len());

    // Generate same key stream as compile time
    for (i, &byte) in encrypted.iter().enumerate() {
        let key_byte = derive_key_byte(&seed, string_id, i as u64);
        decrypted.push(byte ^ key_byte);
    }

    // Convert to string (should be valid UTF-8 if encryption was correct)
    String::from_utf8(decrypted).unwrap_or_else(|_| String::new())
}

/// Decrypt and return as static str (using thread-local or leaked memory)
///
/// This is more efficient for strings that are used repeatedly.
/// The decrypted string is cached and never freed.
///
/// # Safety
/// Uses Box::leak internally - memory is intentionally leaked for 'static lifetime
#[inline(never)]
pub fn decrypt_static(encrypted: &'static [u8], string_id: u64) -> &'static str {
    // For efficiency, we could use a global cache here
    // For now, just decrypt and leak
    let decrypted = decrypt_string(encrypted, string_id);

    // Leak to get 'static lifetime
    // This is intentional - obfuscated strings are typically few and small
    Box::leak(decrypted.into_boxed_str())
}

/// Derive a single key byte for decryption
/// Must match the compile-time implementation exactly!
#[inline(always)]
fn derive_key_byte(seed: &[u8; 32], string_id: u64, position: u64) -> u8 {
    // FNV-1a hash of (seed || string_id || position)
    let mut hash = 0xcbf29ce484222325u64;

    // Mix in seed
    for &byte in seed {
        hash ^= byte as u64;
        hash = hash.wrapping_mul(0x100000001b3);
    }

    // Mix in string_id
    for &byte in &string_id.to_le_bytes() {
        hash ^= byte as u64;
        hash = hash.wrapping_mul(0x100000001b3);
    }

    // Mix in position
    for &byte in &position.to_le_bytes() {
        hash ^= byte as u64;
        hash = hash.wrapping_mul(0x100000001b3);
    }

    // Return single byte
    (hash & 0xFF) as u8
}

/// Cached string decryption with OnceLock (more efficient for repeated access)
///
/// Usage in generated code:
/// ```ignore
/// static CACHED: OnceLock<&'static str> = OnceLock::new();
/// let s = CACHED.get_or_init(|| decrypt_static(&ENCRYPTED, ID));
/// ```
#[cfg(feature = "std")]
pub mod cached {
    #[allow(unused_imports)]
    use super::{decrypt_string, decrypt_static};
    use std::sync::OnceLock;
    use std::collections::HashMap;

    /// Global cache for decrypted strings
    static STRING_CACHE: OnceLock<std::sync::RwLock<HashMap<u64, &'static str>>> = OnceLock::new();

    /// Get or decrypt a string (cached)
    pub fn get_or_decrypt(encrypted: &'static [u8], string_id: u64) -> &'static str {
        let cache = STRING_CACHE.get_or_init(|| std::sync::RwLock::new(HashMap::new()));

        // Try read lock first
        {
            let read = cache.read().unwrap();
            if let Some(&s) = read.get(&string_id) {
                return s;
            }
        }

        // Need to decrypt and cache
        let decrypted = super::decrypt_static(encrypted, string_id);

        // Store in cache
        {
            let mut write = cache.write().unwrap();
            write.insert(string_id, decrypted);
        }

        decrypted
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_decrypt_roundtrip() {
        // This test verifies that the runtime decryption matches compile-time encryption
        // We simulate what the macro does:

        let original = "Hello, World!";
        let string_id = 0x12345678u64;

        // Simulate compile-time encryption
        let seed = get_build_seed();
        let encrypted: Vec<u8> = original
            .bytes()
            .enumerate()
            .map(|(i, b)| b ^ derive_key_byte(&seed, string_id, i as u64))
            .collect();

        // Runtime decryption
        let decrypted = decrypt_string(&encrypted, string_id);

        assert_eq!(original, decrypted);
    }

    #[test]
    fn test_different_ids_different_keys() {
        let seed = get_build_seed();
        let key1 = derive_key_byte(&seed, 1, 0);
        let key2 = derive_key_byte(&seed, 2, 0);

        // Different string IDs should produce different keys
        assert_ne!(key1, key2);
    }

    #[test]
    fn test_different_positions_different_keys() {
        let seed = get_build_seed();
        let key1 = derive_key_byte(&seed, 1, 0);
        let key2 = derive_key_byte(&seed, 1, 1);

        // Different positions should produce different keys
        assert_ne!(key1, key2);
    }
}