Storing Secrets in Windows Apps - Avoiding Plaintext Configuration with DPAPI

· Updated: · · Windows Development, Security, DPAPI, C# / .NET, Win32

Revision history (1 updates, last updated Sep 1, 2026)

A log of the changes made to this article. Where a pre-update version was archived, it stays readable at a permanent DOI link.

Retranslated as a full translation of the Japanese original. The previous English version was an abridgement that carried only part of the source, so sections, tables, Mermaid diagrams, figure captions and FAQ entries were missing. All of them have been restored to match the Japanese original, and the technical claims are the same as in the Japanese version. Read the version before this update (DOI: 10.5281/zenodo.21614505)
First published
Cite this article(DOI: 10.5281/zenodo.21614504)

This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.

Go Komura (2026). Storing Secrets in Windows Apps - Avoiding Plaintext Configuration with DPAPI. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21614504 https://comcomponent.com/en/blog/2026/03/16/000-windows-app-secret-storage-best-practices-dpapi/

DOI (latest version)
10.5281/zenodo.21614504
DOI (this version)
10.5281/zenodo.22217148

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 it, and the discussion drifts away from any realistic conversation about security.

1. The Conclusion First

In practice, this order of thinking is the clearest.

  1. 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
  2. If local storage is truly necessary, do not store it in plaintext
    • On Windows, make DPAPI / ProtectedData the first candidate
  3. For ordinary desktop apps, default to DataProtectionScope.CurrentUser
    • LocalMachine has quite limited use cases
  4. 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 the same as DPAPI from a security standpoint?”

This is half right, and the conclusion is wrong.

  • Roll-your-own AES with the key placed in the same app or the same configuration is, indeed, close to plaintext
  • But DPAPI moves key management onto the OS and binds the party that can decrypt to “that Windows user” or “that computer”
  • As a result, how well it holds up against mishaps like a leak of the configuration file by itself, the file carried to another PC, a mis-sent attachment, a backup leak, 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 left under the doormat and a key handed over at the front desk after an identity check the same thing is a bit rough.

The key is somewhere is not enough to make them the sameA diagram showing the central point of this article - at the abstract level of the key being somewhere, plaintext and DPAPI look identical, but who can use it, in what context, and how easily is completely different.Looking only hereLooking hereThe key is somewhere (abstract view)They look the sameWho can use it, in what context, how easilyCompletely different

Figure 1: At the abstract level they look alike, but who can use the key and in what context is what separates plaintext from DPAPI.

