Created
February 20, 2026 14:26
-
-
Save swaters86/691c8423bed217909f443027518a6e62 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.Net48.cs | |
| // Target: .NET Framework 4.8 (C# 7.x) (Windows only) | |
| // No NuGet needed for DPAPI on .NET Framework. | |
| using System; | |
| using System.Security.Cryptography; | |
| using System.Text; | |
| using Microsoft.Win32; | |
| public static class DpapiRegistrySecrets | |
| { | |
| // Keep stable once deployed; changing breaks decryption of existing values. | |
| private const string DefaultPurpose = "YourCompany.Connector.Secrets.v1"; | |
| // --------- One-liner DPAPI helpers --------- | |
| public static string ProtectToBase64(string plaintext, string purpose = null, bool localMachine = true) | |
| { | |
| EnsureWindows(); | |
| if (plaintext == null) return null; | |
| byte[] entropy = Encoding.UTF8.GetBytes(purpose ?? DefaultPurpose); | |
| byte[] data = Encoding.UTF8.GetBytes(plaintext); | |
| var scope = localMachine ? DataProtectionScope.LocalMachine : DataProtectionScope.CurrentUser; | |
| byte[] protectedBytes = ProtectedData.Protect(data, entropy, scope); | |
| return Convert.ToBase64String(protectedBytes); | |
| } | |
| public static string UnprotectFromBase64(string protectedBase64, string purpose = null, bool localMachine = true) | |
| { | |
| EnsureWindows(); | |
| if (string.IsNullOrWhiteSpace(protectedBase64)) return null; | |
| byte[] entropy = Encoding.UTF8.GetBytes(purpose ?? DefaultPurpose); | |
| byte[] protectedBytes = Convert.FromBase64String(protectedBase64); | |
| var scope = localMachine ? DataProtectionScope.LocalMachine : DataProtectionScope.CurrentUser; | |
| byte[] bytes = ProtectedData.Unprotect(protectedBytes, entropy, scope); | |
| return Encoding.UTF8.GetString(bytes); | |
| } | |
| // --------- Registry storage helpers --------- | |
| // Recommended for Windows Services: RegistryHive.LocalMachine + localMachineDpapi = true | |
| 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"); | |
| string 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); | |
| } | |
| } | |
| 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); | |
| } | |
| } | |
| 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); | |
| } | |
| } | |
| private static void EnsureWindows() | |
| { | |
| // .NET Framework has no OperatingSystem.IsWindows() and no RuntimeInformation by default. | |
| 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."); | |
| } | |
| } | |
| /* | |
| Example usage (.NET 4.8 Windows Service): | |
| var keyPath = @"SOFTWARE\YourCompany\Connector\Secrets"; | |
| // Save (typically done by installer or enrollment tool running elevated) | |
| DpapiRegistrySecrets.SaveSecret( | |
| hive: RegistryHive.LocalMachine, | |
| subKeyPath: keyPath, | |
| valueName: "CognitoClientSecret", | |
| plaintext: "super-secret", | |
| purpose: "YourCompany.Cognito.ClientSecret.v1", | |
| localMachineDpapi: true); | |
| // Load (service runtime) | |
| var secret = DpapiRegistrySecrets.LoadSecret( | |
| RegistryHive.LocalMachine, | |
| keyPath, | |
| "CognitoClientSecret", | |
| purpose: "YourCompany.Cognito.ClientSecret.v1", | |
| localMachineDpapi: true); | |
| */ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment