Storing secrets securly in Windows


One of the challenges with scripting / programming is securely storing passwords or API secrets across many different languages. Most of what i develop runs in Windows and as such it made sense to tap into the ProtectedData API and the Windows Registry.

PowerShell Code

Add-Type -AssemblyName System.Security;
$RegRootPath = "HKCU:\SOFTWARE\MIKE"
[byte[]]$AdditionalEntropy = 77,33,22,44,66,12,46,74,22,11,45,23,11,34,66

function Load-SecureDataFromRegistry
{
	param (
		[string]$name,
		[string]$keyName
	)
	
	$regPath = "$($regRootPath)\$($name)\"
	
	if (Test-Path $regPath)
	{
		
		$encryptedBase64String = Get-ItemPropertyValue -Path $regPath -Name $keyName
		#Write-Host "base64 string$($encryptedBase64String)"
		$toDecrypt = [System.Convert]::FromBase64String($encryptedBase64String)
        #Write-Host "base64 data: $($toDecrypt)"
		$toDecrypt = [System.Security.Cryptography.ProtectedData]::Unprotect($toDecrypt, $AdditionalEntropy, 'CurrentUser')
		$toDecrypt = [System.Text.UnicodeEncoding]::ASCII.GetString($toDecrypt)
		
		return $toDecrypt
	}
	
	return [String]::Empty
}

function Save-SecureDataToRegistry
{
	param (
		[string]$name,
		[string]$keyName,
		[string]$keyValue,
		[string]$description
	)
	$toEncrypt = [System.Text.UnicodeEncoding]::ASCII.GetBytes($keyValue)
	$toEncrypt = [System.Security.Cryptography.ProtectedData]::Protect($toEncrypt, $AdditionalEntropy, 'CurrentUser')
	$toEncrypt = [System.Convert]::ToBase64String($toEncrypt)
	
	$regPath = "$($regRootPath)\$($name)\"

	if (!(Test-Path -Path $regPath))
	{
		New-Item -Path $regRootPath -Name $name -Force -Confirm:$false
	}

	Set-ItemProperty -Path $regPath -Name $keyName -Value $toEncrypt -Force -Confirm:$false
	Set-ItemProperty -Path $regPath -Name "$($keyName)_Description" -Value $description -Force -Confirm:$false
	
}

Powershell Usage

Save-SecureDataToRegistry -name "DefenderForEndpoint" -keyName "ApiKey1" -description "Defender for endpoint api key - read only" -keyValue 'example secret'
$apiKey1 = Load-SecureDataFromRegistry -name "DefenderForEndpoint" -keyName "ApiKey1"

