obfuse 0.1.4

Compile-time string obfuscation with runtime decryption and secure memory wiping
Documentation

obfuse-rs

Compile-time string encryption for Rust with runtime decryption and secure memory wiping.

Security Notice: This library provides string obfuscation, not military-grade encryption. The encryption key is embedded in the binary alongside the ciphertext. A determined attacker with access to your binary can extract both.

Appropriate uses:

  • Preventing casual inspection of binaries (strings command, hex editors)
  • Stopping automated string extraction tools
  • Basic protection against unsophisticated reverse engineering

NOT appropriate for:

  • Protecting highly sensitive secrets (use proper secrets management)
  • Compliance requirements (PCI-DSS, HIPAA, SOC2, etc.)
  • Scenarios where key extraction would be catastrophic

Features

  • Compile-time encryption: Strings are encrypted during compilation, never stored in plaintext in binaries
  • Multiple encryption algorithms: Choose via Cargo features
    • aes-256-gcm (default) - AES-256 in GCM mode
    • aes-128-gcm - AES-128 in GCM mode
    • chacha20-poly1305 - ChaCha20-Poly1305 AEAD
    • xor - Simple XOR (fast, less secure, good for obfuscation)
  • Secure memory handling: Volatile zeroing of sensitive data on drop
  • Zero-copy decryption: Decrypt only when accessed
  • No runtime dependencies: Encryption happens at compile time

Installation

Add to your Cargo.toml:

[dependencies]
obfuse = "0.1"

Selecting Encryption Algorithm

By default, aes-256-gcm is used. To use a different algorithm:

# Use AES-128
[dependencies]
obfuse = { version = "0.1", default-features = false, features = ["aes-128-gcm"] }

# Use ChaCha20-Poly1305
[dependencies]
obfuse = { version = "0.1", default-features = false, features = ["chacha20-poly1305"] }

# Use XOR (fast obfuscation, not cryptographically secure)
[dependencies]
obfuse = { version = "0.1", default-features = false, features = ["xor"] }

Usage

Basic Usage

use obfuse::obfuse;

fn main() {
    // String is encrypted at compile time
    let secret = obfuse!("my secret API key");

    // Decrypted only when accessed
    println!("Secret: {}", secret.as_str());

    // Memory is securely wiped when `secret` goes out of scope
}

With Explicit Type

use obfuse::{obfuse, ObfuseStr};

fn main() {
    let secret: ObfuseStr = obfuse!("database password");

    // Use the decrypted string
    connect_to_database(secret.as_str());

    // `secret` is automatically zeroed on drop
}

Lazy Decryption

use obfuse::obfuse;

fn main() {
    let secret = obfuse!("sensitive data");

    // String remains encrypted until first access
    if should_use_secret() {
        // Decryption happens here
        use_secret(secret.as_str());
    }
    // If condition is false, string is never decrypted
}

Error Handling

For defensive programming, use the fallible API:

use obfuse::{obfuse, ObfuseStrError};

fn main() {
    let secret = obfuse!("sensitive data");

    // Fallible decryption - recommended for critical code paths
    match secret.try_as_str() {
        Ok(s) => println!("Secret: {s}"),
        Err(ObfuseStrError::AllocationFailed) => {
            eprintln!("Out of memory during decryption");
        }
        Err(ObfuseStrError::AuthenticationFailed) => {
            eprintln!("Decryption failed - binary may be corrupted");
        }
        Err(ObfuseStrError::InvalidUtf8(e)) => {
            eprintln!("Invalid UTF-8: {e}");
        }
    }
}

Or with ? operator:

use obfuse::{obfuse, ObfuseStrError};

fn get_secret() -> Result<String, ObfuseStrError> {
    let secret = obfuse!("my secret");
    Ok(secret.try_as_str()?.to_string())
}

How It Works

  1. Compile Time: The obfuse! macro:

    • Generates a random encryption key and nonce
    • Encrypts the string literal using the selected algorithm
    • Embeds encrypted bytes, key, and nonce in the binary
  2. Runtime: The ObfuseStr type:

    • Stores encrypted data until accessed
    • Decrypts on first call to as_str() or Deref
    • Caches decrypted value for subsequent accesses
  3. Drop: When ObfuseStr is dropped:

    • Uses std::ptr::write_volatile to zero all sensitive memory
    • Zeros: encryption key, nonce, and decrypted plaintext
    • Prevents compiler from optimizing away the zeroing

Build Modes: Random vs Deterministic

This library supports two build modes for different use cases:

Default: Random Key (Recommended for Production)

// Random key generated each compile - different binary every build
let secret = obfuse!("my secret");
println!("{}", secret.as_str());  // Auto-decrypts
Build 1: key = [0xab, 0xcd, ...] (random)
Build 2: key = [0x12, 0x34, ...] (different random)
Build 3: key = [0x9f, 0xe2, ...] (different random)

Benefits:

  • Each build produces unique encryption
  • Harder for attackers to create universal decryption tools
  • Best obfuscation for production binaries

With Seed: Deterministic Key (For Testing/CI)

