我想使用 DPAPI 加密 SecureString,以便将其保存到磁盘。
.net DPAPI 类是 ProtectedData 类,但是,ProtectedData.Protect 有一个采用字节数组的重载。没有接受 SecureString 的重载。
在 加密 .NET app.config 文件中的密码中,John Galloway 通过首先将 SecureString 转换为不安全的字符串来利用上述重载。我想避免这种情况,因为它首先违背了使用 SecureString 的目的。
ConvertFrom-SecureString PowerShell cmdlet 似乎可以满足我的需要,因为“如果未指定密钥,则使用 Windows 数据保护 API (DPAPI) 来加密标准字符串表示形式”,但我不确定如何执行直接从 .net 使用此 cmdlet,或者即使这样做是个好主意。
编写了一对扩展方法,它们复制了 ProtectedData.Protect (SecureStringExtensions.Protect) 和 ProtectedData.Unprotect (SecureStringExtensions.AppendProtected) 的行为),但可以与安全字符串而不是字节数组无缝协作。 它们的设计是安全和稳健的,所以我希望它们有用。 (.NET Framework 版本直接在下面;较新的 .NET 8 版本在下面。)
/*
* 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);
}
}
}
示例:
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();
/*
* 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);
}
}
}
据我所知,目前在使用 DPAPI 加密 SecureStrings 方面存在一些 API 差距。通过查看可用的方法,并阅读 Jeff Griffin 的
SecureString: Soup to Nuts, Part I,您似乎无法使用方便的花花公子 ProtectedData.Protect 和 Unprotect 方法,因为它们不采用字节数组,如果您将 SecureString 转换为字节数组,它将再次不受保护地坐在那里,从而达不到目的。 因此,解决方案涉及使用 P/Invoke 直接调用 ProtectedData 方法使用的底层 C++ 命令。我相信这就是 Jeff Griffin 的博客所做的,但到实际扩展方法的链接似乎已死,所以我试图拼凑出它们可能的样子,这样它们就不会迷路,但同样,我不是安全的专家所以对这一切持保留态度:
我还发现这个示例作为参考非常有用:
http://www.obviex.com/samples/dpapi.aspx
public static class EncryptionExtensions
{
public static string Decrypt(this byte[] data)
{
DATA_BLOB plainTextBlob = new DATA_BLOB();//we need to pass all of these as parameters
DATA_BLOB cipherTextBlob = new DATA_BLOB();
DATA_BLOB entropyBlob = new DATA_BLOB();//though atm I'm omitting entropy so this will just be empty.
CRYPTPROTECT_PROMPTSTRUCT prompt = new CRYPTPROTECT_PROMPTSTRUCT();
InitPrompt(ref prompt);//make it empty.
try
{
// Convert ciphertext bytes into a BLOB structure.
try
{
// Use empty array for null parameter.
if (data == null)
data = new byte[0];
// Allocate memory for the BLOB data.
cipherTextBlob.pbData = Marshal.AllocHGlobal(data.Length);
// Make sure that memory allocation was successful.
if (cipherTextBlob.pbData == IntPtr.Zero)
throw new Exception(
"Unable to allocate data buffer for BLOB structure.");
// Specify number of bytes in the BLOB.
cipherTextBlob.cbData = data.Length;
// Copy data from original source to the BLOB structure.
Marshal.Copy(data, 0, cipherTextBlob.pbData, data.Length);
}
catch (Exception ex)
{
throw new Exception(
"Cannot initialize ciphertext BLOB.", ex);
}
// Call DPAPI to decrypt data.
bool success = CryptUnprotectData(ref cipherTextBlob, null, ref entropyBlob, IntPtr.Zero, ref prompt, CryptProtectFlags.CRYPTPROTECT_UI_FORBIDDEN, ref plainTextBlob);
// Check the result.
if (!success)
{
// If operation failed, retrieve last Win32 error.
int errCode = Marshal.GetLastWin32Error();
// Win32Exception will contain error message corresponding
// to the Windows error code.
throw new Exception(
"CryptUnprotectData failed.", new Win32Exception(errCode));
}
return Marshal.PtrToStringAuto(plainTextBlob.pbData);//convert your pointer back into a string. Not sure why PtrToStringBTSR doesn't work but Auto seems to.
}
catch (Exception ex)
{
throw new Exception("DPAPI was unable to decrypt data.", ex);
}
// Free all memory allocated for BLOBs.
finally
{
if (plainTextBlob.pbData != IntPtr.Zero)
Marshal.FreeHGlobal(plainTextBlob.pbData);
if (cipherTextBlob.pbData != IntPtr.Zero)
Marshal.FreeHGlobal(cipherTextBlob.pbData);
if (entropyBlob.pbData != IntPtr.Zero)
Marshal.FreeHGlobal(entropyBlob.pbData);
}
}
public static Byte[] Encrypt(this SecureString self, int length)
{
IntPtr unmanagedString = Marshal.SecureStringToBSTR(self);//get the basic unmanaged string representation
int len = Marshal.ReadInt32(unmanagedString, -4) + 2; //get the length of the bstr structure from it's index, this doesn't include the null bytes hence + 2.
DATA_BLOB plainTextBlob = new DATA_BLOB();//initiate our blobs
DATA_BLOB cipherTextBlob = new DATA_BLOB();
DATA_BLOB entropyTextBlob = new DATA_BLOB();
CRYPTPROTECT_PROMPTSTRUCT prompt = new CRYPTPROTECT_PROMPTSTRUCT();
try
{
//Processing code here. Resist the urge to Marshal.PtrToStringBSTR.
plainTextBlob.cbData = len;//set the length of the array
plainTextBlob.pbData = unmanagedString;//set the data to our pointer.
InitPrompt(ref prompt);
// Call DPAPI to encrypt data.
bool success = CryptProtectData(ref plainTextBlob, null, ref entropyTextBlob, IntPtr.Zero, ref prompt, CryptProtectFlags.CRYPTPROTECT_UI_FORBIDDEN, ref cipherTextBlob);
// Check the result.
if (!success)
{
// If operation failed, retrieve last Win32 error.
int errCode = Marshal.GetLastWin32Error();
// Win32Exception will contain error message corresponding
// to the Windows error code.
throw new Exception(
"CryptProtectData failed.", new Win32Exception(errCode));
}
// Allocate memory to hold ciphertext.
byte[] cipherTextBytes = new byte[cipherTextBlob.cbData];
// Copy ciphertext from the BLOB to a byte array.
Marshal.Copy(cipherTextBlob.pbData,
cipherTextBytes,
0,
cipherTextBlob.cbData);
// Return the result.
return cipherTextBytes;
}
finally
{
Marshal.ZeroFreeBSTR(unmanagedString); //free the buffer holding our secret
if (cipherTextBlob.pbData != IntPtr.Zero)
Marshal.FreeHGlobal(cipherTextBlob.pbData);
}
}
//The below regions are all the PInvoke signatures. These translate C++ commands into usable C# commands. These come directly from pinvoke.net
#region PInvokeSignatures
/// <summary>
/// Initializes empty prompt structure.
/// </summary>
/// <param name="ps">
/// Prompt parameter (which we do not actually need).
/// </param>
private static void InitPrompt(ref CRYPTPROTECT_PROMPTSTRUCT ps)
{
ps.cbSize = Marshal.SizeOf(typeof(CRYPTPROTECT_PROMPTSTRUCT));
ps.dwPromptFlags = 0;
ps.hwndApp = IntPtr.Zero;
ps.szPrompt = null;
}
[DllImport("Crypt32.dll", SetLastError = true, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CryptProtectData(
ref DATA_BLOB pDataIn,
String szDataDescr,
ref DATA_BLOB pOptionalEntropy,
IntPtr pvReserved,
ref CRYPTPROTECT_PROMPTSTRUCT pPromptStruct,
CryptProtectFlags dwFlags,
ref DATA_BLOB pDataOut
);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct DATA_BLOB
{
public int cbData;
public IntPtr pbData;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct CRYPTPROTECT_PROMPTSTRUCT
{
public int cbSize;
public CryptProtectPromptFlags dwPromptFlags;
public IntPtr hwndApp;
public String szPrompt;
}
[Flags]
private enum CryptProtectPromptFlags
{
// prompt on unprotect
CRYPTPROTECT_PROMPT_ON_UNPROTECT = 0x1,
// prompt on protect
CRYPTPROTECT_PROMPT_ON_PROTECT = 0x2
}
[Flags]
private enum CryptProtectFlags
{
// for remote-access situations where ui is not an option
// if UI was specified on protect or unprotect operation, the call
// will fail and GetLastError() will indicate ERROR_PASSWORD_RESTRICTION
CRYPTPROTECT_UI_FORBIDDEN = 0x1,
// per machine protected data -- any user on machine where CryptProtectData
// took place may CryptUnprotectData
CRYPTPROTECT_LOCAL_MACHINE = 0x4,
// force credential synchronize during CryptProtectData()
// Synchronize is only operation that occurs during this operation
CRYPTPROTECT_CRED_SYNC = 0x8,
// Generate an Audit on protect and unprotect operations
CRYPTPROTECT_AUDIT = 0x10,
// Protect data with a non-recoverable key
CRYPTPROTECT_NO_RECOVERY = 0x20,
// Verify the protection of a protected blob
CRYPTPROTECT_VERIFY_PROTECTION = 0x40
}
[DllImport("Crypt32.dll", SetLastError = true, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CryptUnprotectData(
ref DATA_BLOB pDataIn,
StringBuilder szDataDescr,
ref DATA_BLOB pOptionalEntropy,
IntPtr pvReserved,
ref CRYPTPROTECT_PROMPTSTRUCT pPromptStruct,
CryptProtectFlags dwFlags,
ref DATA_BLOB pDataOut
);
#endregion
}