Skip to content

Instantly share code, notes, and snippets.

@swaters86
Created February 20, 2026 14:17
Show Gist options
  • Select an option

  • Save swaters86/61823669aa99300f648fa5acd1996117 to your computer and use it in GitHub Desktop.

Select an option

Save swaters86/61823669aa99300f648fa5acd1996117 to your computer and use it in GitHub Desktop.
// DpapiRegistrySecrets.cs
// Works on: .NET Framework 4.8 (C# 7.x) + .NET 10 (Aspire) ON WINDOWS.
// Notes for .NET 10:
// - This uses DPAPI via ProtectedData (Windows-only). If your .NET 10 project can't resolve ProtectedData,
// add NuGet: System.Security.Cryptography.ProtectedData (Windows-only). Then it will compile/run on Windows.
//
// Usage is a few lines: SaveSecret(...), LoadSecret(...), ProtectToBase64(...), UnprotectFromBase64(...)
using System;
using System.Text;
using System.Security.Cryptography;
using Microsoft.Win32;
#if !NETFRAMEWORK
using System.Runtime.Versioning;
#endif
public static class DpapiRegistrySecrets
{
// Keep this stable once deployed. Changing breaks decryption of existing values.
private const string DefaultPurpose = "YourCompany.Connector.Secrets.v1";
// ---- Public: One-liners for app code ----
/// <summary>
/// Encrypts plaintext with DPAPI and returns Base64.
/// </summary>
public static string ProtectToBase64(string plaintext, string purpose = null, bool localMachine = true)
{
EnsureWindows();
if (plaintext == null) return null;
var entropy = PurposeToEntropy(purpose ?? DefaultPurpose);
var data = Encoding.UTF8.GetBytes(plaintext);
var scope = localMachine ? DataProtectionScope.LocalMachine : DataProtectionScope.CurrentUser;
var protectedBytes = ProtectedData.Protect(data, entropy, scope);
return Convert.ToBase64String(protectedBytes);
}
/// <summary>
/// Decrypts Base64 (DPAPI-protected) back to plaintext.
/// </summary>
public static string UnprotectFromBase64(string protectedBase64, string purpose = null, bool localMachine = true)
{
EnsureWindows();
if (string.IsNullOrWhiteSpace(protectedBase64)) return null;
var entropy = PurposeToEntropy(purpose ?? DefaultPurpose);
var protectedBytes = Convert.FromBase64String(protectedBase64);
var scope = localMachine ? DataProtectionScope.LocalMachine : DataProtectionScope.CurrentUser;
var bytes = ProtectedData.Unprotect(protectedBytes, entropy, scope);
return Encoding.UTF8.GetString(bytes);
}
/// <summary>
/// DPAPI-protects plaintext and stores it in the registry as a Base64 string.
/// </summary>
/// <param name="hive">LocalMachine recommended for services; CurrentUser for per-user apps.</param>
/// <param name="subKeyPath">e.g. @"SOFTWARE\YourCompany\Connector\Secrets"</param>
/// <param name="valueName">e.g. "CognitoClientSecret"</param>
public static void SaveSecret(
RegistryHive hive,
string subKeyPath,
string valueName,
string plaintext,
string purpose = null,
bool localMachineDpapi = true,
RegistryView view = RegistryView.Registry64)
{
EnsureWindows();
if (string.IsNullOrWhiteSpace(subKeyPath)) throw new ArgumentException("subKeyPath is required.", "subKeyPath");
if (string.IsNullOrWhiteSpace(valueName)) throw new ArgumentException("valueName is required.", "valueName");
var protectedBase64 = ProtectToBase64(plaintext, purpose, localMachineDpapi);
using (var baseKey = RegistryKey.OpenBaseKey(hive, view))
using (var key = baseKey.CreateSubKey(subKeyPath, writable: true))
{
if (key == null) throw new InvalidOperationException("Failed to create/open registry key: " + hive + "\\" + subKeyPath);
key.SetValue(valueName, protectedBase64 ?? string.Empty, RegistryValueKind.String);
}
}
/// <summary>
/// Loads a DPAPI-protected Base64 string from registry and returns plaintext (or null if missing/empty).
/// </summary>
public static string LoadSecret(
RegistryHive hive,
string subKeyPath,
string valueName,
string purpose = null,
bool localMachineDpapi = true,
RegistryView view = RegistryView.Registry64)
{
EnsureWindows();
if (string.IsNullOrWhiteSpace(subKeyPath)) throw new ArgumentException("subKeyPath is required.", "subKeyPath");
if (string.IsNullOrWhiteSpace(valueName)) throw new ArgumentException("valueName is required.", "valueName");
using (var baseKey = RegistryKey.OpenBaseKey(hive, view))
using (var key = baseKey.OpenSubKey(subKeyPath, writable: false))
{
if (key == null) return null;
var protectedBase64 = key.GetValue(valueName) as string;
if (string.IsNullOrWhiteSpace(protectedBase64)) return null;
return UnprotectFromBase64(protectedBase64, purpose, localMachineDpapi);
}
}
/// <summary>
/// Deletes a stored secret value from the registry (no-op if missing).
/// </summary>
public static void DeleteSecret(
RegistryHive hive,
string subKeyPath,
string valueName,
RegistryView view = RegistryView.Registry64)
{
EnsureWindows();
using (var baseKey = RegistryKey.OpenBaseKey(hive, view))
using (var key = baseKey.OpenSubKey(subKeyPath, writable: true))
{
if (key == null) return;
key.DeleteValue(valueName, throwOnMissingValue: false);
}
}
// ---- Internals ----
private static byte[] PurposeToEntropy(string purpose)
{
// Convert "purpose" to entropy bytes. Must be stable. (You can version purpose when you *want* a breaking change.)
return Encoding.UTF8.GetBytes(purpose ?? DefaultPurpose);
}
private static void EnsureWindows()
{
#if NETFRAMEWORK
// .NET Framework is effectively Windows-only in your scenario, but keep a guard anyway.
var p = Environment.OSVersion.Platform;
var isWindows = (p == PlatformID.Win32NT) || (p == PlatformID.Win32Windows) || (p == PlatformID.Win32S) || (p == PlatformID.WinCE);
if (!isWindows)
throw new PlatformNotSupportedException("DPAPI/Registry secrets are supported only on Windows.");
#else
if (!OperatingSystem.IsWindows())
throw new PlatformNotSupportedException("DPAPI/Registry secrets are supported only on Windows.");
#endif
}
}
/* ---------------------------
Example usage (few lines)
---------------------------
.NET Framework 4.8 Windows Service (LocalMachine DPAPI + HKLM):
--------------------------------------------------------------
var keyPath = @"SOFTWARE\YourCompany\Connector\Secrets";
// Save once (installer/enrollment/admin tool typically runs elevated)
DpapiRegistrySecrets.SaveSecret(
hive: RegistryHive.LocalMachine,
subKeyPath: keyPath,
valueName: "CognitoClientId",
plaintext: "abc123-client-id",
purpose: "YourCompany.Cognito.ClientId.v1",
localMachineDpapi: true);
DpapiRegistrySecrets.SaveSecret(
hive: RegistryHive.LocalMachine,
subKeyPath: keyPath,
valueName: "CognitoClientSecret",
plaintext: "super-secret",
purpose: "YourCompany.Cognito.ClientSecret.v1",
localMachineDpapi: true);
// Load at runtime
string clientId = DpapiRegistrySecrets.LoadSecret(RegistryHive.LocalMachine, keyPath, "CognitoClientId", "YourCompany.Cognito.ClientId.v1", true);
string clientSecret = DpapiRegistrySecrets.LoadSecret(RegistryHive.LocalMachine, keyPath, "CognitoClientSecret", "YourCompany.Cognito.ClientSecret.v1", true);
.NET 10 Aspire API/Worker running on Windows (same calls):
---------------------------------------------------------
var keyPath = @"SOFTWARE\YourCompany\Connector\Secrets";
var secret = DpapiRegistrySecrets.LoadSecret(
RegistryHive.LocalMachine,
keyPath,
"CognitoClientSecret",
purpose: "YourCompany.Cognito.ClientSecret.v1",
localMachineDpapi: true);
(If you only want encryption/decryption without registry:)
---------------------------------------------------------
var b64 = DpapiRegistrySecrets.ProtectToBase64("hello", purpose: "demo.v1", localMachine: true);
var plain = DpapiRegistrySecrets.UnprotectFromBase64(b64, purpose: "demo.v1", localMachine: true);
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment