Created
February 20, 2026 14:17
-
-
Save swaters86/61823669aa99300f648fa5acd1996117 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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