4.5 out of 5 (2 reviews on Visual Studio Marketplace)

How to Hide API Keys, URLs and Connection Strings in a .NET Assembly

Hands-On Guide · Opaquer .NET Obfuscator · ~14 min read
Direct Answer: Opaquer protects sensitive string literals in .NET assemblies through two escalating mechanisms: in-assembly encryption (strings become encrypted ciphertext within the managed binary, decrypted at runtime) and native DLL storage (strings are extracted entirely from the managed assembly and placed in a compiled C++ binary that .NET decompilers cannot read). Choose in-assembly encryption for convenience and general protection; choose native DLL storage for high-value secrets like license keys, API endpoints, and database connection strings that must never appear in decompiled output.

The Problem

String literals are the single most underappreciated attack surface in .NET applications. Unlike compiled code, which requires at least some reverse-engineering effort to understand, string literals sit in the assembly metadata in plain, readable text. Any decompiler-including free tools like ILSpy, dnSpy, or dotPeek-will display them without any analysis required.

This creates a specific and immediate danger. Consider what lives in a typical commercial .NET application:

An attacker doesn't need to understand your architecture to exploit these. They simply search the decompiled output for recognizable patterns. The string "https://api.mycompany.com" tells them where your application phones home. The string "Server=prod-sql01;Database=Customers;User Id=appuser;Password=..." is a complete set of credentials. The string "Invalid license key" points directly to the validation logic that can be patched.

Once an attacker locates a string of interest, they are positioned to either extract the secret directly or use the string as a landmark to find and modify the surrounding code. String encryption removes this first and easiest foothold.

Small C# Example

The following class demonstrates the exact kinds of sensitive string literals that appear in production .NET applications:

C# - Sensitive Strings
public class AppConfiguration
{
    // API endpoint for external service
    public const string ApiUrl = "https://api.mycompany.com/v2/data";
    
    // Database connection with credentials
    public const string ConnectionString = 
        "Server=prod-sql01.internal;Database=Customers;User Id=appuser;Password=Xk9mP2qR;";
    
    // License validation endpoint
    public const string LicenseServer = "https://license.mycompany.com/verify";
    
    // Error message revealing internal state
    public const string ErrorMessage = "License validation failed: server returned status 403";
    
    public void Connect()
    {
        Console.WriteLine($"Connecting to {ApiUrl}...");
        // ... use ConnectionString to open database
    }
}

Compile this class, and every one of those values is embedded in the assembly as a searchable, readable literal. The connection string alone would allow an attacker to directly access the production database if the credentials are valid and the server is reachable.

What Happens Without Obfuscation

Step 1: Open the Assembly in ILSpy

Load the compiled .dll or .exe into ILSpy, dnSpy, or any other .NET decompiler. The tool reconstructs the source code with near-perfect accuracy.

Step 2: Search for the String

Use the decompiler's search function (Ctrl+Shift+F in ILSpy) to search for "api.mycompany.com" or "prod-sql01". The search returns an immediate hit.

Step 3: Show Plaintext

The decompiler displays the AppConfiguration class with all string literals intact:

ILSpy - Unprotected Output
public class AppConfiguration
{
    public const string ApiUrl = "https://api.mycompany.com/v2/data";
    public const string ConnectionString = 
        "Server=prod-sql01.internal;Database=Customers;User Id=appuser;Password=Xk9mP2qR;";
    public const string LicenseServer = "https://license.mycompany.com/verify";
    public const string ErrorMessage = "License validation failed: server returned status 403";
}

Every secret is visible. The attacker now has:

  1. The database server hostname (prod-sql01.internal)
  2. Valid credentials (appuser / Xk9mP2qR) for that database
  3. The exact API endpoint for your external service
  4. The license validation URL, which can be intercepted or redirected
  5. A clear landmark ("License validation failed") to locate the validation logic in the surrounding code

This is not a theoretical risk. It is the default state of every unprotected .NET assembly.

Step-by-Step Opaquer Configuration

Step 1: Load the Assembly

Open Opaquer and load the assembly containing your sensitive strings. Both .dll and .exe files are supported.

Step 2: Navigate to the Strings Tab

The Strings tab-page displays a hierarchical tree of your assembly's structure-classes, methods, fields, and the string literals they contain. Use the left pane to navigate; the right pane provides configuration options for the selected strings.

Step 3: Select Strings for Encryption

You can select strings in three ways:

For targeted protection of sensitive values, select only the strings that matter-API URLs, connection strings, license endpoints, and error messages that reveal internal logic. Opaquer also provides a "Do not obfuscate Strings with length less than" threshold to exclude trivial short strings automatically.

Step 4: Choose the String Storage Location

This is the critical decision. Opaquer offers two options under "Where Assembly Strings Are Saved":

Option A - In-Assembly Encryption:

Strings are encrypted and remain within the managed assembly. A decryption routine is injected into the assembly and runs at runtime to restore strings in memory when needed.