In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (21 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle

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

Plaintext loses its secrecy the moment a third party reads it.

  • 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.

Plaintext loses its secrecy the moment it is readA diagram showing that once a configuration file is read by a third party through mundane paths such as Git contamination, a troubleshooting ZIP, a backup leak, or an attachment, plaintext loses its secrecy.Git contamination, troubleshooting ZIP, attachment, backupThe configuration file gets readSecrecy is lost the moment a third party reads itThe attacker does not even need to be sophisticated

Figure 2: The weakness of plaintext is that secrecy is lost the moment a third party reads it through an everyday mishap path.

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. Put another way, encryption always ends up resting on some anchor of trust.

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.”

What decryption needs - stays on the OS side and is not contained in the ciphertextProtected with CurrentUserProtected with LocalMachineUser master keywhen protected with CurrentUserComputer master keywhen protected with LocalMachineCiphertextends up in config files / DB columns / troubleshooting ZIPs= what can be carried awayCode running as that user-> can decryptAnother user on the same PC-> cannot decryptCode on the same PC-> another user can decrypt tooCopy only the ciphertext to another PC-> cannot decryptMove the whole roaming profile-> the key material moves with it, so it can decrypt (Section 6.5)

Figure 3: Only the ciphertext sits on the side that can be carried away; the master key that decryption needs stays on the OS side. The reach of LocalMachine, however, is the whole PC, so it cannot shut out another user on the same machine

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.

Where DPAPI helps and where it does notA diagram drawing the line between what DPAPI covers - file leaks, misplacement, offline exfiltration, and access by another user - and what it does not cover, namely attack code running with the same user privileges and an already compromised machine.Helps hereDoes not help hereWhat DPAPI coversLeaks, misplacement, another userCode with the same user privilegesGet the line wrong and you misjudge it

Figure 4: Without knowing the line between where it helps and where it does not, you either underestimate it and skip it, or overestimate it and relax.

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, in mishaps like these, plaintext and DPAPI come out very differently:

  • 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 blast radius without casting the attacker as a movie superhuman.

How the blast radius shrinksA diagram showing that even when everyday mishaps such as a mis-sent support attachment, a troubleshooting ZIP, or a backup leak push a file outside, what leaves is only ciphertext, so it does not become a leak of the secret and the blast radius shrinks.Mis-sent attachment, troubleshooting ZIP, backup leakOnly the ciphertext leavesIt does not have to become a leak of the secretThe everyday blast radius shrinks

Figure 5: You cannot prevent files from leaving, but you can change what leaves into ciphertext.

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 on top. 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.”

The essential way to view DPAPIA diagram showing that DPAPI is closer to the truth when viewed not as an API for choosing an encryption algorithm but as an API that delegates to the OS the key management question of how to create a key and where to put it.Not this viewThis view is closer to the essenceAn API for choosing an algorithmDPAPIAn API that delegates key management to the OSKey creation and storage can be removed from the implementation

Figure 6: Viewing DPAPI as the place key management is delegated to makes it clear what the app can stop owning.

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, which is a practical advantage.

The difference in tamper detectionA diagram showing that roll-your-own crypto tends to stop at encrypting with AES and forget tamper detection, whereas DPAPI carries integrity protection over the encrypted data so that detecting a rewritten ciphertext can ride on the OS-side machinery.Roll-your-own cryptoTamper detection is easily forgottenDPAPICarries integrity protectionDetecting a rewrite also rides on the OS side

Figure 7: Not just encryption but tamper detection too can ride on the OS-side machinery, and that is the advantage of DPAPI.

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 work

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. The logic is simple: if the app can decrypt it on any single machine, that secret can be extracted.

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.

How a shared long-lived secret ripples outA diagram showing that when a long-lived secret is handed identically to every client, the secret can be extracted from any single machine where the app can decrypt it, so a breach of one machine ripples out to everything.A long-lived secret shared by all clientsThe app can decrypt it on some machineThe secret can be extracted from that one machineIt ripples out to everythingStoring it with DPAPI is not a fundamental fix

Figure 8: A shared secret means one fallen machine ripples out to everything, so revisit where it lives rather than how it is stored.

Secrets of this kind are better steered in the following directions than stored more cleverly:

  • keep them on the server side
  • have the client hold only tokens
  • make them per-user credentials
  • make them expiring tokens

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

And the motives for reaching for LocalMachine are usually these three:

  • it can still be read after switching users
  • services can read it too
  • it is convenient if it just works

Every one of them is about “easy,” not about “protected.” Choosing LocalMachine in an ordinary desktop app extends decryptability to other processes on that PC, which changes the meaning considerably.

What happens when LocalMachine is chosen for convenienceA diagram showing that choosing LocalMachine for the convenience of reading across users and from services extends decryptability to other processes on that PC, which is a different state from being protected.Easy across users and from servicesChoose LocalMachineDecryptability extends to other processes on the PCThat is easy, not protected

Figure 9: Choosing LocalMachine for convenience widens the decryptable range to the whole PC.

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.

With impersonation, the typical failure, spelled out in Microsoft Learn as well, is “Key not valid for use in specified state.” DPAPI keeps its key data in the user profile, so decryption fails when the profile is not loaded. You have to load the target user’s profile before impersonating.

The typical failure with impersonationA diagram showing that because DPAPI keeps its key data in the user profile, impersonating and decrypting while the profile is not loaded produces the typical error, and the target user profile has to be loaded first.Impersonate without the profile loadedDecryption fails with an errorLoad the profile firstDecryption works after impersonatingThe key lives in the profile

Figure 10: The key sits on the profile side, so the profile has to be loaded before impersonating.

6.5. Know the Cases Where Decryption Stops Working

In practice, losing the ability to decrypt hurts more than the encryption itself. DPAPI binds the party that can decrypt to a Windows user or a computer, so once that binding is broken, the data becomes unreadable.

These are the five cases worth knowing up front:

Case What happens How to prepare
An administrator resets the password The protection tied to the user’s password comes off, and DPAPI-protected data can become inaccessible. Microsoft support documents this as data protected by DPAPI becoming inaccessible after an administrator resets the password Design so the secret can be re-acquired. Route a decryption failure into re-entry
The profile is recreated A new profile carries different key material, so the earlier ciphertext cannot be decrypted Version the configuration file, and do not turn a decryption failure into an abnormal termination
Only the ciphertext is copied to another PC The key material decryption needs lives on the user profile side, so carrying only a CurrentUser ciphertext gets you nothing (this is the flip side of the “strength” listed in 3.1.) Design on the assumption that the secret is stored again per machine
Roaming profile This one can be read. The key material moves with the profile, and Microsoft Learn states explicitly that a user with a roaming profile can decrypt from a different computer on the network. Treat it the same as the previous row and your migration procedure will recreate credentials for no reason Do not assume “another PC means it cannot be read.” Confirm whether roaming is in play before deciding the migration procedure
The service execution account changes If the execution account differs between protection time and decryption time, it cannot be read with CurrentUser Build a re-protection step into operations for account changes

In short, write the code on the premise that ProtectedData.Unprotect can fail. A failed decryption throws CryptographicException, so catch it there and steer toward re-entry.

using System;
using System.Security.Cryptography;
using System.Text;

// protectedBase64: the ciphertext read from the configuration file (Base64)
// entropy: pass the same value used at protection time. null is acceptable
static bool TryUnprotect(string protectedBase64, byte[]? entropy, out string plaintext)
{
    plaintext = string.Empty;

    try
    {
        byte[] plainBytes = ProtectedData.Unprotect(
            Convert.FromBase64String(protectedBase64),
            optionalEntropy: entropy,
            scope: DataProtectionScope.CurrentUser);

        plaintext = Encoding.UTF8.GetString(plainBytes);
        return true;
    }
    catch (CryptographicException)
    {
        // Cannot decrypt = the environment most likely changed.
        // Do not crash here; let the caller route into the re-entry flow
        return false;
    }
    catch (FormatException)
    {
        // When the value is not valid Base64
        return false;
    }
}

When a support request arrives saying “we encrypted it but cannot decrypt it,” it is usually one of the rows in that table.

A code flow that assumes decryption can failA diagram showing the flow of writing code on the premise that ProtectedData.Unprotect can fail when the environment changes, catching CryptographicException instead of terminating abnormally, and steering into the re-entry flow.SuccessFails with an exceptionTry UnprotectUse the secretCatch the exception and do not crashSteer into the re-entry flowIt can fail once the binding is broken

Figure 11: Write on the premise that decryption can fail, and connect a failure to re-entry rather than to an abnormal termination.

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.

  • Server URL
  • Username
  • Database name
  • Feature flags

These can often stay in plaintext.

Whereas:

  • Passwords
  • API tokens
  • Refresh tokens
  • Shared folder credentials

are the protection targets.

With this split, you get:

  • easier configuration editing
  • easier diff review
  • clarity about what is secret
  • simpler overall operations
Splitting so that only the secret items are protectedA diagram showing that instead of encrypting the whole configuration wholesale, leaving URLs and usernames in plaintext and protecting only secret items such as passwords and tokens keeps the file editable and makes clear what is secret.Stays in plaintextProtectedConfiguration fileURL, username, flags, and so onPasswords, tokens, and so onWhat is secret is clear and operations stay simple

Figure 12: Protecting only the secret items rather than encrypting everything keeps both ease of handling and safety.

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.

Where optionalEntropy actually belongsA diagram showing that optionalEntropy is not a magic second key that becomes safe once embedded in the binary, but is best used as purpose identification, passing the application name and purpose name as a fixed byte sequence so ciphertext from a different purpose is not accepted by mistake.Do not expect thisThis use is about rightoptionalEntropyA magic second keyAn identifier for the app name and purposeCiphertext from a different purpose is not accepted by mistake

Figure 13: Entropy is not a secret key but a tag for purpose identification and misuse prevention.

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.

Why even ciphertext stays out of GitA diagram showing that DPAPI ciphertext is better than plaintext but still stays out of the repository, because it lives a long time, the file carries information besides the secret, and a culture of being sloppy because it is protected takes root, which is a separate matter from being safe anywhere.Better than plaintextEven soDPAPI ciphertextHolds up better when things go wrongKeep it out of the repositoryIt lives long, carries other information, and loosens the culture

Figure 14: Better than plaintext is not a reason to be careless about where it lives.

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

8.1. First, You Need to Add a Reference

ProtectedData looks like it is part of the BCL, but where it comes from depends on the target framework. Trip over this and the type name ProtectedData will not resolve.

Target What you have to do Provided by
.NET Framework Add a System.Security assembly reference to the project System.Security.dll
.NET Core / .NET 5 and later (including .NET 6 / 8) Add the NuGet package System.Security.Cryptography.ProtectedData System.Security.Cryptography.ProtectedData.dll

This package is not included in any shared framework of .NET Core / .NET 5 and later. Even on a Windows-targeting TFM such as net8.0-windows, the reference has to be explicit.

dotnet add package System.Security.Cryptography.ProtectedData

There is one more thing worth knowing before you start implementing. ProtectedData is Windows-only. It depends on DPAPI, so calling it on .NET on a non-Windows platform throws PlatformNotSupportedException. If your codebase assumes cross-platform, design differently from the start, as 10.1. describes.

What to confirm before using ProtectedDataA diagram showing the two things to confirm before implementing - a reference matching the target has to be added or the type name will not resolve, and ProtectedData is Windows-only so calling it elsewhere raises an exception.Without itCalled outside WindowsWant to use ProtectedDataAdd the reference that matches the targetThe type name does not resolveUnderstand that it is Windows-onlyThrows at run time

Figure 15: Before implementing, nail down the two prerequisites: adding the reference, and Windows-only.

8.2. The Minimal Implementation

Below is a minimal example protecting a string saved to a configuration file with CurrentUser. It includes a fixed optionalEntropy for purpose identification, but 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
  • Fewer things go wrong 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

A design where every user holds the same API key is not fundamentally fixed by storing it with DPAPI. The reasons, and where to steer such secrets instead, are collected in 5.2.

9.3. Choosing LocalMachine Because “It Is Easy”

This one really is common. But “easy” is not “protected.” The motives for reaching for it, and what widens when you do, are covered in 6.2. If the call is hard to make, look at the three lines in 6.3.

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.

The danger of feeling safe with roll-your-own cryptoA diagram showing that roll-your-own crypto that puts an AES key in the source code or in another configuration field, or treats an obfuscated string as a key, has little effect, and that a large gulf separates not being plaintext from being secure.A large gulf between themRoll-your-own crypto with the key in code or configA state of not being plaintextA state of being secureThe effect is usually slight

Figure 16: Roll-your-own crypto with the key in the same place is only “not plaintext” and never reaches “secure.”

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

If what you want to store is clearly a pair of:

  • username
  • password

then a credential store Windows already provides is more straightforward than writing it to your own file with DPAPI. This is a frequent point of hesitation in practice, so here is a comparison.

Aspect DPAPI (ProtectedData) Credential Locker (PasswordVault) Credential Manager (CredWrite / CredRead)
Where it is stored A file you choose yourself (how the ciphertext is placed is up to the app) A credential store Windows manages A credential store Windows manages
What can be stored Any byte sequence (a connection string, a token, or part of the configuration) A username + password pair Credentials (a struct per credential type)
API System.Security.Cryptography WinRT Windows.Security.Credentials Win32 (wincred.h / Advapi32.dll)
Usable from a desktop app Usable as is Usable from WPF / WinForms, not just WinUI (the setup for calling WinRT APIs is required) Usable as is
Sync None Roams across machines with a Microsoft account None (a local set of user credentials)
Limits Effectively none Up to 20 entries per app. Not meant for large data Tied to the logon session of the current token
Who manages it The app (you decide the storage location and the ACLs) The OS (no storage location to design) The OS (manageable from Credential Manager in Control Panel)

Rough guidance on choosing:

  • What you store is a username + password pair, and there are few of them -> Credential Locker / Credential Manager is the first candidate. You do not have to own the storage design or the ACLs
  • What you store is not shaped like “username + password” -> connection strings, API tokens, refresh tokens, or parts of a configuration file are more straightforward on the DPAPI side. That is what this article deals with
  • You want it to carry across machines -> Credential Locker roaming works. CurrentUser in DPAPI has the opposite goal, since “it does not carry over” is precisely its advantage
  • There are many entries, or they are large -> you hit the 20-entry limit of Credential Locker. Move to your own file with DPAPI

Note that the credential stores also ride on the OS protection machinery internally, so this is not a ranking of “safer than DPAPI” or “DPAPI is inferior.” The practical approach is to choose based on the shape of what you store and whether you need roaming.

And whichever you choose, for a new app it is worth first considering passwordless options such as Windows Hello or passkeys. If you can avoid holding a long-lived password at all, that is the strongest position.

The center of this article remains the practical DPAPI baseline for “stopping plaintext configuration files on Windows clients.”

Finally, when in doubt in practice, thinking in this order keeps things organized. Work down from the top, and drop to the next level only when the conditions are not met.

YesNoNoYesThat pair, and few of themYesYesDomain / local accountNoA token or another shapeYesNoOne is enough (the user, or adedicated service account)Several accounts must beable to decryptYes, for certainNot for certainCan we avoid keeping along-lived secret on the machinePriority 1: do not hold it(Windows auth, short-lived tokens)Can the secret be splitper userCheck whether it has becomea shared key and revisit the designIs what you store a pair ofusername + password (Section 10.3)Does it need to carry across machinesMachines synced with aMicrosoft account (Section 10.3)Use Credential LockerroamingDPAPI cannot carry it over.Manage it server-side (Section 10.2)Credential Locker /Credential ManagerDoes it need to carry across machinesHow many accounts decryptthe same secretPriority 3: DPAPI + CurrentUserFor unattended runs, confirm thatthe profile is loaded (Section 6.4)Can you say for certain thatno other users log onPriority 4: DPAPI + LocalMachineTreat it as an exception and record the rationaleOther users could decrypt it too.Revisit the authentication method instead

Figure 17: The order for selecting a storage method. Look at the shape of the secret and whether roaming is needed before you get to DPAPI. Choose LocalMachine not because it is easy, but as an exception when no other option holds up

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 has to equal a leak of the secret
  • However:
    • code running with the same user privileges
    • a fully compromised machine
    • long-lived shared secrets that should not be on the client

are not solved by it

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.

Where DPAPI realistically sitsA diagram showing that DPAPI is not an almighty fortress wall but does replace the wide-open window pane of plaintext configuration with at least a decent window, and that starting there is the realistic move.Replace with DPAPIPlaintext configuration = a wide-open window paneAt least a decent windowNot an almighty fortress wallStarting here is the realistic move

Figure 18: DPAPI is a window replacement rather than a fortress wall, but in practice that difference is what counts most.

13. References

  • Previous article: /en/blog/2026/03/14/001-windows-app-security-minimum-checklist/
  • Microsoft Learn: CryptProtectData https://learn.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectdata
  • Microsoft Learn: ProtectedData https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.protecteddata?view=windowsdesktop-10.0
  • Microsoft Learn: DataProtectionScope https://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
  • Microsoft Learn: CredWrite (the Win32 API of Windows Credential Manager) https://learn.microsoft.com/en-us/windows/win32/api/wincred/nf-wincred-credwritew
  • NuGet: System.Security.Cryptography.ProtectedData https://www.nuget.org/packages/System.Security.Cryptography.ProtectedData
  • Microsoft Support: You cannot access DPAPI data after an administrator resets your password https://support.microsoft.com/en-us/topic/you-cannot-access-dpapi-data-after-an-administrator-resets-your-password-on-a-windows-server-2012-based-domain-controller-4aa890cd-12b5-fe5c-9e68-06244e70673d

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

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.

Frequently Asked Questions

Common questions about the topic of this article.

What is DPAPI?
DPAPI (Data Protection API) is the data protection mechanism Windows provides. It delegates encryption key management to the OS and binds the party that can decrypt to that Windows user or that computer. From C# / .NET you reach it through the System.Security.Cryptography.ProtectedData class without adding any extra library. It is closer to the truth to see it as an API that delegates key management to the OS than as an API for choosing an encryption algorithm. It is the realistic option for not leaving passwords and API tokens sitting in a configuration file in plaintext.
The key has to be stored somewhere anyway, so isn't plaintext the same as DPAPI?
They are not the same. Roll-your-own AES with the key placed in the same app or the same configuration file is indeed close to plaintext, but DPAPI moves key management onto the OS and binds the party that can decrypt to a Windows user or a computer. As a result, it holds up far better against mishaps such as a leak of the configuration file by itself, the file being carried to another PC, a mis-sent attachment, a backup leak, or repository contamination. The decisive difference from plaintext is that DPAPI can separate 'can read the file' from 'can use the secret'.
What does DPAPI not protect?
Code that runs with the same user privileges can, fundamentally, decrypt whatever that user can decrypt. So it does not protect a machine already compromised by malware, a machine taken over at the administrator level, or the plaintext in memory after decryption. A long-lived secret handed identically to every client is not fundamentally solved by storing it with DPAPI either, because extraction from any single machine tends to ripple out to everything. Where DPAPI helps is mainly the file-leak, misplacement, offline exfiltration, and other-user-access side.
Should I use CurrentUser or LocalMachine for DataProtectionScope?
For an ordinary Windows desktop app, CurrentUser is the default. The data can be treated as that user's secret, and you get the property that copying just the ciphertext to another PC does not make it readily usable. LocalMachine is broadly decryptable by processes running on that PC, so it easily becomes dangerous on shared machines and in environments where multiple users log on; its use cases are quite limited, mainly Windows services on trusted single-purpose machines. Choose LocalMachine because 'everyone can use it and that is easy' and you will usually regret it later.

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.

Back to the Blog