C# Code

    /// <summary>
    /// https://learn.microsoft.com/en-us/dotnet/standard/security/how-to-use-data-protection
    /// https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.protecteddata?view=net-8.0&redirectedfrom=MSDN
    /// </summary>
    public static class EncryptionManagement
    {
        private static byte[] _additionalEntropy = { 77,33,22,44,66,12,46,74,22,11,45,23,11,34,66 };
        private static string _regRootPath = @"SOFTWARE\MIKE";

        public static string LoadSecureStringFromRegistry(string name, string keyName)
        {
            var regKey = Registry.CurrentUser.OpenSubKey($@"{_regRootPath}\{name}");
            if (regKey != null)
            {
                var secureText = (string)regKey.GetValue(keyName);
                return secureText.DecryptString();
            }
            return string.Empty;
        }

        public static void SaveSecureStringToRegistry(this string keyValue, string name, string keyName, string description)
        {
            var regKey = Registry.CurrentUser.OpenSubKey($@"{_regRootPath}\{name}");
            if (regKey == null)
            {
                Registry.CurrentUser.CreateSubKey($@"{_regRootPath}\{name}");
                regKey = Registry.CurrentUser.OpenSubKey($@"{_regRootPath}\{name}");
            }

            regKey.SetValue(keyName, keyValue.EncryptString(), RegistryValueKind.String);
            regKey.SetValue("{keyName}_Description", description, RegistryValueKind.String);
        }

        public static string EncryptString(this string text)
        {
            return EncryptString(text, DataProtectionScope.CurrentUser);
        }

        public static string DecryptString(this string text)
        {
            return DecryptString(text, DataProtectionScope.CurrentUser);
        }

        public static void ProtectedByteArrayToString(this Byte[] myArr)
        {
            foreach (Byte i in myArr)
            {
                Console.Write("\t{0}", i);
            }
            Console.WriteLine();
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="text">String</param>
        /// <param name="scope"></param>
        /// <returns>Base64EncodedString</returns>
        public static string EncryptString(this string text, DataProtectionScope scope)
        {
            byte[] toEncrypt = UnicodeEncoding.ASCII.GetBytes(text);
            toEncrypt = Encrypt(toEncrypt, scope);
            return Convert.ToBase64String(toEncrypt);
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="text">Base64EncodedString</param>
        /// <param name="scope"></param>
        /// <returns>String</returns>
        public static string DecryptString(this string text, DataProtectionScope scope)
        {
            byte[] toDecrypt = Convert.FromBase64String(text);
            toDecrypt = Decrypt(toDecrypt, scope);
            return UnicodeEncoding.ASCII.GetString(toDecrypt);
        }

        public static byte[] Encrypt(byte[] data, DataProtectionScope scope)
        {
            try
            {
                // Encrypt the data using the supplied scope. The result can be decrypted
                // only by the same scope.
                return ProtectedData.Protect(data, _additionalEntropy, scope);
            }
            catch (CryptographicException e)
            {
                Console.WriteLine("Data was not encrypted. An error occurred.");
                Console.WriteLine(e.ToString());
                return null;
            }
        }

        public static byte[] Decrypt(byte[] data, DataProtectionScope scope)
        {
            try
            {
                //Decrypt the data using supplied scope.
                return ProtectedData.Unprotect(data, _additionalEntropy, scope);
            }
            catch (CryptographicException e)
            {
                Console.WriteLine("Data was not decrypted. An error occurred.");
                Console.WriteLine(e.ToString());
                return null;
            }
        }

        public static byte[] Encrypt(byte[] data)
        {
            try
            {
                // Encrypt the data using DataProtectionScope.CurrentUser. The result can be decrypted
                // only by the same current user.
                return ProtectedData.Protect(data, _additionalEntropy, DataProtectionScope.CurrentUser);
            }
            catch (CryptographicException e)
            {
                Console.WriteLine("Data was not encrypted. An error occurred.");
                Console.WriteLine(e.ToString());
                return null;
            }
        }

        public static byte[] Decrypt(byte[] data)
        {
            try
            {
                //Decrypt the data using DataProtectionScope.CurrentUser.
                return ProtectedData.Unprotect(data, _additionalEntropy, DataProtectionScope.CurrentUser);
            }
            catch (CryptographicException e)
            {
                Console.WriteLine("Data was not decrypted. An error occurred.");
                Console.WriteLine(e.ToString());
                return null;
            }
        }

        public static void EncryptInMemoryData(byte[] Buffer, MemoryProtectionScope Scope)
        {
            if (Buffer == null)
                throw new ArgumentNullException(nameof(Buffer));
            if (Buffer.Length <= 0)
                throw new ArgumentException("The buffer length was 0.", nameof(Buffer));

            // Encrypt the data in memory. The result is stored in the same array as the original data.
            ProtectedMemory.Protect(Buffer, Scope);
        }

        public static void DecryptInMemoryData(byte[] Buffer, MemoryProtectionScope Scope)
        {
            if (Buffer == null)
                throw new ArgumentNullException(nameof(Buffer));
            if (Buffer.Length <= 0)
                throw new ArgumentException("The buffer length was 0.", nameof(Buffer));

            // Decrypt the data in memory. The result is stored in the same array as the original data.
            ProtectedMemory.Unprotect(Buffer, Scope);
        }

        public static byte[] CreateRandomEntropy()
        {
            // Create a byte array to hold the random value.
            byte[] entropy = new byte[16];

            // Create a new instance of the RNGCryptoServiceProvider.
            // Fill the array with a random value.
            new RNGCryptoServiceProvider().GetBytes(entropy);

            // Return the array.
            return entropy;
        }

        public static int EncryptDataToStream(byte[] Buffer, byte[] Entropy, DataProtectionScope Scope, Stream S)
        {
            if (Buffer == null)
                throw new ArgumentNullException(nameof(Buffer));
            if (Buffer.Length <= 0)
                throw new ArgumentException("The buffer length was 0.", nameof(Buffer));
            if (Entropy == null)
                throw new ArgumentNullException(nameof(Entropy));
            if (Entropy.Length <= 0)
                throw new ArgumentException("The entropy length was 0.", nameof(Entropy));
            if (S == null)
                throw new ArgumentNullException(nameof(S));

            int length = 0;

            // Encrypt the data and store the result in a new byte array. The original data remains unchanged.
            byte[] encryptedData = ProtectedData.Protect(Buffer, Entropy, Scope);

            // Write the encrypted data to a stream.
            if (S.CanWrite && encryptedData != null)
            {
                S.Write(encryptedData, 0, encryptedData.Length);

                length = encryptedData.Length;
            }

            // Return the length that was written to the stream.
            return length;
        }

        public static byte[] DecryptDataFromStream(byte[] Entropy, DataProtectionScope Scope, Stream S, int Length)
        {
            if (S == null)
                throw new ArgumentNullException(nameof(S));
            if (Length <= 0)
                throw new ArgumentException("The given length was 0.", nameof(Length));
            if (Entropy == null)
                throw new ArgumentNullException(nameof(Entropy));
            if (Entropy.Length <= 0)
                throw new ArgumentException("The entropy length was 0.", nameof(Entropy));

            byte[] inBuffer = new byte[Length];
            byte[] outBuffer;

            // Read the encrypted data from a stream.
            if (S.CanRead)
            {
                S.Read(inBuffer, 0, Length);

                outBuffer = ProtectedData.Unprotect(inBuffer, Entropy, Scope);
            }
            else
            {
                throw new IOException("Could not read the stream.");
            }

            // Return the decrypted data
            return outBuffer;
        }
    }

C# Usage

pass = EncryptionManagement.LoadSecureStringFromRegistry("DefenderForEndpoint", "key1");
EncryptionManagement.SaveSecureStringToRegistry("example key", "DefenderForEndpoint", "key1", "Read only defender secret");

Python Code

import base64
import winreg
import win32crypt #actual module to install pypiwin32 or pywin32

reg_root_path = r"SOFTWARE\MIKE"
additional_entropy = bytes([77,33,22,44,66,12,46,74,22,11,45,23,11,34,66])


def save_secure_data_to_registry(name, key_name, key_value, description, reg_root_path, additional_entropy):
    # Encrypt the key_value
    key_value = key_value.encode('utf-8')
    to_encrypt = win32crypt.CryptProtectData(key_value, None, additional_entropy , None, None, 0)
    to_encrypt = base64.b64encode(to_encrypt).decode('ascii')

    reg_path = f"{reg_root_path}\\{name}"
    # Create registry key if it doesn't exist
    try:
        winreg.CreateKey(winreg.HKEY_CURRENT_USER, reg_path)
    except FileExistsError:
        print("error writing to reg")

    # Set registry values
    with winreg.OpenKey(winreg.HKEY_CURRENT_USER, reg_path, 0, winreg.KEY_WRITE) as reg_key:
        winreg.SetValueEx(reg_key, key_name, 0, winreg.REG_SZ, to_encrypt)
        winreg.SetValueEx(reg_key, f"{key_name}_Description", 0, winreg.REG_SZ, description)

def load_secure_data_from_registry(name, key_name, reg_root_path, additional_entropy):
    reg_path = f"{reg_root_path}\\{name}"
    additional_entropy_bytes = bytes(additional_entropy)
    try:
        with winreg.OpenKey(winreg.HKEY_CURRENT_USER, reg_path) as key:
            encrypted_base64_string, _ = winreg.QueryValueEx(key, key_name)
            encoded_bytes = encrypted_base64_string.encode('ascii')
            #print(f"base64secret: {encoded_bytes}")
            decoded_data = base64.decodebytes(encoded_bytes)
            #print(f"secret: {decoded_data}")
            decrypted_data_result = win32crypt.CryptUnprotectData(decoded_data, additional_entropy_bytes, None, None, 0)
            decrypted_data = decrypted_data_result[1].decode('ascii')
            return decrypted_data
    except FileNotFoundError:
        return ""

Python Usage

save_secure_data_to_registry("DefenderForEndpoint1", "DefenderApiKey", apikey, "defender key", reg_root_path, additional_entropy)
apikey1 = load_secure_data_from_registry("DefenderForEndpoint", "DefenderApiKey", reg_root_path, additional_entropy)
print (f"apikey1 {apikey1}")