Option B - Strings Stored in Separate DLL (Recommended):

Opaquer extracts the selected strings entirely from the managed assembly, generates C++ source code containing the strings, compiles that into a native machine-code DLL (appExtension.dll by default), and rewrites your .NET assembly to retrieve the strings via P/Invoke at runtime.

Step 5: Configure Encryption Settings

If using in-assembly encryption, you can select a cryptographic algorithm and provide a password. The password is encrypted and stored within the assembly, or you can store it in a separate file that must be deployed alongside your assembly.

For native DLL storage, Opaquer handles the encryption and compilation automatically. You can specify the output DLL filename.

Step 6: Apply Obfuscation

Click Obfuscate and save the output. For native DLL storage, appExtension.dll appears in the same output directory as your obfuscated assembly.

Before and After in ILSpy

Before Obfuscation (Unprotected)

ILSpy - Unprotected
public class AppConfiguration
{
    public const string ApiUrl = "https://api.mycompany.com/v2/data";
    public const string ConnectionString = 
        "Server=prod-sql01.internal;Database=Customers;User Id=appuser;Password=Xk9mP2qR;";
    public const string LicenseServer = "https://license.mycompany.com/verify";
    public const string ErrorMessage = "License validation failed: server returned status 403";
}

After Opaquer (In-Assembly Encryption)

The strings are replaced with encrypted representations. The decompiled output shows encoded byte arrays or ciphertext rather than readable text:

ILSpy - In-Assembly Encryption
public class AppConfiguration
{
    public const string ApiUrl = "\u0018\u0043\u00A7\u0021\u009B...";
    public const string ConnectionString = "\u000E\u0072\u00C4\u0038...";
    public const string LicenseServer = "\u00A1\u0056\u008D\u00F2...";
    public const string ErrorMessage = "\u003F\u0099\u00B1\u0004...";
}

The decryption routine is visible in the assembly, but the strings themselves are not readable.

After Opaquer (Native DLL Storage - Recommended)

The strings are completely absent from the managed assembly. In their place are P/Invoke declarations that call into appExtension.dll:

ILSpy - Native DLL Storage
public class AppConfiguration
{
    [DllImport("appExtension.dll", EntryPoint = "GetString0")]
    public static extern string GetApiUrl();
    
    [DllImport("appExtension.dll", EntryPoint = "GetString1")]
    public static extern string GetConnectionString();
    
    // ... additional P/Invoke declarations
}

Searching ILSpy for "api.mycompany.com" or "prod-sql01" returns zero results. The strings exist only in the native binary appExtension.dll, which cannot be opened by any standard .NET assembly browser. Native code is orders of magnitude harder to analyze than managed IL.

When to Choose Which Approach

Both approaches protect strings, but they serve different needs. Use this table to decide:

Aspect In-Assembly Encryption Native DLL Storage
Deployment Single managed assembly, no extra files Requires shipping appExtension.dll alongside the assembly
Strings visible in ILSpy Encrypted, unreadable Completely absent from managed assembly
Resistance to static analysis Moderate - decryption routine is visible in IL High - strings live in native machine code
Resistance to runtime analysis Low to moderate - strings appear in memory after decryption Moderate - strings still appear in memory, but retrieval is harder to trace
Best for Error messages, UI text, log formats, low-sensitivity strings API keys, connection strings, license server URLs, encryption salts
Deployment complexity Low Medium - must include native DLL in installer
Recommendation Standard Recommended for secrets
Rule of thumb: If the string would cause harm when read by an attacker, put it in the native DLL. If it merely reveals internal structure, in-assembly encryption is sufficient.

Testing the Obfuscated Application

1. Verify String Protection

Open the obfuscated assembly in ILSpy and search for each sensitive string value. Confirm that:

2. Functional Test

Run the application and exercise every code path that uses the protected strings. Confirm that API calls succeed, database connections open, and license validation works. The strings should be retrieved transparently at runtime with no behavioral change visible to the user.

3. Verify Native DLL Deployment

If using native DLL storage, confirm that appExtension.dll is present in the same directory as your application's executable. Attempt to run the application without the DLL to confirm that it throws DllNotFoundException as expected-this verifies the dependency is properly established.

4. Check for Missing Dependencies

Use dumpbin /dependents (Windows SDK) or a tool like Dependency Walker to verify that your obfuscated assembly correctly references appExtension.dll. On deployment, verify the DLL is included in your installer or distribution package.

Common Failures and Fixes

Failure: Application Throws DllNotFoundException at Startup

Likely cause: appExtension.dll is not present in the application directory.

Fix: Ensure appExtension.dll is deployed alongside your main assembly. Update your build script, installer, or deployment package to include it. There is no graceful fallback-the application cannot retrieve strings without the native DLL.

Failure: Strings Still Visible After In-Assembly Encryption

