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:
[]
= "1.0.0"
Usage
use ;
use Zeroize;
const SECRET: =
new;
When SECRET goes out of scope, the Zeroize drop strategy securely overwrites the decrypted buffer.
Different Drop Strategies
Zeroize (recommended for production)
use ;
use Zeroize;
const API_KEY: =
new;
ReEncrypt (re-encrypts on drop)
use ;
use ReEncrypt;
const PASSWORD: =
new;
NoOp (no cleanup; use with caution)
use ;
use NoOp;
const TEST_DATA: =
new;
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 ;
use Zeroize;
use Rc4;
const KEY: = *b"my-secret-key!!";
const SECRET: =
new;
RC4 with ReEncrypt
use ;
use ;
const KEY: = *b"rc4key!!";
const DATA: =
new;
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
1) Verify plaintext is absent from the binary
|
# Expected: no output
2) Verify release assembly has atomic guard + XOR transforms
|
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:
|
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:
|
Building
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/)
# Run specific benchmark
# Copy results to docs folder for GitHub Pages
&&
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.
|
# Output: 0
# 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
|
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.