Storing Secrets in Windows Apps - Avoiding Plaintext Configuration with DPAPI
· Go Komura · Windows Development, Security, DPAPI, C# / .NET, Win32
In the previous post, “A Minimum Security Checklist for Windows Application Development,” we drew the baseline: “do not put secrets in source code or plaintext configuration” and “on Win32 / .NET, use DPAPI / ProtectedData.”
This time we dig a bit deeper into one part of that: “using DPAPI to at least be better than plaintext.”
The target is Windows apps like these:
- WPF / WinForms / WinUI desktop apps
- C# / .NET Windows clients
- Apps tempted to store connection credentials or API tokens in local configuration files
What we cover here is a realistic design for “not leaving secrets that must be stored locally sitting in plaintext in appsettings.json.”
This is not a story about “perfect defense that beats any attacker.” Oversell that, and security talk quickly turns into a ghost story.
1. The Conclusion First
In practice, this order of thinking is the clearest.
- Do not give the client long-lived secrets in the first place
- Prefer Windows authentication, integrated authentication, interactive user login, and server-side secret management
- If local storage is truly necessary, do not store it in plaintext
- On Windows, make DPAPI /
ProtectedDatathe first candidate
- On Windows, make DPAPI /
- For ordinary desktop apps, default to
DataProtectionScope.CurrentUserLocalMachinehas quite limited use cases
- DPAPI does not protect you all the way to “the machine is fully compromised”
- Code running with the same user privileges can, fundamentally, decrypt whatever that user can decrypt
And the most important point of this article is here:
“The secret key has to be stored somewhere anyway, so isn’t plaintext and DPAPI the same from a security standpoint?”
This is half right, and the conclusion is wrong.
- Roll-your-own AES with the key stored in the same app or the same configuration is, indeed, close to plaintext
- But DPAPI delegates key management to the OS and binds the party that can decrypt to “that Windows user” or “that computer”
- As a result, the resilience against accidents like a leaked configuration file by itself, the file carried to another PC, mis-sent attachments, backup leaks, or repository contamination changes dramatically
In other words: even if the abstract claim “the key is somewhere” makes them look the same, “who can use it, in what context, and how easily” is completely different.
Calling “a key under the doormat” and “a key handed out at the front desk after identity verification” the same thing is a bit rough.
2. Why Plaintext Configuration Is Dangerous
The reason plaintext storage is dangerous is far more mundane than cryptographic theory. In practice, leaks happen through paths like these:
- The configuration file gets committed to Git as is
- The configuration file ends up wholesale in a troubleshooting ZIP
- A support request comes with the configuration file attached
- Third parties can read it via backups or file shares
- Connection strings and tokens appear verbatim in logs
- A departed employee or another user can read the file on the same machine
The biggest problem with plaintext is that the moment it is read, it is over as a secret.
- Open the file, and it is over
- Copy it, and it is over
- Attach it to an email, and it is over
- Leave it in a repository, and you babysit it semi-permanently
The attacker does not even need to be sophisticated. “Opens in a text editor” is, by itself, quite weak.
3. The Answer to “The Key Is Stored Somewhere Anyway, So Isn’t It the Same?”
This question is reasonable. And answering it sloppily is how security articles suddenly go vague.
The answer: yes in the sense that “a key is needed somewhere”; no in the sense that “therefore they are the same.”
3.1. What Is the Same and What Is Different
Indeed, encryption ultimately needs some root of trust. Secrets do not spring for free from somewhere in the universe. It is a harsh world that way.
However, the security difference is determined by these three points:
- Does the app hold the key directly?
- What party is the key bound to?
- If only the file is stolen, can it be decrypted?
A rough table of the differences:
| Method | Config file is read | Only the file is carried to another PC | Read by another user on the same PC | Code running with the same user privileges |
|---|---|---|---|---|
| Plaintext | Leaks on the spot | Leaks as is | Leaks as is | Readable, of course |
| Roll-your-own crypto + key in the same config / binary | Mostly leaks | Mostly leaks | Mostly leaks | Decryptable, of course |
DPAPI + CurrentUser |
Not immediately readable from the file alone | Normally hard to decrypt | Normally hard to decrypt | Decryptable |
DPAPI + LocalMachine |
Not immediately readable from the file alone | Normally hard to decrypt anywhere but that PC | Broadly decryptable on the same PC | Decryptable |
The important point here is that DPAPI separates “can read the file” from “can use the secret.”
With plaintext, these two are the same thing. If the file can be read, so can the secret.
But with DPAPI — at least with CurrentUser — decryption must happen:
- as that Windows user
- in that Windows context
- through the OS protection machinery
At an actual incident scene, this difference is substantial.
3.2. “But the Same User Can Still Decrypt It, Right?” — Correct
This is a point that should be written without hedging.
Code executed with the same user privileges can, fundamentally, decrypt whatever that user can decrypt.
In other words, DPAPI is not primarily aimed at situations like:
- The machine is already compromised by malware
- The attacker can execute code as that user
- The machine has been fully taken over at the administrator level
In those situations, since the app itself can decrypt, the attacker’s code can decrypt too. “But it is encrypted” is not very reassuring there.
Where DPAPI helps is mainly the “file leak / misplacement / offline exfiltration / access by another user” side.
Get this backwards and you get both:
- underestimating what it can protect and not using it
- overestimating what it cannot protect and feeling safe
Both are quietly dangerous.
3.3. So What Is the Actual Benefit?
The benefit of DPAPI in one sentence:
“You can decouple the secret itself from the readability of the configuration file.”
For example, plaintext and DPAPI differ in accidents like these:
- A user sent the configuration file to support
- The configuration file ended up in a troubleshooting ZIP
- Only the configuration file leaked from a backup
- It got copied onto a shared folder
- Developers could look at only ciphertext and not read the contents
These are quite realistic benefits. You can shrink the everyday accident radius without casting the attacker as a movie superhuman.
4. Why DPAPI Is Just Right
When handling locally stored secrets on Windows, the reasons DPAPI hits the practical sweet spot are as follows.
4.1. Key Management Can Be Delegated to the OS
Generate an AES key yourself, store it, set permissions on it, rotate it, think through the blast radius of a leak, and add tamper detection. This is heavier than it looks. And done sloppily, it usually ends with the key placed in the same location.
With DPAPI, the “how do we create the encryption key and where do we put it” problem can be removed from the app implementation.
In that sense, it is closer to the essence to see DPAPI as “an API for delegating key management to the OS,” not “an API for choosing an encryption algorithm.”
4.2. The Decrypting Party Can Be Bound to a Windows User or the Computer
For an ordinary desktop app, choosing CurrentUser is often the right call.
Decryption is predicated on:
- that user being logged on
- the operation running in that user’s context
As a result, you get the property that copying just the ciphertext to another PC does not make it readily usable.
4.3. Tamper Detection Comes Along Easily
A common failure with roll-your-own crypto is declaring “we encrypted with AES, done” and forgetting tamper detection.
DPAPI also provides integrity protection over the encrypted data, so detecting when someone rewrites the ciphertext can ride on the OS-side machinery — a practical advantage.
4.4. Straightforward to Use from C# / .NET
In C#, you can use System.Security.Cryptography.ProtectedData directly.
Not having to add extra libraries is a real help for Windows-only apps.
5. What DPAPI Protects, and What It Does Not
It is safer to draw this line clearly.
5.1. What Becomes Easier to Protect
DPAPI is effective at least in scenarios like these:
- Plaintext leakage of configuration files
- Files carried off to another PC
- Access by another user on the same PC (assuming
CurrentUser) - Leakage via backups or attachments
- The “oops, anyone can read this” state in development and maintenance settings
5.2. What It Cannot Protect, or Protects Weakly
On the other hand, do not over-trust it in these situations:
- Attack code running with the same user privileges
- Full compromise of the machine itself
- Takeover with administrator privileges
- The plaintext in memory after the app decrypts
- Long-lived secrets distributed identically to all clients
The last one — “a long-lived secret shared by all clients” — is especially important.
For example, designs like:
- embedding the same API key for all customers
- having the same shared password on all machines
- shipping a fixed decryption key that lives entirely on the client
tend to be designs where extraction from any single machine ripples out to everything.
DPAPI is effective at “making that storage location better than plaintext,” but it does not justify secrets that should not be on the client in the first place.
6. Choosing Between CurrentUser and LocalMachine
This part matters a lot. Choose carelessly and the meaning changes.
6.1. The Default Is CurrentUser
For an ordinary Windows desktop app, start from CurrentUser.
Good fits:
- User-facing WPF / WinForms / WinUI desktop apps
- Apps with per-user settings and credentials
- Apps that keep settings under
%LocalAppData%or%AppData%
In this case, it becomes natural to treat the data as “that Windows user’s secret.”
6.2. LocalMachine Has Quite Limited Use Cases
LocalMachine looks convenient, but for an ordinary desktop app it is too broad.
It is suited to cases like:
- A Windows service on a trusted, single-purpose machine
- Secrets used only by specific processes on that machine
- Cases that genuinely must work across logon users on the same machine
But the caveats are heavy.
- Broadly decryptable by processes running on that PC
- Tends to become dangerous on shared machines, RDS, jump hosts, and multi-user environments
- Choosing it because “everyone can use it, so it is easy” usually causes trouble later
6.3. When in Doubt, Think Like This
- Ordinary UI app ->
CurrentUser - A special case that truly must be protected per machine ->
LocalMachine - Any user must be able to decrypt, but other users are on the machine -> usually better to revisit the design itself
6.4. Services and Impersonation Add Caution
Bring Windows services or impersonation into the mix, and the meaning of CurrentUser gets a bit heavier.
- Who is the execution account?
- Is that account’s profile loaded?
- In which context does decryption happen?
If these get misaligned, you easily end up with “it encrypted fine but will not decrypt.”
For service scenarios, “just use CurrentUser” is sometimes not enough.
7. Minimum Implementation Guidelines
If all you want is to “stop having plaintext in the configuration file” in a Windows app, the design does not need to be very complex. But there are a few points you do not want to miss.
7.1. Protect Only the Secrets
Rather than encrypting the entire configuration wholesale, it is easier to handle if you protect only the secret items first.
For example, split it like this. These can often stay in plaintext:
- Server URL
- Username
- Database name
- Feature flags
Whereas these are protection targets:
- Passwords
- API tokens
- Refresh tokens
- Shared folder credentials
With this split, you get:
- easier configuration editing
- easier diff review
- clarity about what is secret
- simpler overall operations
7.2. Default the Storage Location to Per-User
For an ordinary desktop app, default the storage location to a per-user place.
%LocalAppData%\Vendor\App\settings.json%AppData%\Vendor\App\settings.json
At minimum, do not casually put it under the installation folder or somewhere easily shared.
Even with DPAPI protection, if the ACLs on the storage location are sloppy, you end up with “the ciphertext gets read,” “the configuration structure is visible,” “operational mistakes happen.” Defense works better layered, not single-tiered.
7.3. optionalEntropy Is Not an Almighty Second Key
ProtectedData lets you pass optionalEntropy.
This is handy, but it is not “a magic second key that makes you safe if you embed it in the binary.”
- Put it in the same file and it is not a secret
- Embed it as a fixed value in the binary and it is not a strong secret either
- Even so, it is useful for purpose identification and misuse prevention
In practice, passing a fixed byte sequence built from:
- the application name
- the purpose name
- a version identifier
and using it “to avoid accidentally accepting ciphertext from a different purpose” is about the right level.
7.4. This Does Not Mean Ciphertext May Go into Git
This is quietly important too.
DPAPI ciphertext is far better than plaintext, but that does not mean the configuration file may go into the repository.
The reasons are simple:
- Ciphertext lives a long time
- The same machine or the same context may someday be reproduced
- The file contains information besides the secret
- A culture of “it is protected, so we can be sloppy with it” takes root
“Better than plaintext” and “safe anywhere you put it” are entirely different things.
7.5. Do Not Put It in Logs
A surprisingly common pattern is ruining everything by logging the value after decryption.
- Dumping the entire connection string on connection failure
- Leaving the Authorization header in logs on an API 401
- Mixing secrets into exception messages
Do these, and even after eliminating plaintext configuration files, your logs become the plaintext warehouse instead. Sad, but very much real-world.
8. A Minimal C# / .NET Implementation
Below is a minimal example protecting a string saved to a configuration file with CurrentUser.
It includes a fixed optionalEntropy for purpose identification — do not think of this as a secret key.
using System;
using System.Security.Cryptography;
using System.Text;
public static class DpapiSecretProtector
{
// For purpose identification. Not a second secret key.
private static readonly byte[] Entropy =
Encoding.UTF8.GetBytes("ComComponent:DesktopApp:SettingsSecret:v1");
public static string ProtectToBase64(string plaintext)
{
ArgumentNullException.ThrowIfNull(plaintext);
byte[] plainBytes = Encoding.UTF8.GetBytes(plaintext);
byte[] protectedBytes = Array.Empty<byte>();
try
{
protectedBytes = ProtectedData.Protect(
plainBytes,
optionalEntropy: Entropy,
scope: DataProtectionScope.CurrentUser);
return Convert.ToBase64String(protectedBytes);
}
finally
{
Array.Clear(plainBytes, 0, plainBytes.Length);
if (protectedBytes.Length > 0)
{
Array.Clear(protectedBytes, 0, protectedBytes.Length);
}
}
}
public static string UnprotectFromBase64(string protectedBase64)
{
ArgumentNullException.ThrowIfNull(protectedBase64);
byte[] protectedBytes = Convert.FromBase64String(protectedBase64);
byte[] plainBytes = Array.Empty<byte>();
try
{
plainBytes = ProtectedData.Unprotect(
protectedBytes,
optionalEntropy: Entropy,
scope: DataProtectionScope.CurrentUser);
return Encoding.UTF8.GetString(plainBytes);
}
finally
{
Array.Clear(protectedBytes, 0, protectedBytes.Length);
if (plainBytes.Length > 0)
{
Array.Clear(plainBytes, 0, plainBytes.Length);
}
}
}
}
Usage is simple.
string protectedPassword = DpapiSecretProtector.ProtectToBase64(password);
// Save to JSON etc.
// settings.DbPasswordProtected = protectedPassword;
string password = DpapiSecretProtector.UnprotectFromBase64(settings.DbPasswordProtected);
The configuration file can then take a form like this.
{
"ApiBaseUrl": "https://api.example.com/",
"UserName": "app-user",
"PasswordProtected": "AQAAANCMnd8BFdERjHoAwE..."
}
The nice things about this form:
- The URL and username can be edited normally
- Only the password is protected
- The configuration structure is easy to read
- Less accident-prone than leaving it in plaintext
9. Designs That Are Still Dangerous
Even with DPAPI in use, the following designs are still dangerous.
9.1. Carrying the Decrypted Value Around for a Long Time
You want to avoid taking the decrypted value and:
- putting it in logs
- putting it on screen
- including it in exceptions
- leaving it parked on long-lived objects
“Encrypted at rest” and “safe while in use” are separate problems.
9.2. Giving Every Installation a Common Secret
For example, a design where every user holds the same API key is not fundamentally fixed by storing it with DPAPI.
Because if the app can decrypt it on any single machine, the secret can be extracted there.
Secrets of this kind should be steered toward:
- keeping them on the server side
- having the client hold only tokens
- per-user credentials
- expiring tokens
9.3. Choosing LocalMachine Because “It Is Easy”
This one really is common.
You are tempted to choose LocalMachine because:
- it still reads after switching users
- services can read it too
- it is convenient if it just works
But for an ordinary desktop app, it extends decryptability to other processes on that PC, which changes the meaning considerably.
9.4. Adding Roll-Your-Own Crypto and Feeling Safe
Instead of DPAPI, implementations like:
- embedding an AES key in the source code
- putting the AES key in another field of the configuration file
- treating a “slightly obfuscated string” as a key
are usually of little effect.
Between “not plaintext” and “secure” lies a rather large gulf.
10. Cases Where DPAPI Is Not Enough
DPAPI is useful, but not almighty. Consider other options in the following cases.
10.1. You Want to Run on Platforms Other Than Windows
DPAPI / ProtectedData are for Windows.
A cross-platform app cannot be built on that assumption.
10.2. You Want the Same Secret Across Multiple Machines and Users
Requirements like decrypting the same ciphertext on multiple PCs, or sharing it across multiple users, fall outside the strength of DPAPI, which binds to “that machine, that user.”
In that case, consider designs that fit the requirement:
- server-side secret management
- a credential infrastructure
- Windows authentication / integrated authentication
- a credential store for the application
10.3. The Thing Being Stored Is a User Credential Itself
For packaged desktop apps / WinUI, if the thing being stored is clearly a pair of:
- username
- password
then the Credential Locker is also an option. But the focus of this article remains the practical DPAPI baseline for “stopping plaintext configuration files on Windows clients.”
11. Recommended Priorities in Practice
Finally, when in doubt in practice, thinking in this order keeps things organized.
Priority 1: Do not hold it at all
- Windows authentication
- Integrated authentication
- Interactive login
- Server-side secret keeping
- Short-lived tokens
Priority 2: Move toward per-user secrets
- Per-user over shared secrets
- Renewable tokens over long-lived fixed credentials
- Avoid keys common to all clients
Priority 3: If local storage is needed, DPAPI
- Normally
CurrentUser - Per-user storage location
- Protect only the secret items
- Keep it out of logs
Priority 4: Treat LocalMachine as the exception
- Does it truly need to be per machine?
- Do other users ever log on to that machine?
- Is it sound as a service design?
12. Summary
When a Windows app must store sensitive information in a configuration file, you want to avoid leaving it in plaintext.
And to the question:
“The key gets stored somewhere anyway, so isn’t it the same?”
the practical answer is this:
- Roll-your-own crypto with the key in the same place is, mostly, the same
- DPAPI is not the same
- Key management can be delegated to the OS
- The decrypting party can be bound to a Windows user / computer
- A leak of the file by itself no longer equals a leak of the secret
- However, it does not solve:
- code running with the same user privileges
- a fully compromised machine
- long-lived shared secrets that should not be on the client in the first place
In short, DPAPI is not an almighty fortress wall. But it does have roughly the effect of replacing the wide-open window pane that is a plaintext configuration file with at least a decent window.
In real-world Windows client work, that difference is substantial. Starting by getting this part right is the most realistic move.
13. References
- Previous article: https://comcomponent.com/en/blog/2026/03/14/001-windows-app-security-minimum-checklist/
- Microsoft Learn:
CryptProtectDatahttps://learn.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectdata - Microsoft Learn:
ProtectedDatahttps://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.protecteddata?view=windowsdesktop-10.0 - Microsoft Learn:
DataProtectionScopehttps://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.dataprotectionscope?view=windowsdesktop-10.0 - Microsoft Learn: How to: Use Data Protection https://learn.microsoft.com/en-us/dotnet/standard/security/how-to-use-data-protection
- Microsoft Learn: Credential locker for Windows apps https://learn.microsoft.com/en-us/windows/apps/develop/security/credential-locker
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
How to Concretely Isolate "Only the Operations That Need Administrator Privileges" in a Windows App
A concrete walkthrough of keeping a Windows app UI at asInvoker while isolating only the administrator-privileged operations into a helpe...
A Minimum Security Checklist for Windows App Development
A checklist-style guide to the security basics for WPF / WinForms / WinUI / C++ / C# business apps: privileges, signing, updates, secrets...
Why Windows Became What It Is Today: The Evolution of Windows Through a Developer's Eyes
A look at the changes from Windows 95 to Windows 11 — not as a visual timeline, but from a Windows application developer's perspective: c...
Handling Windows Impersonation Tokens Correctly — Borrowing Privileges per Thread and Reverting Safely
A practical guide to Windows impersonation tokens — access tokens, primary tokens, thread tokens, impersonation levels, RevertToSelf, and...
Why Windows Shows "Windows protected your PC"
A practical look at why SmartScreen warnings appear when distributing Windows apps, covering code signing, EV/OV certificates, Azure Arti...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Where This Topic Connects
This article connects naturally to the following service pages.
Windows App Development
This topic touches the overall design of a Windows app — how credentials are stored, where per-user settings live, and what goes into logs — so it fits well with our Windows application development service.
Technical Consulting & Design Review
If you want to start by reviewing plaintext configuration in an existing app or sorting out when to use DPAPI versus the Credential Locker, this topic works well as a technical consulting / design review engagement.
Author Profile
Profile page for the article author.
Go Komura
Representative of KomuraSoft LLC
Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.
Public links