Likely cause: The strings were not selected for encryption, or the minimum length threshold excluded them.

Fix: Verify that the specific strings are checked in the Strings tab. If using a length threshold, lower it or select strings individually. Some strings may be optimized by the compiler into constant folding-check for inline usage as well.

Failure: Performance Degradation After String Encryption

Likely cause: Encrypting every string in a large assembly, including frequently-called hot paths.

Fix: Be selective. Encrypt sensitive strings (credentials, keys, URLs) rather than all strings. Opaquer's string selection interface allows precise targeting. For performance-sensitive paths, consider whether the string is truly sensitive before encrypting it.

Failure: Native DLL Architecture Mismatch

Likely cause: The generated appExtension.dll is compiled for the wrong platform (32-bit vs 64-bit) relative to your application.

Fix: Verify that your Opaquer output targets the correct platform. If you ship both 32-bit and 64-bit builds, you may need corresponding native DLLs. Consider making the native DLL architecture match your primary deployment target.

Production Recommendations

Choose the Right Storage for the Right String

Not all strings deserve the same level of protection. Use this decision framework:

String Type Recommended Protection Rationale
Database connection strings Native DLL Credentials must never appear in managed assemblies
API keys and tokens Native DLL Direct financial/security impact if extracted
License server endpoints Native DLL Prevents redirecting validation to attacker-controlled servers
Error messages In-Assembly Encryption Reveals internal logic; less critical than credentials
UI text and labels In-Assembly Encryption or None Low sensitivity; performance matters
Log format strings None or In-Assembly Rarely useful to attackers

The native DLL approach is best reserved for the most sensitive string assets rather than applied indiscriminately.

Understand the Trade-offs

In-Assembly Encryption:

Native DLL Storage:

Plan for Maintenance

String encryption is a build-time operation, not a one-time fix. Add it to your release pipeline:

  1. Build the assembly in Release configuration
  2. Run Opaquer with your saved string encryption settings
  3. Verify that sensitive strings are absent from decompiled output
  4. Confirm that appExtension.dll is included in the deployment package
  5. Test the obfuscated application end-to-end

Version your Opaquer configuration alongside your source code so protection settings remain consistent across releases.

Combine with Other Protections

String encryption addresses the most visible attack surface, but it works best in combination with:

For license validation and cryptographic code, consider High-Intensity Control Flow or code virtualization (Enterprise) in addition to string protection.

FAQ

What is the difference between in-assembly string encryption and native DLL storage?

In-assembly encryption keeps strings within the managed assembly but replaces them with encrypted representations. Native DLL storage removes strings from the managed assembly entirely, placing them in a compiled C++ binary. The native DLL approach provides stronger protection because the strings never exist in any form that .NET decompilers can inspect.

Why can't I just encrypt strings with a standard algorithm in my source code?

You can, but the decryption key must also exist somewhere in the assembly, and the decryption routine is visible in managed IL. An attacker can locate the decryption logic and extract the key. Opaquer's approach either hides the decryption in native code (DLL storage) or uses injection techniques that make the decryption routine harder to isolate.

Will string encryption break my application?

No, if configured correctly. Opaquer replaces strings with calls that retrieve the original values at runtime. The application behavior is unchanged. However, if you use string literals in contexts that require compile-time constants (like attribute parameters or switch case labels), those specific strings cannot be encrypted and will remain visible.

Do I need to ship appExtension.dll if I use in-assembly encryption?

No. In-assembly encryption keeps everything in the managed assembly. Only the native DLL storage option requires the additional file.

What happens if appExtension.dll is missing at runtime?

The application will throw DllNotFoundException and fail to start. There is no fallback mechanism-the strings simply cannot be retrieved without the native DLL. Always verify that the DLL is included in your deployment package.

Can I use both in-assembly encryption and native DLL storage in the same assembly?

Yes. You can select some strings for in-assembly encryption and others for native DLL storage. This allows you to protect critical secrets with native storage while keeping less sensitive strings in the managed assembly for simplicity.

Does string encryption affect application performance?

There is a small runtime cost for decryption. In-assembly encryption has minimal overhead. Native DLL storage adds P/Invoke calls, which introduce a small marshaling cost. For most applications, the impact is negligible. If you encrypt strings in performance-critical loops, test the impact and consider whether those specific strings need protection.

What about strings stored in configuration files or resources?

Opaquer's string encryption targets string literals embedded in IL code. Strings in .config files, .resx resources, or external JSON files are not covered. For those, use .NET's built-in configuration encryption (aspnet_regiis -pe for web.config) or move sensitive values to environment variables or a secrets manager.

Can I verify that the strings are truly protected?

Yes. After obfuscation, open the assembly in ILSpy and search for the original string values. With in-assembly encryption, they should appear as unreadable ciphertext. With native DLL storage, they should be completely absent. Also verify that the application still functions correctly with the obfuscated assembly.