// Same seed = same key = reproducible output
let secret = obfuse!("my secret", seed = "test_seed_123");
println!("{}", secret.as_str());  // Auto-decrypts (same as random mode)
Build 1 (seed="test"): key = [0xaa, 0xbb, ...] (deterministic)
Build 2 (seed="test"): key = [0xaa, 0xbb, ...] (same!)
Build 3 (seed="prod"): key = [0xcc, 0xdd, ...] (different seed = different key)

Benefits:

  • Reproducible builds for CI/CD pipelines
  • Testable encrypted output
  • Debugging with known encryption state

Which Mode Should You Use?

Use Case Recommended
Production builds obfuse!("...") (random)
Unit tests obfuse!("...", seed = "test")
CI/CD pipelines obfuse!("...", seed = "ci")
Debugging encryption issues obfuse!("...", seed = "debug")

Important: Both Modes Are Obfuscation

┌─────────────────────────────────────────────────────┐
│            Your Binary (Both Modes)                 │
├─────────────────────────────────────────────────────┤
│  Encrypted Data: [0x4a, 0x7f, 0x2c, ...]           │
│  Encryption Key: [0xab, 0xcd, 0xef, ...]  ← HERE   │
│  Nonce:          [0x11, 0x22, 0x33, ...]           │
└─────────────────────────────────────────────────────┘
        Key is ALWAYS embedded in binary
        This is OBFUSCATION, not real encryption

For real secret protection, use runtime secrets management (environment variables, Vault, AWS Secrets Manager).

Security Considerations

What This Protects Against

  • Static binary analysis (strings command, hex editors)
  • Simple memory dumps of unaccessed secrets
  • Accidental logging of encrypted values

What This Does NOT Protect Against

  • Runtime memory inspection while string is in use
  • Sophisticated reverse engineering
  • Side-channel attacks
  • Compromised systems with debugging access

Best Practices

  1. Minimize lifetime: Keep ObfuseStr in scope only while needed
  2. Avoid cloning: Don't clone decrypted strings unnecessarily
  3. Use strong algorithms: Prefer aes-256-gcm or chacha20-poly1305 for real security
  4. Defense in depth: Use as one layer of protection, not the only one

API Reference

obfuse! Macro

// Random key (production)
obfuse!("string literal") -> ObfuseStr

// Deterministic key (testing/CI)
obfuse!("string literal", seed = "your_seed") -> ObfuseStr

Encrypts a string literal at compile time.

  • Without seed: Random key each compile (non-reproducible)
  • With seed: Deterministic key derived from seed (reproducible)

ObfuseStr Type

impl ObfuseStr {
    /// Returns the decrypted string, decrypting on first access.
    /// Panics with detailed message on error.
    pub fn as_str(&self) -> &str;

    /// Fallible version - returns Result instead of panicking.
    /// Recommended for critical code paths.
    pub fn try_as_str(&self) -> Result<&str, ObfuseStrError>;

    /// Returns the decrypted string as bytes.
    pub fn as_bytes(&self) -> &[u8];

    /// Fallible version of as_bytes().
    pub fn try_as_bytes(&self) -> Result<&[u8], ObfuseStrError>;

    /// Returns true if the string has been decrypted.
    pub fn is_decrypted(&self) -> bool;

    /// Pre-decrypt without returning the value.
    pub fn try_decrypt(&self) -> Result<(), ObfuseStrError>;

    /// Manually zero memory (also happens automatically on drop).
    pub fn zeroize(&mut self);
}

impl Deref for ObfuseStr {
    type Target = str;
    fn deref(&self) -> &str; // Triggers decryption, panics on error
}

impl Drop for ObfuseStr {
    fn drop(&mut self); // Volatile zeroing of all sensitive data
}

ObfuseStrError Type

/// Errors that can occur during ObfuseStr decryption
#[derive(Debug)]
pub enum ObfuseStrError {
    /// Memory allocation failed during decryption (OOM)
    AllocationFailed,

    /// AEAD authentication tag verification failed.
    /// Indicates ciphertext tampering or algorithm mismatch.
    AuthenticationFailed,

    /// Decrypted bytes are not valid UTF-8
    InvalidUtf8(std::str::Utf8Error),
}

impl std::fmt::Display for ObfuseStrError { /* ... */ }
impl std::error::Error for ObfuseStrError { /* ... */ }

Project Structure

obfuse-rs/
├── Cargo.toml              # Workspace configuration
├── README.md
├── obfuse/               # Main library crate (re-exports)
│   ├── Cargo.toml
│   └── src/lib.rs
├── obfuse-macros/        # Procedural macro crate
│   ├── Cargo.toml
│   └── src/lib.rs
└── obfuse-core/          # Core encryption/decryption logic
    ├── Cargo.toml
    └── src/
        ├── lib.rs
        ├── obfuse_str.rs    # ObfuseStr type implementation
        ├── aes.rs          # AES encryption
        ├── chacha.rs       # ChaCha20 encryption
        └── xor.rs          # XOR encryption

Building

# Build with default features (AES-256-GCM)
cargo build

# Build with specific algorithm
cargo build --no-default-features --features chacha20-poly1305

# Run tests
cargo test

# Run tests for specific algorithm
cargo test --no-default-features --features aes-128-gcm

License

MIT License - see LICENSE for details.

Contributing

Contributions welcome! Please read the contributing guidelines first.

Acknowledgments