Compile-time encrypted memory protection for .NET
Unique encryption algorithms and compile-time encrypted constants — plaintext never appears in the binary
🧬 Unique Per-Type — Each subclass gets its own randomly generated algorithm
🔐 Compile-Time Encrypted Constants — Literal values (ints, floats, strings) are encrypted at compile time and never appear as plaintext in the binary
🔀 High Entropy — 20–50 operations (ADD, SUB, XOR, ROL, ROR, NOT) with large random constants, randomly ordered
🔑 IL-Embedded Constants — Large operands (10,000–999,999) emitted directly as IL instructions
🛡️ Memory Safe — All decrypted data zeroed in finally blocks, even on exceptions
👻 Pointer Hiding — HiddenValue<T> encrypts GCHandle pointers to break reference chains
⏱️ Ephemeral Access — EphemeralValue<T> destroys decrypted values immediately after callback
🏗️ Build-Unique — Fresh random parameters every build; static analysis must restart each time
🎭 Complex IL — Grouped operations with multiple Int64 locals produce intricate decompiled output
- 🏛️ Architecture Overview
- 📁 Project Structure
- 🚀 Quick Start
- 🧩 Core Types
- 🧬 Fody Weaver — Compile-Time Algorithm Injection
- 🔐 Compile-Time Constant Encryption
- 🔨 Building
- 📖 Usage Guide
- 🔒 Security Model
┌──────────────────────────────────────────────────────────────────────────┐
│ Consumer Assembly │
│ │
│ class SecureHealth : EncryptedValue<int> { } ← Fody injects algo A │
│ class SecureGold : EncryptedValue<int> { } ← Fody injects algo B │
│ class SecureApiKey : EphemeralValue<string> {} ← Fody injects algo C │
│ EncryptedValue<int> direct = 42; ← uses base type algo │
│ var hidden = new HiddenValue<Config>(cfg); ← uses EncryptedValue<long>│
│ │
│ Each type gets an override of Encrypt(byte[]) and Decrypt(byte[]) │
│ with a unique chain of 20-50 randomly ordered byte-level operations — │
│ different large constants and rotation amounts every time. │
│ HiddenValue delegates encryption to an internal EncryptedValue<long>. │
└──────────────────────────────────────────────────────────────────────────┘
│ references
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ zsCrypt Core Types │
│ │
│ Types/ │
│ ├─ ISecureValue / ISecureValue<T> — common interface │
│ ├─ EncryptedValue<T> — virtual Encrypt/Decrypt (weaver-injected) │
│ ├─ EphemeralValue<T> — virtual Encrypt/Decrypt (weaver-injected) │
│ └─ HiddenValue<T> — delegates to internal EncryptedValue<long> │
│ │
│ Serialization/ │
│ └─ SerializationProvider — Marshal/UTF-8 serialize with memory safety │
│ │
│ Memory/ │
│ └─ MemoryCleaner — secure zero strings, arrays, values │
└──────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────┐
│ zsCrypt.Fody (build-time only) │
│ │
│ ModuleWeaver │
│ ├─ Scan assembly for EncryptedValue / EphemeralValue types & subclasses │
│ ├─ For each: generate unique CSPRNG parameters & operation sequence │
│ │ ├─ 20-50 randomly ordered operations from 6 types │
│ │ │ (ADD, SUB, XOR, ROL, ROR, NOT with large random constants) │
│ │ └─ Grouped with multiple Int64 locals for complex decompiled output │
│ └─ Emit override Encrypt/Decrypt IL with those parameters │
│ Large constants (10000-999999) embedded directly as IL operands │
└──────────────────────────────────────────────────────────────────────────┘
zsCrypt/
├── zsCrypt.sln
├── README.md
├── demo.md # Visual walkthrough with code and output
└── src/
├── zsCrypt.Example/ # Example console app with core types (net8.0)
│ ├── zsCrypt.Example.csproj
│ ├── Program.cs # Subclass definitions and usage demos
│ ├── FodyWeavers.xml # Fody configuration
│ ├── Serialization/
│ │ └── SerializationProvider.cs # Type-to-bytes (with unmanaged memory zeroing)
│ ├── Memory/
│ │ └── MemoryCleaner.cs # Secure memory zeroing
│ └── Types/
│ ├── ISecureValue.cs # Common interface for all secure value types
│ ├── EncryptedValue.cs # Virtual Encrypt/Decrypt — encrypted value-type wrapper
│ ├── EphemeralValue.cs # Virtual Encrypt/Decrypt — controlled-lifetime wrapper
│ └── HiddenValue.cs # Delegates to EncryptedValue<long> — GCHandle pointer hiding
│
└── zsCrypt.Fody/ # Fody weaver (netstandard2.0, build-time only)
├── zsCrypt.Fody.csproj
└── ModuleWeaver.cs # Algorithm injection via Mono.Cecil IL rewriting
using zsCrypt.Types;
// Option A: Use EncryptedValue<T> directly — the weaver injects a generic algorithm
EncryptedValue<int> health = new(100);
health.Value -= 25;
int hp = health; // 75 — decrypted automatically
health.Dispose(); // securely clears encrypted bytes
// Option B: Define a subclass — each subclass gets its own unique algorithm
class SecureHealth : EncryptedValue<int> {
public SecureHealth() { }
public SecureHealth(int value) : base(value) { }
public static implicit operator SecureHealth(int value) => new(value);
}
SecureHealth secureHp = 100;
secureHp.Value -= 25;
int raw = secureHp; // 75 — decrypted automatically
secureHp.Dispose(); // securely clears encrypted bytesNote
Creating subclasses is optional. Using EncryptedValue<T> or EphemeralValue<T> directly is fully supported — the Fody weaver injects a generic encryption/decryption routine into the base types themselves. Subclasses are useful when you want each type to receive its own unique algorithm. HiddenValue<T> can also be used directly — it delegates to an internal EncryptedValue<long>.
All three types implement ISecureValue (IDisposable + IsDisposed). Types with direct value access also implement ISecureValue<T> (adds Value property).
EncryptedValue<T> and EphemeralValue<T> share the same pattern: virtual Encrypt(byte[]) and Decrypt(byte[]) methods. The Fody weaver injects unique algorithm code into both the base types and each subclass at compile time. Creating subclasses is optional — using the base types directly is fully supported. Without the weaver, values are stored unencrypted (development mode).
HiddenValue<T> takes a different approach — it stores an encrypted GCHandle pointer via an internal EncryptedValue<long>, delegating all encryption to that instance. No subclassing of HiddenValue is needed; use it directly.
📖 See demo.md for a visual walkthrough with code samples and output for every type.
Purpose: Encrypts an unmanaged value type (int, float, double, structs) in memory. The value is serialized to bytes, encrypted via the virtual method, and stored. On read, the bytes are cloned, decrypted, deserialized, and the decrypted clone is zeroed inside a try/finally.
Constraint: where T : struct
Key features:
- Implements
ISecureValue<T>for uniform lifecycle and value access - Implicit operators for transparent
T ↔ EncryptedValue<T>conversion - Thread-safe via
lock IDisposable— securely clears encrypted bytes on cleanup- Decrypted byte clones are always zeroed in
finallyblocks (exception-safe) - Plaintext serialized bytes are zeroed on encryption failure
//Define a subclass — the weaver injects an algorithm at compile time
class SecureHealth : EncryptedValue<int> {
public SecureHealth() { }
public SecureHealth(int value) : base(value) { }
public static implicit operator SecureHealth(int value) => new SecureHealth(value);
}
//Use it
SecureHealth health = 100;
health.Value -= 25;
int raw = health; // 75
health.Dispose();HiddenValue<T>
Purpose: Obscures a reference type in memory by storing it behind a GCHandle whose IntPtr pointer value is encrypted via an internal EncryptedValue<long>. Memory scanners following pointer chains will not find the target object because the handle value is encrypted.
Constraint: where T : class
Key features:
- Implements
ISecureValue<T>for uniform lifecycle and value access - GCHandle keeps the object alive while hiding the reference path
- Handle pointer stored as an encrypted
longviaEncryptedValue<long>— no own Encrypt/Decrypt needed IDisposable+ finalizer safety net for GCHandle cleanup- Thread-safe via
lock - No subclassing required — use
HiddenValue<T>directly
//Use HiddenValue directly — encryption is handled by the internal EncryptedValue<long>
using (var config = new HiddenValue<DatabaseConfig>(LoadConfig())) {
var conn = new SqlConnection(config.Value.ConnectionString);
}Purpose: The most secure wrapper. Encrypts data and strictly controls the lifetime of the decrypted form. The decrypted value is only accessible inside a callback and is destroyed immediately after the callback returns. Internally uses HiddenValue<byte[]> for the backing cipher text, adding pointer obscuring on top of encryption.
Constraint: T must be supported by both SerializationProvider and MemoryCleaner.
Key features:
- Implements
ISecureValuefor uniform lifecycle management - Callback-based access via
refdelegates:Use(RefAction<T>)andUse<TResult>(RefFunc<T, TResult>)— prevents value-type stack copies from leaking unzeroed memory - Decrypted bytes AND deserialized values destroyed in
finallyblocks (exception-safe) refconstructor can erase the caller's original variable- Plaintext bytes zeroed on encryption failure in
Set IDisposablefor cleanup
⚠️ CRITICAL — Do not allocate new copies of the value inside the callbackThe decrypted value is passed by
refusing customRefAction<T>/RefFunc<T, TResult>delegates. This prevents automatic stack copies of value types from leaking unzeroed memory — the callback operates on the same location thatMemoryCleaner.Destroyzeroes after the callback returns.
MemoryCleanerzeroes reference-type values in-place: for strings it overwrites the character buffer directly; for byte arrays it clears the array in place. This means holding another reference to the same object is safe — both variables point to the same backing memory that will be zeroed when the callback exits.What IS unsafe is any operation that allocates a new object, because that new object is untracked and will not be zeroed:
- Strings: concatenation (
"Bearer " + key),string.Copy,Substring,ToUpper, string interpolation ($"...{key}...") — each produces a newstring- Value types: any explicit assignment (e.g.
int x = value;) copies the value itself, leaving the copy unzeroed
class SecureApiKey : EphemeralValue<string> {
public SecureApiKey() { }
public SecureApiKey(ref string value, bool eraseOrigin = true) : base(ref value, eraseOrigin) { }
}
string apiKey = "sk-abc123";
using (var secure = new SecureApiKey(ref apiKey, eraseOrigin: true)) {
//apiKey characters are now zeroed
secure.Use((ref string key) => {
//✅ SAFE: the value is passed by ref — no stack copy is made
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", key);
//key's character buffer is zeroed when this callback exits
});
//✅ ALSO SAFE: 'alias' points to the same string object; its chars will be zeroed too
//string alias = null;
//secure.Use((ref string key) => alias = key);
//❌ UNSAFE: concatenation allocates a NEW string that will NOT be zeroed
//string header = null;
//secure.Use((ref string key) => header = "Bearer " + key); // new string escapes lifecycle control
}- At build time, Fody runs
zsCrypt.Fody'sModuleWeaver.Execute()as a post-compile step - The weaver scans the target assembly for
EncryptedValue<T>,EphemeralValue<T>, and all types that inherit from them (HiddenValue is excluded — it delegates to EncryptedValue internally) - For each type, it generates unique random parameters using
RandomNumberGenerator - It creates override methods for
Encrypt(byte[])andDecrypt(byte[])with IL that implements the unique algorithm - The modified assembly is written back — the algorithm exists only in IL, never in source
For each type, the weaver generates a unique chain of 20–50 randomly selected and ordered operations. The conceptual equivalent (the actual code is IL, not C#):
// Auto-generated by zsCrypt.Fody — unique per type, unique per build
class SecureHealth : EncryptedValue<int> {
protected override void Encrypt(byte[] data) {
for (int i = 0; i < data.Length; i++) {
long val = data[i];
val += 487293; // ADD large constant
val ^= 173059; // XOR large constant
val = (val << 3) | ((val & 0xFF) >> 5); // Rotate left 3
val = ~val; // Bitwise NOT
val -= 892714; // SUB large constant
val = ((val & 0xFF) >> 5) | (val << 3); // Rotate right 5
val ^= 551082; // XOR large constant
val += 304167; // ADD large constant
// ... more random operations (20-50 total) ...
data[i] = (byte)(val & 0xFF);
}
}
protected override void Decrypt(byte[] data) {
// Exact inverse: reversed order, inverse operations
// Sub → Xor → Rol → Add → Ror → Not → Xor → Sub → ...
}
}The weaver selects from 6 distinct operation types, each with random parameters:
| Operation | Encrypt | Decrypt (Inverse) | Parameters |
|---|---|---|---|
| ADD Constant | val = val + c |
val = val - c |
c: 10,000–999,999 |
| SUB Constant | val = val - c |
val = val + c |
c: 10,000–999,999 |
| XOR Constant | val ^= c |
val ^= c (self-inverse) |
c: 10,000–999,999 |
| Rotate Left | ROL(val, n) |
ROR(val, n) |
n: 1–7 bits |
| Rotate Right | ROR(val, n) |
ROL(val, n) |
n: 1–7 bits |
| Bitwise NOT | ~val |
~val (self-inverse) |
— |
Important
All operations use Int64 (long) arithmetic internally and are masked with & 0xFF at the end to stay within the byte range. The large constants (10,000–999,999) make pattern recognition significantly harder compared to small byte-range values.
IL complexity: Operations are grouped into blocks of 5–30 and processed through 5–10 Int64 local variables. The grouping and local-variable chaining produce complex expressions in decompiled output, making reverse engineering significantly harder.
Why this design is effective:
- Each type has a completely different algorithm — cracking one tells you nothing about another
- The large random constants (10,000–999,999) are embedded directly as IL operands, not stored as arrays
- Each build produces different random parameters — static analysis must be redone per build
- 6 operation types × 20–50 operations × large random constants × grouped local-variable chains creates enormous combinatorial diversity
- The generated IL with grouped Int64 operations blends with normal compiled code — no obvious "crypto routine" signature
The Fody weaver doesn't just inject encryption algorithms — it also encrypts literal constant values directly at compile time. When you assign a literal (int, float, or string) to an encrypted type, the weaver replaces the plaintext value in the binary with pre-encrypted bytes. The original constant never appears in the compiled assembly.
// "12345" is encrypted in the binary — it does NOT exist as plaintext in the DLL
EncryptedValue<int> secret = new(12345);
Console.WriteLine(secret.Value); // 12345 — decrypted at runtime
// String literals are also encrypted — "sk-example-..." is NOT in the binary
EphemeralValue<string> key = "sk-example-xxxxxxxxxxxx";
key.Use((ref string k) => Console.WriteLine(k)); // decrypted only inside the callback
// Subclass literals use that type's own algorithm
SecureHealth health = 9999; // 9999 encrypted at compile time with SecureHealth's algorithmImportant
This uses the same per-type algorithm that the weaver injects for runtime encryption — no separate mechanism. The literal is serialized to bytes, encrypted with the type's own algorithm at build time, and stored as an encrypted byte array in the IL. At runtime, the type's Decrypt method handles it normally.
- .NET 8.0 SDK or later
- Visual Studio 2022+ or the
dotnetCLI
cd zsCrypt
dotnet restore
dotnet builddotnet run --project src/zsCrypt.Example📋 Expected output
=== Obfuscation Library Demo ===
Each type has a unique algorithm injected by the Fody weaver.
--- EncryptedValue<int> (direct usage, no subclass) ---
Initial value: 42
After add: 50
--- SecureHealth (EncryptedValue<int>) ---
Initial health: 100
After damage: 75
Implicit cast: 75
--- SecureGold (EncryptedValue<int>, different algorithm) ---
Initial gold: 5000
After loot: 5250
--- SecureScore (EncryptedValue<float>) ---
Score: 99.5
--- HiddenValue<string> (encrypted GCHandle pointer) ---
Hidden value: SuperSecretPassword123
Via implicit: SuperSecretPassword123
Disposed — GCHandle freed, encrypted data cleared.
--- HiddenValue<byte[]> (encrypted GCHandle pointer) ---
Hidden bytes: [1, 2, 3, 4]
--- SecureApiKey (EphemeralValue<string>) ---
Before: apiKey = "sk-abc123def456ghi789"
After ref store: apiKey = "" (zeroed)
Inside callback: "sk-abc123def456ghi789"
Callback done — decrypted value destroyed.
--- SecurePassword (EphemeralValue<string>, different algorithm) ---
Computed length inside callback: 17
Returned length: 17
Decrypted value is already destroyed.
=== Demo Complete ===
Creating subclasses is optional — you can use EncryptedValue<T> directly. However, each subclass you create gets its own unique algorithm injected at compile time. Even two subclasses wrapping the same type get different algorithms:
using zsCrypt.Types;
//Use EncryptedValue<T> directly — encryption is injected into the base type
EncryptedValue<int> directValue = new(42);
int raw = directValue.Value; // decrypted automatically
//Or define subclasses — each gets a DIFFERENT algorithm
class SecureHealth : EncryptedValue<int> {
public SecureHealth() { }
public SecureHealth(int value) : base(value) { }
public static implicit operator SecureHealth(int value) => new SecureHealth(value);
}
class SecureGold : EncryptedValue<int> {
public SecureGold() { }
public SecureGold(int value) : base(value) { }
public static implicit operator SecureGold(int value) => new SecureGold(value);
}
//EphemeralValue subclass for controlled-lifetime sensitive data
class SecureApiKey : EphemeralValue<string> {
public SecureApiKey() { }
public SecureApiKey(ref string value, bool eraseOrigin = true) : base(ref value, eraseOrigin) { }
}//EncryptedValue — use directly or via subclass
EncryptedValue<int> points = new(50);
int p = points.Value;
//EncryptedValue subclass — transparent usage via implicit operators
SecureHealth health = 100;
health.Value -= 25;
int hp = health;
//HiddenValue — use directly, no subclass needed
using (var config = new HiddenValue<AppConfig>(LoadConfig())) {
UseConfig(config.Value);
}
//EphemeralValue — callback-only, automatic cleanup
string key = GetApiKey();
using (var secure = new SecureApiKey(ref key, eraseOrigin: true)) {
secure.Use((ref string k) => CallApi(k));
int len = secure.Use((ref string k) => k.Length);
}Note
Creating subclasses is optional. Using EncryptedValue<T> or EphemeralValue<T> directly (e.g., new EncryptedValue<int>(42)) is fully supported — the Fody weaver injects a generic encryption/decryption routine into the base types themselves. Subclasses are useful when you want each type to receive its own unique algorithm. HiddenValue<T> can also be used directly — it delegates to an internal EncryptedValue<long>.
This library helps protects against:
- 🔍 Memory scanners (Cheat Engine, process memory dumpers) searching for known value patterns
- 🔗 Automated pointer-chain scanners that follow GCHandle → object references
- 🕵️ Casual reverse engineering of process memory dumps
- 🔎 Static binary analysis — literal constants are encrypted at compile time and never appear as plaintext
- 🔄 Cross-build analysis — each build has different algorithms and keys
It does not protect against:
- ⚡ Analysis by advanced attackers.
| # | Layer | Technique | Protects Against |
|---|---|---|---|
| 1 | Compile-time constant encryption | Literal values encrypted at build time — plaintext never in the binary | Static binary analysis for hardcoded secrets |
| 2 | Algorithm uniqueness | Different operation chain per type (20-50 ops from 6 types) | Cross-type correlation |
| 3 | Build uniqueness | New random parameters per build (CSPRNG) | Cross-build static analysis |
| 4 | IL-embedded constants | Large operands (10,000–999,999) as direct IL instructions | Memory scanning for key material |
| 5 | Operation diversity | ADD, SUB, XOR, ROL, ROR, NOT with large random constants | Pattern matching / signature detection |
| 6 | IL complexity | Grouped operations with multiple Int64 locals (5–10) | Automated decompiler pattern recognition |
| 7 | Pointer hiding | GCHandle + IntPtr encrypted via EncryptedValue<long> |
Reference chain scanning |
| 8 | Lifetime control | Callback-only access via ref delegates (EphemeralValue) |
Decrypted value lingering + value-type stack copies |
| 9 | Memory zeroing | try/finally + MemoryCleaner on all decrypted buffers |
Residual data in freed memory |
| 10 | Unmanaged zeroing | ZeroUnmanagedMemory before FreeHGlobal |
Plaintext in unmanaged heap |
| 11 | Origin erasure | EphemeralValue ref constructor | Original variable remaining readable |
| 12 | Failure safety | Plaintext zeroed in catch blocks on encrypt failure |
Leak on exception paths |
This project is provided as-is for educational and development purposes.