const-secret 0.1.0

A `no_std` crate for compile-time encrypted secrets.
Documentation

const-secret

Crates.io Crates.io Downloads Docs.rs License MSRV Rust Edition no_std

A compile-time constant encryption library for Rust with pluggable drop strategies and multiple algorithms.

Motivation

A lot of the static or const string libraries make use of heavy macros which I don't like lol.

Features

  • Compile-time encryption: Secrets are encrypted at compile time; plaintext never appears in the binary.
  • Multiple algorithms:
    • XOR — Simple, fast single-byte XOR (best for basic obfuscation).
    • RC4 — Stream cipher with variable-length keys (1-256 bytes) for slightly better obfuscation.
  • Generic drop strategies: Choose how the decrypted buffer is handled on drop:
    • Zeroize — Securely overwrite memory using the zeroize crate (vendor-approved).
    • ReEncrypt<KEY> — Re-encrypt the buffer back to ciphertext on drop.
    • NoOp — Leave the buffer as-is (for testing or when you have other guarantees).
  • Lazy decryption: Decryption happens only when you dereference the value; the first dereference triggers decryption and sets a flag to prevent re-decryption.
  • no_std support: Fully no_std compatible (requires only core).
  • StringLiteral and ByteArray modes: Use StringLiteral to deref as &str, or ByteArray to deref as &[u8; N].

Installation

Add this to your Cargo.toml:

[dependencies]
const-secret = "1.0.0"

Usage

use const_secret::{Encrypted, Xor, ByteArray, StringLiteral};
use const_secret::drop_strategy::Zeroize;

const SECRET: Encrypted<Xor<0xAA, Zeroize>, StringLiteral, 5> =
    Encrypted::<Xor<0xAA, Zeroize>, StringLiteral, 5>::new(*b"hello");

fn main() {
    let plaintext: &str = &*SECRET;
    println!("{}", plaintext);  // prints: hello
}

When SECRET goes out of scope, the Zeroize drop strategy securely overwrites the decrypted buffer.

Different Drop Strategies

Zeroize (recommended for production)

use const_secret::{Encrypted, Xor, ByteArray};
use const_secret::drop_strategy::Zeroize;

const API_KEY: Encrypted<Xor<0x42, Zeroize>, ByteArray, 16> =
    Encrypted::<Xor<0x42, Zeroize>, ByteArray, 16>::new(*b"supersecretkey!!");

ReEncrypt (re-encrypts on drop)

use const_secret::{Encrypted, Xor, StringLiteral};
use const_secret::xor::ReEncrypt;

const PASSWORD: Encrypted<Xor<0xBB, ReEncrypt<0xBB>>, StringLiteral, 8> =
    Encrypted::<Xor<0xBB, ReEncrypt<0xBB>>, StringLiteral, 8>::new(*b"password");

NoOp (no cleanup; use with caution)

use const_secret::{Encrypted, Xor, ByteArray};
use const_secret::drop_strategy::NoOp;

const TEST_DATA: Encrypted<Xor<0xCC, NoOp>, ByteArray, 4> =
    Encrypted::<Xor<0xCC, NoOp>, ByteArray, 4>::new([1, 2, 3, 4]);

RC4 Algorithm (variable-length keys)

RC4 is a stream cipher that supports keys from 1 to 256 bytes. Note: RC4 is cryptographically broken; use only for obfuscation purposes.

use const_secret::{Encrypted, StringLiteral};
use const_secret::drop_strategy::Zeroize;
use const_secret::rc4::Rc4;

const KEY: [u8; 16] = *b"my-secret-key!!";

const SECRET: Encrypted<Rc4<16, Zeroize<[u8; 16]>>, StringLiteral, 6> =
    Encrypted::<Rc4<16, Zeroize<[u8; 16]>>, StringLiteral, 6>::new(*b"rc4sec", KEY);

fn main() {
    let plaintext: &str = &*SECRET;
    println!("{}", plaintext);  // prints: rc4sec
}

RC4 with ReEncrypt

use const_secret::{Encrypted, ByteArray};
use const_secret::rc4::{Rc4, ReEncrypt};

const KEY: [u8; 8] = *b"rc4key!!";

const DATA: Encrypted<Rc4<8, ReEncrypt<8>>, ByteArray, 10> =
    Encrypted::<Rc4<8, ReEncrypt<8>>, ByteArray, 10>::new(*b"sensitive!", KEY);

