1 unstable release
Uses new Rust 2024
| 0.1.0 | Feb 18, 2026 |
|---|
#335 in Data structures
758 downloads per month
1.5MB
803 lines
const-secret
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 thezeroizecrate (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_stdsupport: Fullyno_stdcompatible (requires onlycore).- StringLiteral and ByteArray modes: Use
StringLiteralto deref as&str, orByteArrayto 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
- 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.
- Lazy decryption:
Derefchecks 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.
- Drop: selected
DropStrategyruns:Zeroize: secure overwrite viazeroize,ReEncrypt: re-encrypts plaintext back to ciphertext,NoOp: no cleanup.
Verification
Quick Checks (click to expand)
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.
Detailed Verification
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:
xorpsis 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 payloadsmovaps/xorps. - AArch64: look for atomic primitives (
ldxr/stxrloops orcas*) andeorvector/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:
- UNENCRYPTED (0): Initial state - first thread to see this attempts decryption via
compare_exchange - DECRYPTING (1): A thread has won the race and holds exclusive mutable access to decrypt in-place
- 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_exchangeis 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% |
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:
- Run benchmarks locally:
cargo bench - Copy results:
cp -r target/criterion/* docs/benchmarks/ - 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
ZeroizeandReEncryptonly 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/ReEncryptwith 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.
Example: Checking the Binary
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:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
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.