For anyone who comes across this question in the future, I've written a pair of extension methods for SecureString which replicate the behaviour of ProtectedData.Protect (SecureStringExtensions.Protect) and ProtectedData.Unprotect (SecureStringExtensions.AppendProtected), but work seamlessly with secure strings rather than byte arrays.
They've been designed to be secure and robust, so I hope they'll be of use. (.NET Framework version directly below; newer .NET 8 version further down.)
/*
* This code is licensed under a Creative Commons Attribution 4.0 International License.
* See: https://creativecommons.org/licenses/by/4.0/
*/
using System;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Cryptography;
namespace MyNamespace
{
/// <summary>Extension methods for <see cref="T:System.Security.SecureString" /> to enable safe serialisation and deserialisation of secure strings. This class cannot be inherited.</summary>
/// <remarks>The <see cref="M:Protect(SecureString, System.Byte[], DataProtectionScope)" /> and <see cref="M:AppendProtected(SecureString, System.Byte[], System.Byte[], DataProtectionScope)" /> methods can be treated as secure string-based equivalents of <see cref="M:System.Security.Cryptography.ProtectedData.Protect(System.Byte[], System.Byte[], System.Security.Cryptography.DataProtectionScope)" /> and <see cref="M:System.Security.Cryptography.ProtectedData.Unprotect(System.Byte[], System.Byte[], System.Security.Cryptography.DataProtectionScope)" />, respectively.</remarks>
/// <seealso cref="T:System.Security.Cryptography.ProtectedData" />
public static class SecureStringExtensions
{
/// <summary>Specifies the scope of the data protection to be applied by the <see cref="Protect(SecureString, byte[], DataProtectionScope)" /> and <see cref="AppendProtected(SecureString, byte[], byte[], DataProtectionScope)" /> methods.</summary>
/// <remarks>This enumeration is equivalent to <see cref="T:System.Security.Cryptography.DataProtectionScope" />.</remarks>
/// <seealso cref="T:System.Security.Cryptography.DataProtectionScope" />
public enum DataProtectionScope
{
/// <summary>
/// The protected data is associated with the current user. Only threads running under the current user context can unprotect the data.
/// </summary>
CurrentUser,
/// <summary>
/// The protected data is associated with the machine context. Any process running on the computer can unprotect data.
/// This enumeration value is usually used in server-specific applications that run on a server where untrusted users are not allowed access.
/// </summary>
LocalMachine
}
/// <summary>Encrypts the data in a secure string and returns a byte array that contains the encrypted data.</summary>
/// <remarks>This method can be treated as equivalent to <see cref="M:System.Security.Cryptography.ProtectedData.Protect(System.Byte[], System.Byte[], System.Security.Cryptography.DataProtectionScope)" />, except that it encrypts a secure string instead of a byte array.</remarks>
/// <param name="secureString">The secure string.</param>
/// <param name="optionalEntropy">An optional additional byte array used to increase the complexity of the encryption, or <see langword="null" /> for no additional complexity.</param>
/// <param name="scope">One of the enumeration values that specifies the scope of encryption.</param>
/// <returns>A byte array representing the encrypted data.</returns>
/// <exception cref="T:System.Security.Cryptography.CryptographicException">The encryption failed.</exception>
/// <exception cref="T:System.OutOfMemoryException">The system ran out of memory while encrypting the data.</exception>
/// <seealso cref="M:System.Security.Cryptography.ProtectedData.Protect(System.Byte[], System.Byte[], System.Security.Cryptography.DataProtectionScope)" />
public static byte[] Protect(this SecureString secureString, byte[] optionalEntropy, DataProtectionScope scope)
{
byte[] result = null;
NativeMethods.DATA_BLOB dataIn = null;
NativeMethods.DATA_BLOB entropy = null;
NativeMethods.DATA_BLOB dataOut = new NativeMethods.DATA_BLOB();
GCHandle ptrOptionalEntropy = new GCHandle();
try
{
// +++ Handle secureString
dataIn = new NativeMethods.DATA_BLOB
{
cbData = secureString.Length * sizeof(char),
pbData = Marshal.SecureStringToGlobalAllocUnicode(secureString)
};
// ---
// +++ Handle optionalEntropy
if (optionalEntropy != null)
{
ptrOptionalEntropy = GCHandle.Alloc(optionalEntropy, GCHandleType.Pinned);
entropy = new NativeMethods.DATA_BLOB
{
cbData = optionalEntropy.Length,
pbData = ptrOptionalEntropy.AddrOfPinnedObject()
};
}
// ---
// +++ Handle scope
NativeMethods.CryptProtectFlags flags = NativeMethods.CryptProtectFlags.CRYPTPROTECT_UI_FORBIDDEN;
if (scope.HasFlag(DataProtectionScope.LocalMachine))
flags |= NativeMethods.CryptProtectFlags.CRYPTPROTECT_LOCAL_MACHINE;
// ---
if (!NativeMethods.CryptProtectData(dataIn, IntPtr.Zero, entropy, IntPtr.Zero, IntPtr.Zero, flags, dataOut))
throw new CryptographicException(Marshal.GetLastWin32Error());
else
{
if (dataOut.pbData == IntPtr.Zero)
throw new OutOfMemoryException();
result = new byte[dataOut.cbData];
Marshal.Copy(dataOut.pbData, result, 0, dataOut.cbData);
}
}
finally
{
if (dataOut.pbData != IntPtr.Zero)
{
NativeMethods.ZeroMemory(dataOut.pbData, (UIntPtr)dataOut.cbData);
Marshal.FreeHGlobal(dataOut.pbData);
}
if (ptrOptionalEntropy.IsAllocated)
ptrOptionalEntropy.Free();
if (dataIn != null)
Marshal.ZeroFreeGlobalAllocUnicode(dataIn.pbData);
}
return (result);
}
/// <summary>Decrypts the data in a specified byte array and appends it to a secure string.</summary>
/// <remarks>This method can be treated as equivalent to <see cref="M:System.Security.Cryptography.ProtectedData.Unprotect(System.Byte[], System.Byte[], System.Security.Cryptography.DataProtectionScope)" />, except that it appends the decrypted data to a secure string instead returning it in a byte array.</remarks>
/// <param name="secureString">The secure string.</param>
/// <param name="encryptedData">A byte array containing data encrypted using the <see cref="M:Protect(SecureString, System.Byte[], DataProtectionScope)" /> method.</param>
/// <param name="optionalEntropy">An optional additional byte array that was used to encrypt the data, or <see langword="null" /> if the additional byte array was not used.</param>
/// <param name="scope">One of the enumeration values that specifies the scope of data protection that was used to encrypt the data.</param>
/// <exception cref="T:System.ArgumentNullException">The <paramref name="encryptedData" /> parameter is <see langword="null" />.</exception>
/// <exception cref="T:System.InvalidOperationException">The secure string is read only.</exception>
/// <exception cref="T:System.Security.Cryptography.CryptographicException">The decryption failed.</exception>
/// <exception cref="T:System.OutOfMemoryException">The system ran out of memory while decrypting the data.</exception>
/// <seealso cref="M:System.Security.Cryptography.ProtectedData.Unprotect(System.Byte[], System.Byte[], System.Security.Cryptography.DataProtectionScope)" />
public static void AppendProtected(this SecureString secureString, byte[] encryptedData, byte[] optionalEntropy, DataProtectionScope scope)
{
NativeMethods.DATA_BLOB dataIn = null;
NativeMethods.DATA_BLOB entropy = null;
NativeMethods.DATA_BLOB dataOut = new NativeMethods.DATA_BLOB();
GCHandle ptrEncryptedData = new GCHandle();
GCHandle ptrOptionalEntropy = new GCHandle();
if (encryptedData == null)
throw new ArgumentNullException("encryptedData");
if (encryptedData.IsReadOnly)
throw new InvalidOperationException();
try
{
// +++ Handle encryptedData
ptrEncryptedData = GCHandle.Alloc(encryptedData, GCHandleType.Pinned);
dataIn = new NativeMethods.DATA_BLOB
{
cbData = encryptedData.Length,
pbData = ptrEncryptedData.AddrOfPinnedObject()
};
// ---
// +++ Handle optionalEntropy
if (optionalEntropy != null)
{
ptrOptionalEntropy = GCHandle.Alloc(optionalEntropy, GCHandleType.Pinned);
entropy = new NativeMethods.DATA_BLOB
{
cbData = optionalEntropy.Length,
pbData = ptrOptionalEntropy.AddrOfPinnedObject()
};
}
// ---
// +++ Handle scope
NativeMethods.CryptProtectFlags flags = NativeMethods.CryptProtectFlags.CRYPTPROTECT_UI_FORBIDDEN;
if (scope.HasFlag(DataProtectionScope.LocalMachine))
flags |= NativeMethods.CryptProtectFlags.CRYPTPROTECT_LOCAL_MACHINE;
// ---
if (!NativeMethods.CryptUnprotectData(dataIn, IntPtr.Zero, entropy, IntPtr.Zero, IntPtr.Zero, flags, dataOut))
throw new CryptographicException(Marshal.GetLastWin32Error());
else
{
if (dataOut.pbData == IntPtr.Zero)
throw new OutOfMemoryException();
// Sanity check: can't be a valid string if length is not a multiple of sizeof(char)
if ((dataOut.cbData % sizeof(char)) != 0)
throw new CryptographicException();
for (int i = 0; i < dataOut.cbData; i += sizeof(char))
secureString.AppendChar((char)Marshal.ReadInt16(dataOut.pbData, i));
}
}
finally
{
if (dataOut.pbData != IntPtr.Zero)
{
NativeMethods.ZeroMemory(dataOut.pbData, (UIntPtr)dataOut.cbData);
Marshal.FreeHGlobal(dataOut.pbData);
}
if (ptrOptionalEntropy.IsAllocated)
ptrOptionalEntropy.Free();
if (ptrEncryptedData.IsAllocated)
ptrEncryptedData.Free();
}
}
private static class NativeMethods
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1049:TypesThatOwnNativeResourcesShouldBeDisposable")]
[StructLayout(LayoutKind.Sequential)]
public class DATA_BLOB
{
public int cbData;
public IntPtr pbData;
}
[Flags]
public enum CryptProtectFlags : uint
{
CRYPTPROTECT_UI_FORBIDDEN = 0x01,
CRYPTPROTECT_LOCAL_MACHINE = 0x04,
CRYPTPROTECT_VERIFY_PROTECTION = 0x40
}
[DllImport("kernel32.dll", EntryPoint = "RtlZeroMemory")]
public static extern void ZeroMemory(IntPtr destination, UIntPtr length);
[DllImport("crypt32.dll", SetLastError = true)]
public static extern bool CryptProtectData(DATA_BLOB pDataIn, IntPtr szDataDescr, DATA_BLOB pOptionalEntropy, IntPtr pvReserved, IntPtr pPromptStruct, CryptProtectFlags dwFlags, DATA_BLOB pDataOut);
[DllImport("crypt32.dll", SetLastError = true)]
public static extern bool CryptUnprotectData(DATA_BLOB pDataIn, IntPtr ppszDataDescr, DATA_BLOB pOptionalEntropy, IntPtr pvReserved, IntPtr pPromptStruct, CryptProtectFlags dwFlags, DATA_BLOB pDataOut);
}
}
}
Example:
byte[] entropy = { 1, 2, 3, 4, 5 }; // Example entropy
string password = "This is my password"; // Don't store passwords like this!
// Add password to secure string
SecureString ss1 = new SecureString();
Array.ForEach(password.ToCharArray(), ss1.AppendChar);
ss1.MakeReadOnly();
// Encrypt secure string
byte[] protectedBytes = ss1.Protect(entropy, SecureStringExtensions.DataProtectionScope.CurrentUser);
Console.WriteLine("Encrypted secure string: {0}", Convert.ToBase64String(protectedBytes));
// Decrypt and add to a new secure string
SecureString ss2 = new SecureString();
ss2.AppendProtected(protectedBytes, entropy, SecureStringExtensions.DataProtectionScope.CurrentUser);
ss2.MakeReadOnly();
After many years, I've recently had to port this code to .NET 8. Here's the updated version:
/*
* This code is licensed under a Creative Commons Attribution 4.0 International License.
* See: https://creativecommons.org/licenses/by/4.0/
*/
using System;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Cryptography;
namespace MyNamespace
{
/// <summary>Extension methods for <see cref="System.Security.SecureString" /> to enable safe serialisation and deserialisation of secure strings. This class cannot be inherited.</summary>
/// <remarks>The <see cref="Protect(SecureString, System.Byte[], DataProtectionScope)" /> and <see cref="AppendProtected(SecureString, System.Byte[], System.Byte[], DataProtectionScope)" /> methods can be treated as secure string-based equivalents of <see cref="System.Security.Cryptography.ProtectedData.Protect(System.Byte[], System.Byte[], System.Security.Cryptography.DataProtectionScope)" /> and <see cref="System.Security.Cryptography.ProtectedData.Unprotect(System.Byte[], System.Byte[], System.Security.Cryptography.DataProtectionScope)" />, respectively.</remarks>
/// <seealso cref="System.Security.Cryptography.ProtectedData" />
public static partial class SecureStringExtensions
{
/// <summary>Specifies the scope of the data protection to be applied by the <see cref="Protect(SecureString, byte[], DataProtectionScope)" /> and <see cref="AppendProtected(SecureString, byte[], byte[], DataProtectionScope)" /> methods.</summary>
/// <remarks>This enumeration is equivalent to <see cref="System.Security.Cryptography.DataProtectionScope" />.</remarks>
/// <seealso cref="System.Security.Cryptography.DataProtectionScope" />
public enum DataProtectionScope
{
/// <summary>
/// The protected data is associated with the current user. Only threads running under the current user context can unprotect the data.
/// </summary>
CurrentUser,
/// <summary>
/// The protected data is associated with the machine context. Any process running on the computer can unprotect data.
/// This enumeration value is usually used in server-specific applications that run on a server where untrusted users are not allowed access.
/// </summary>
LocalMachine
}
/// <summary>Encrypts the data in a secure string and returns a byte array that contains the encrypted data.</summary>
/// <remarks>This method can be treated as equivalent to <see cref="System.Security.Cryptography.ProtectedData.Protect(System.Byte[], System.Byte[], System.Security.Cryptography.DataProtectionScope)" />, except that it encrypts a secure string instead of a byte array.</remarks>
/// <param name="secureString">The secure string.</param>
/// <param name="optionalEntropy">An optional additional byte array used to increase the complexity of the encryption, or <see langword="null" /> for no additional complexity.</param>
/// <param name="scope">One of the enumeration values that specifies the scope of encryption.</param>
/// <returns>A byte array representing the encrypted data.</returns>
/// <exception cref="System.Security.Cryptography.CryptographicException">The encryption failed.</exception>
/// <seealso cref="System.Security.Cryptography.ProtectedData.Protect(System.Byte[], System.Byte[], System.Security.Cryptography.DataProtectionScope)" />
public static unsafe byte[] Protect(this SecureString secureString, byte[]? optionalEntropy, DataProtectionScope scope)
{
byte[]? protectedBytes = null;
NativeMethods.DATA_BLOB dataIn = default;
NativeMethods.DATA_BLOB dataOut = default;
try
{
bool result;
NativeMethods.DATA_BLOB entropy;
fixed (byte* ptrOptionalEntropy = optionalEntropy)
{
NativeMethods.DATA_BLOB* ptrEntropy;
// +++ Handle secureString
dataIn = new NativeMethods.DATA_BLOB
{
cbData = (uint)secureString.Length * sizeof(char),
pbData = (byte*)Marshal.SecureStringToGlobalAllocUnicode(secureString)
};
// ---
// +++ Handle optionalEntropy
if (optionalEntropy is not null)
{
entropy = new NativeMethods.DATA_BLOB
{
cbData = (uint)optionalEntropy.Length,
pbData = ptrOptionalEntropy
};
ptrEntropy = &entropy;
}
else
ptrEntropy = (NativeMethods.DATA_BLOB*)nint.Zero;
// ---
// +++ Handle scope
NativeMethods.CryptProtectFlags flags = NativeMethods.CryptProtectFlags.CRYPTPROTECT_UI_FORBIDDEN;
if (scope.HasFlag(DataProtectionScope.LocalMachine))
flags |= NativeMethods.CryptProtectFlags.CRYPTPROTECT_LOCAL_MACHINE;
// ---
result = NativeMethods.CryptProtectData(&dataIn, nint.Zero, ptrEntropy, nint.Zero, nint.Zero, flags, &dataOut);
}
if (result)
{
if (dataOut.pbData == (byte*)nint.Zero)
throw new CryptographicException();
ReadOnlySpan<byte> buffer = new(dataOut.pbData, (int)dataOut.cbData);
protectedBytes = buffer.ToArray();
}
else
throw new CryptographicException(Marshal.GetHRForLastWin32Error());
}
finally
{
if (dataOut.pbData != (byte*)nint.Zero)
{
CryptographicOperations.ZeroMemory(new Span<byte>(dataOut.pbData, (int)dataOut.cbData));
Marshal.FreeHGlobal((nint)dataOut.pbData);
}
if (dataIn.pbData != (byte*)nint.Zero)
Marshal.ZeroFreeGlobalAllocUnicode((nint)dataIn.pbData);
}
return (protectedBytes);
}
/// <summary>Decrypts the data in a specified byte array and appends it to a secure string.</summary>
/// <remarks>This method can be treated as equivalent to <see cref="System.Security.Cryptography.ProtectedData.Unprotect(System.Byte[], System.Byte[], System.Security.Cryptography.DataProtectionScope)" />, except that it appends the decrypted data to a secure string instead returning it in a byte array.</remarks>
/// <param name="secureString">The secure string.</param>
/// <param name="encryptedData">A byte array containing data encrypted using the <see cref="Protect(SecureString, System.Byte[], DataProtectionScope)" /> method.</param>
/// <param name="optionalEntropy">An optional additional byte array that was used to encrypt the data, or <see langword="null" /> if the additional byte array was not used.</param>
/// <param name="scope">One of the enumeration values that specifies the scope of data protection that was used to encrypt the data.</param>
/// <exception cref="System.ArgumentNullException">The <paramref name="encryptedData" /> parameter is <see langword="null" />.</exception>
/// <exception cref="System.InvalidOperationException">The secure string is read only.</exception>
/// <exception cref="System.Security.Cryptography.CryptographicException">The decryption failed.</exception>
/// <seealso cref="System.Security.Cryptography.ProtectedData.Unprotect(System.Byte[], System.Byte[], System.Security.Cryptography.DataProtectionScope)" />
public static unsafe void AppendProtected(this SecureString secureString, byte[] encryptedData, byte[]? optionalEntropy, DataProtectionScope scope)
{
ArgumentNullException.ThrowIfNull(encryptedData);
if (encryptedData.IsReadOnly)
throw new InvalidOperationException();
NativeMethods.DATA_BLOB dataOut = default;
try
{
bool result;
NativeMethods.DATA_BLOB dataIn;
NativeMethods.DATA_BLOB entropy;
fixed (byte* ptrEncryptedData = encryptedData)
fixed (byte* ptrOptionalEntropy = optionalEntropy)
{
NativeMethods.DATA_BLOB* ptrEntropy;
// +++ Handle encryptedData
dataIn = new NativeMethods.DATA_BLOB
{
cbData = (uint)encryptedData.Length,
pbData = ptrEncryptedData
};
// ---
// +++ Handle optionalEntropy
if (optionalEntropy is not null)
{
entropy = new NativeMethods.DATA_BLOB
{
cbData = (uint)optionalEntropy.Length,
pbData = ptrOptionalEntropy
};
ptrEntropy = &entropy;
}
else
ptrEntropy = (NativeMethods.DATA_BLOB*)nint.Zero;
// ---
// +++ Handle scope
NativeMethods.CryptProtectFlags flags = NativeMethods.CryptProtectFlags.CRYPTPROTECT_UI_FORBIDDEN;
if (scope.HasFlag(DataProtectionScope.LocalMachine))
flags |= NativeMethods.CryptProtectFlags.CRYPTPROTECT_LOCAL_MACHINE;
// ---
result = NativeMethods.CryptUnprotectData(&dataIn, nint.Zero, ptrEntropy, nint.Zero, nint.Zero, flags, &dataOut);
}
if (result)
{
// Sanity check: can't be a valid string if length is not a multiple of sizeof(char), or if dataOut.pbData is null
if (((dataOut.cbData % sizeof(char)) != 0) || (dataOut.pbData == (byte*)nint.Zero))
throw new CryptographicException();
ReadOnlySpan<char> buffer = new(dataOut.pbData, (int)dataOut.cbData / sizeof(char));
foreach (char c in buffer)
secureString.AppendChar(c);
}
else
throw new CryptographicException(Marshal.GetHRForLastWin32Error());
}
finally
{
if (dataOut.pbData != (byte*)nint.Zero)
{
CryptographicOperations.ZeroMemory(new Span<byte>(dataOut.pbData, (int)dataOut.cbData));
Marshal.FreeHGlobal((nint)dataOut.pbData);
}
}
}
private static partial class NativeMethods
{
[StructLayout(LayoutKind.Sequential)]
internal struct DATA_BLOB
{
public uint cbData;
public unsafe byte* pbData;
}
[Flags]
internal enum CryptProtectFlags : uint
{
CRYPTPROTECT_UI_FORBIDDEN = 0x01,
CRYPTPROTECT_LOCAL_MACHINE = 0x04,
CRYPTPROTECT_VERIFY_PROTECTION = 0x40
}
[LibraryImport("crypt32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static unsafe partial bool CryptProtectData(DATA_BLOB* pDataIn, nint szDataDescr, DATA_BLOB* pOptionalEntropy, nint pvReserved, nint pPromptStruct, CryptProtectFlags dwFlags, DATA_BLOB* pDataOut);
[LibraryImport("crypt32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static unsafe partial bool CryptUnprotectData(DATA_BLOB* pDataIn, nint ppszDataDescr, DATA_BLOB* pOptionalEntropy, nint pvReserved, nint pPromptStruct, CryptProtectFlags dwFlags, DATA_BLOB* pDataOut);
}
}
}