Open a .NET assembly in dnSpy or ILSpy and you don't get assembly language. You get readable C#: class names, method bodies and every string literal, more or less as you wrote them. For C# software licensing, that changes the rules. Anything you hard-code into the assembly, from an API key to an IsPro flag, should be treated as public the day you ship.
This guide explains why hard-coded secrets are so easy to pull out of .NET software, why obfuscation alone doesn't fix the problem, and how to license a C# application so the valuable parts never ship in the binary at all. It ends with a practical migration plan and the client-side protections that still matter once the secrets are gone.
Why .NET assemblies give away hard-coded secrets
C# doesn't compile to machine code. It compiles to Intermediate Language (IL) plus rich metadata describing every type, method, field and property. The runtime needs that metadata to load and JIT-compile your code, and decompilers use exactly the same information to rebuild readable source.
- Structure survives compilation. Namespaces, classes and method names are stored in metadata, so a decompiler can lay out your project almost as you wrote it.
- String literals sit in plain sight. Every literal is stored in the assembly's user-strings heap. A simple string search finds API keys, URLs and passwords without reading a line of code.
- Configuration ships alongside. Files such as
appsettings.jsonandApp.configtravel with the app as readable text, and embedded resources can be extracted with the same tools. - Editing is easy too. dnSpy can change a method body and save the assembly back, so turning
return falseintoreturn truetakes minutes.
None of this is a flaw in .NET. It is the trade-off that gives managed code reflection, JIT compilation and excellent tooling. It simply means a C# binary is the worst possible place to keep a secret, and that C# licensing has to be designed around that fact rather than against it.
What counts as a hard-coded secret?
Hard-coded secrets rarely start as a decision. They creep in: a test key that never got replaced, a connection string in a config file, a boolean that switches on the Pro edition. These are the usual suspects, and what an attacker does with each.
| In your C# app | What an attacker does | Move it to |
|---|---|---|
const string ApiKey | Finds it with a string search and reuses it | Your own backend |
| Pro feature flag | Flips false to true and saves the assembly | A server-side value read after login |
| Endpoints and config | Points them at their own server | Server-side strings |
| Premium plugin DLLs | Copies and loads them without a license | Server-side files |
| License validation code | Copies it into a key generator | The licensing server only |
| Key for a local license file | Decrypts, edits and re-encrypts the file | Nowhere: keep license state on the server |
The cost goes beyond piracy. A leaked third-party key is billed to you for every request someone else makes with it. Every install shares the same secret, so one leak compromises all of them. And rotating a hard-coded value means shipping a new release and waiting for every customer to update, while the old value keeps working in every copy already out there.
Why obfuscation doesn't solve hard-coded secrets
Obfuscators are worth using. They rename symbols, encrypt string literals and scramble control flow, which makes decompiled code far harder to follow. But they work on the client, and the client has to run.
An encrypted string must be decrypted before your code can use it, so the key and the decryption routine ship in the same assembly as the ciphertext. A determined attacker can step through that routine in a debugger, or simply wait for the value to appear in memory and dump it. Automated deobfuscators such as de4dot also undo many common obfuscation schemes without any manual work.
So obfuscation buys time. It doesn't buy secrecy. The only secret that can't be extracted from your binary is one that was never in it, which is the principle behind client-side protection vs server-side secrets.
Server-side secrets: how C# licensing should work
The pattern that holds up is the reverse of hard-coding. The assembly knows how to reach the licensing server and nothing else of value. Everything sensitive is held on the server and released only to a session that has proved it holds a valid license on an approved machine.
Hard-coded in the assembly
- Visible to any decompiler
- Same secret in every install
- Rotation needs a new release
- Still works in a cracked build
Delivered from the server
- Never shipped in the build
- Released only after a license check
- Changed in the dashboard, no rebuild
- A cracked build receives nothing
- 01
Ship a build with no secrets
The assembly contains your UI and logic, plus the code that talks to the licensing server. No API keys, no premium flags, no validation algorithm.
- 02
Open a protected session
At startup the app performs a key exchange with the server. Traffic is then encrypted and signed, so a proxy can't read it and a fake server can't answer in place of the real one.
- 03
Let the server decide
The license key and a hardware fingerprint travel inside that session. The server checks that the key is valid, unexpired, unbanned and bound to this machine. The client never makes that call.
- 04
Receive strings and files
Only then does the server release server-side strings and server-side files to the session. In qPapel Auth, secure strings stream directly to memory without disk traces, and modified local files can't read server data.
- 05
Use, then discard
Hold each value only as long as the operation that needs it. Don't log it, cache it to disk or serialise it into user settings.
With this pattern a decompiled assembly shows the shape of your licensing calls and nothing worth taking. Rules, strings, offsets and constants can be changed from the dashboard without a rebuild, so rotating a value no longer depends on customers updating. Native developers get the same model, described in C++ authentication for licensed software.
How to remove hard-coded secrets from an existing C# app
Most teams don't start from a clean slate. Moving an existing app to server-side secrets is a contained project if you take it in order.
- Look at your build the way an attacker does. Open your release assembly in ILSpy or dnSpy and search its strings for keys, tokens, URLs, passwords and connection strings. Check config files and embedded resources too.
- Classify what you find. Third-party credentials move to your own backend. Configuration, endpoints, offsets and feature switches become server-side strings. Premium modules become server-side files.
- Rotate everything that was exposed. Any secret that shipped in an earlier release must be treated as leaked. Moving it server-side protects future builds, not the copies already in circulation.
- Authenticate in
Main, beforeApplication.Run. No window should open on an unauthenticated session, and nothing valuable should be constructed before the license check passes. - Inject server values as dependencies. Pass what the server returns into the classes that need it instead of reading constants, so there is no fallback value left to find.
- Fail closed. A network failure or a failed check means the app exits, not that it carries on in a degraded mode with the full feature set.
// Before: the values ship inside every copy of the assembly.
private const string ReportsEndpoint = "https://api.example.com/v2/reports";
private const bool ProEdition = true;
// After (illustrative): nothing sensitive in the build.
// Licensing.StartSessionAsync and session.GetStringAsync are placeholders
// for your licensing SDK's calls, not qPapel Auth API names.
static async Task Main()
{
var session = await Licensing.StartSessionAsync(LoadLicenseKey());
if (!session.IsAuthenticated)
return; // fail closed
var endpoint = await session.GetStringAsync("reports-endpoint");
var app = new ReportsApp(endpoint); // injected, not hard-coded
app.Run();
}If you are moving from another licensing service at the same time, migrating from KeyAuth walks through mapping products, keys and SDK calls without downtime.
Offline use, caching and other edge cases
Server-side secrets raise practical questions the moment real customers use the app. These are the ones that come up most, and the answers that keep the design intact.
- Offline use. If customers genuinely need to work offline, allow a short window since the last successful check and keep server-dependent features unavailable until the app reconnects. An open-ended offline mode is a way to skip the server entirely.
- Caching server values. Avoid writing delivered strings or files to disk. If you must cache something, Windows DPAPI with the current-user scope protects it from other accounts and from a stolen disk, but not from the user running your app, so cache only what you could live with being read.
- Logs and telemetry. Scrub server-side values from logs, exception messages and analytics events. A secret that never ships in the binary can still leak through a verbose log file.
- Crash dumps. A full memory dump contains whatever the process held at the time. Prefer minimal dumps, and don't upload full dumps automatically.
- Development builds. Keep test credentials out of release configurations entirely, and check the final release assembly, not your debug build, before you ship.
Protecting the .NET client itself
Removing secrets takes away the prize. The client still deserves protection, because an attacker who can't steal a secret will try to share a license, patch your licensing calls or run the app under a debugger to watch what the server sends.
| Tool or technique | What it does to a .NET app | What limits it |
|---|---|---|
| dnSpy, ILSpy | Decompiles and edits assemblies | No secrets to find, plus process blacklists |
| de4dot | Strips common obfuscation | Server-side strings remove the target |
| Process Hacker, System Informer | Inspects and dumps memory | Blacklists and anti-dumping |
| Fiddler, mitmproxy | Intercepts and edits traffic | Encrypted, signed session traffic |
| Renamed or private tools | Evades name-based blacklists | qPAnalyzer runtime analysis |
| Key sharing | Runs one license on many PCs | HWID binding on the server |
In qPapel Auth, process and window blacklists, debugger bans and anti-VM checks are switched on from Remote Settings, so you can tighten them without shipping a release. HWID locking stops one key running on many machines, and qPAnalyzer flags renamed tools by how they look and behave rather than what they are called. If a build does leak, Binary Trace tracks leaked builds and detects whether customers share your files.
Keep obfuscating, too. With the secrets gone, its job changes from hiding values to hiding the structure of your licensing calls, which still raises the cost of patching them. Plans and SDK details are on the qPapel Auth product page.
Najczęstsze pytania
Ship a .NET app with nothing worth stealing
Real C# DRM isn't about hiding secrets better. It is about not shipping them at all. Authenticate first, make the license decision on the server, deliver strings and files only to that session, and keep the highest-value credentials on your own backend. Your assembly stops being the place an attacker goes looking, because there is nothing in it to find.
Start with an honest look at your current build in a decompiler. Whatever you can find in ten minutes is what an attacker will find in five, and that list is your migration plan.
Zespół qPapel ds. bezpieczeństwa
Licencjonowanie i ochrona oprogramowania