How it works

  1. Compile-time encryption: Encrypted::new() encrypts plaintext at compile time using the selected algorithm:
    • XOR: XORs each byte with the single-byte key.
    • RC4: Runs the Key Scheduling Algorithm (KSA) to initialize the S-box, then the Pseudo-Random Generation Algorithm (PRGA) to generate a keystream and XOR with plaintext. The ciphertext is stored in the binary; plaintext never appears.
  2. Lazy decryption: Deref checks an atomic one-time flag:
    • First successful check sets the flag and decrypts ciphertext -> plaintext in place,
    • Later derefs skip re-decryption and return plaintext directly.
  3. Drop: selected DropStrategy runs:
    • Zeroize: secure overwrite via zeroize,
    • ReEncrypt: re-encrypts plaintext back to ciphertext,
    • NoOp: no cleanup.

Verification

1) Verify plaintext is absent from the binary

cargo build --example debug_drop
strings target/debug/examples/debug_drop | grep -E "^(hello|world|secret|leaked)$"
# Expected: no output

2) Verify release assembly has atomic guard + XOR transforms

cargo build --example debug_drop --release
objdump -d target/release/examples/debug_drop | grep -Ei "cmpxchg|xorl|xorb|movaps|xorps|0xaaaaaaaa|0xbbbbbbbb|0xdddddddd|0xeeeeeeee"

Expected:

  • cmpxchg (or equivalent atomic guard pattern),
  • scalar XOR for short buffers (xorl + xorb),
  • SIMD XOR for long buffers (movaps + xorps) when optimization chooses vectorization.

A. Short payloads: expect scalar XOR (very common)

For short strings (like 5 bytes), optimized code typically uses:

  • one 4-byte XOR immediate (imm32),
  • one 1-byte XOR immediate (imm8).

Example pattern:

test   %al,%al
jnz    ...
mov    $0x1,%cl
xor    %eax,%eax
lock cmpxchg %cl,offset(%rsp)
jnz    ...
xorl   $0xbbbbbbbb,(%rsp)
xorb   $0xbb,0x4(%rsp)

This means:

  • one-time guard succeeds once,
  • payload is transformed in place with minimal scalar instructions.

B. Long payloads: SIMD may be emitted

With longer strings (e.g. the long ReEncrypt<0xBB> example in examples/debug_drop.rs), release builds may vectorize XOR into 16-byte chunks.

Quick grep:

cargo build --example debug_drop --release
objdump -d target/release/examples/debug_drop | grep -Ei "cmpxchg|movaps|xorps|0xbbbbbbbb"

Representative pattern:

test   %al,%al
jnz    ...
lock cmpxchg %cl,offset(%rsp)
jnz    ...
movaps xmm0,[key_mask_0xbb]
movaps xmm1,[rsp+...]
xorps  xmm1,xmm0
movaps [rsp+...],xmm1
... repeated for additional 16-byte chunks ...

Notes:

  • xorps is used as a bitwise XOR instruction on XMM registers.
  • Exact mnemonics/registers/offsets vary by LLVM version, optimization level, and target CPU features.
  • Seeing scalar in one build and SIMD in another is normal.

C. Architecture notes

  • x86_64: common to see cmpxchg, xorl/xorb, and for longer payloads movaps/xorps.
  • AArch64: look for atomic primitives (ldxr/stxr loops or cas*) and eor vector/scalar ops.

AArch64 quick check:

cargo build --example debug_drop --release --target aarch64-unknown-linux-gnu
objdump -d target/aarch64-unknown-linux-gnu/release/examples/debug_drop | grep -Ei "ldxr|stxr|cas|eor|0xbb|0xaa|0xdd|0xee"

Building

cargo build
cargo test
cargo build --example debug_drop
cargo run --example debug_drop

Thread Safety

Encrypted is Sync and can be safely shared across threads. The implementation uses a 3-state atomic to coordinate lazy decryption:

  1. UNENCRYPTED (0): Initial state - first thread to see this attempts decryption via compare_exchange
  2. DECRYPTING (1): A thread has won the race and holds exclusive mutable access to decrypt in-place
  3. DECRYPTED (2): Decryption complete - all threads can safely read the plaintext

If a thread loses the race, it spin-waits until decryption completes, ensuring no thread can access the buffer while another thread holds a mutable reference. This implementation has been verified with Miri to be free of data races and undefined behavior.

After the first decryption, all subsequent dereferences are fast-path atomic loads.

Implementation Details

Why AtomicU8 instead of an enum?

no_std environments don't have AtomicUsize or AtomicEnum. We use AtomicU8 with const values because:

  • It's the smallest atomic type available in core::sync::atomic
  • AtomicU8::compare_exchange is available on all platforms that Rust supports
  • Enum discriminants would require #[repr(u8)] and extra casting anyway
  • The three states (0, 1, 2) fit perfectly in a single byte

Benchmarks

All benchmarks run on AMD Ryzen 7 5800X3D @ 3.4-4.0GHz using Criterion.rs.

Single-Threaded Performance

Algorithm Operation Size Time Throughput
XOR First Decrypt 7 bytes 2.07 ns 3,385 MB/s
XOR First Decrypt 23 bytes 4.47 ns 5,149 MB/s
XOR First Decrypt 89 bytes 6.26 ns 14,213 MB/s
XOR Cached Access any 0.47 ns -
RC4 (16B key) First Decrypt 7 bytes 1,160 ns 6.0 MB/s
RC4 (16B key) First Decrypt 23 bytes 1,141 ns 20.2 MB/s
RC4 (16B key) First Decrypt 89 bytes 1,537 ns 57.9 MB/s
RC4 Cached Access any 0.23 ns -

Concurrent Access (23 bytes payload)

Threads XOR Cold XOR Hot RC4 Cold RC4 Hot
10 131 μs 130 μs 128 μs 129 μs
20 273 μs - 271 μs -
50 938 μs - - -

Drop Strategy Overhead (23 bytes payload)

Strategy XOR Cost RC4 Cost
NoOp 11 ns 1,145 ns
Zeroize 14 ns 1,150 ns
ReEncrypt 11 ns 1,625 ns

Alignment Impact (XOR, 89 bytes)

Alignment Time vs Unaligned
Unaligned 6.19 ns baseline
Aligned8 6.19 ns 0.0%
Aligned16 6.21 ns +0.3%

View Interactive Reports →

Running Benchmarks Locally

# Run all benchmarks (results in target/criterion/)
cargo bench

# Run specific benchmark
cargo bench --bench xor_single_threaded

# Copy results to docs folder for GitHub Pages
cp -r target/criterion/* docs/benchmarks/
git add docs/benchmarks && git commit -m "Update benchmarks"

Updating Published Benchmarks

Benchmarks are published to GitHub Pages automatically on every push to main:

  1. Run benchmarks locally: cargo bench
  2. Copy results: cp -r target/criterion/* docs/benchmarks/
  3. Commit and push: CI will deploy to GitHub Pages

Caveats

  • Not cryptographically secure: Both XOR and RC4 provide obfuscation, not encryption. RC4 is cryptographically broken. Use this library for compile-time constant storage with defense-in-depth layering, not as a standalone encryption scheme.

  • Memory observability: This library does not protect against memory-reading attacks. Once a secret is decrypted and in scope, an attacker with physical access (e.g., cold-boot attack), debugger access, or memory-disclosure vulnerabilities can observe the plaintext in RAM. Even Zeroize and ReEncrypt only clean up after the value is dropped—the plaintext remains observable while the value is live and dereferenced.

    This is by design. The library's goal is to prevent secrets from being embedded in the static binary, not to provide runtime memory protection. If you need defense against memory-reading attacks, consider:

    • Using trusted execution environments (TEEs) or secure enclaves
    • Minimizing plaintext lifetime and reducing the number of copies in memory
    • Encrypting sensitive data at rest and only decrypting on demand
    • Layering Zeroize/ReEncrypt with your own memory-access controls

    Use this library as part of a defense-in-depth strategy, not as a standalone guarantee.

Choosing an Algorithm

Algorithm Speed Key Size Use Case
XOR Fastest Single byte (0-255) Speed-critical, simple obfuscation
RC4 Medium 1-256 bytes Variable key length, slightly better obfuscation

Recommendation: Use XOR for most cases—it's faster and simpler. Use RC4 only if you need variable-length keys for some reason.

cargo build --example debug_drop
strings target/debug/examples/debug_drop | grep -c hello
# Output: 0

cargo run --example debug_drop
# Output includes:
# [zeroize] decrypted: "hello"
# [reencrypt] decrypted: "world"
# [reencrypt-long] decrypted: "world-world-world-world-world-world-world-world-world-world-1234"
# [noop-derefed] decrypted: "leaked"
# [bytes-zeroize] decrypted: [de, ad, be, ef]
# done — all secrets dropped

cargo build --example debug_drop --release
objdump -d target/release/examples/debug_drop | grep -Ei "cmpxchg|movaps|xorps|xorl|xorb|0xaaaaaaaa|0xbbbbbbbb|0xdddddddd|0xeeeeeeee"

License

Licensed under either of:

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.